# 有职 App 开发进度与规划文档 > 生成日期:2026-09-10 > 最后更新:2026-09-17(**全仓死控件清零:9 处 → 0 处**,企业沟通 / 人才推荐的 Tab 与搜索真正生效、申诉举证接真实选图、面试评分草稿落库、入职清单可增删与导出,见 4.7) > 上一版:2026-09-15(登录方式改为「验证码 / 密码 / 运营商一键登录」三通道 · 密码闭环打通 · 登录态未写入的严重缺口已修复 · **注册引导链闭环:采集资料真正落库并被主界面读取,删除假首页 P23**,见 4.3 · **简历域死控件清零:新增可写通道与 4 个编辑/预览页,导出真出 PDF、分享真拉面板**,见 4.4 · **127 个接口函数全部预留注释版后端请求,新增审计脚本**,见 4.6) > 范围:项目健康审查 + 已开发 / 待开发清单 > 结论先行:**当前项目可运行**(`tsc` 0 错误、`eslint` 0 error、路由 0 失效跳转、死控件 0 处、219 例测试全绿),无需修正阻塞错误。 --- ## 一、健康审查结论 | 检查项 | 命令 / 方式 | 结果 | |---|---|---| | 类型检查 | `npx tsc --noEmit` | ✅ 0 错误(Exit 0) | | 代码规范 | `npx eslint .` | ✅ 0 error / 0 warning(2026-09-17 复跑) | | 路由闭合 | `node scripts/check-routes.mjs` | ✅ 72 路由全部存在,0 失效跳转(12 个「孤立页」经核对为动态路由 / Tab 页误报,见 4.8) | | 死控件审计 | `node scripts/check-affordances.mjs` | ✅ **0 处**(2026-09-17 清零,9 → 0,见 4.8) | | 后端接口预留审计 | `node scripts/check-backend-stubs.mjs` | ✅ 137 个接口函数全部留有注释版真实请求(见 4.6) | | 单元测试 | `npm test` | ✅ 18 个文件 **219 例全绿**(2026-09-17) | | 入口接线 | 读 `app/_layout.tsx` / `app/index.tsx` | ✅ 根栈 → 入口围栏 → 双端 Tab 完整 | | 应用配置 | `app.config.js` | ✅ name「有职」、scheme `youzhi`、plugins 正确 | **判定**:项目处于可启动状态(`expo start` 即可运行),未发现阻塞运行的错误。本次未进入"修正"环节。 --- ## 二、技术栈与架构 - **框架**:Expo SDK 57 · React Native 0.86.3 · React 19 · TypeScript 6 · Expo Router(文件路由) - **双端同构**: - 求职端 `(tabs)`:岗位 / 沟通 / 成长 / 我的(落地 `jobs`) - 企业端 `(enterprise)`:岗位 / 人才 / 沟通 / 我的(落地 `job-management`) - **分层(src/)**:`types`(领域模型) · `api`(7385 行 mock 数据层) · `hooks`(语义化取数) · `components`(42 组件) · `context`(Auth/Role) · `theme`(colors token) · `constants` · `utils` · `lib`(storage / wechat-sdk / imagePicker / resumeExport / shareFile / cache) - **数据链路**:页面 → Hook(`useXxx`) → API(`fetchXxx`) → 后端。当前全 mock,签名已固化,接真实后端只改 `src/api/api.ts` 函数体。 - **配色治理**:统一走 `src/theme/colors.ts`,禁止硬编码十六进制;透明度用 token + `15`/`20` 后缀。 --- ## 三、已开发模块(按业务域) > 页面规模以行数为代理指标;5 行薄壳(如隐私政策 / 用户协议)是合理架构——内容由 `LegalDocScreen` 按 `docId` 加载,**非空壳**。 ### 3.1 用户端(求职端) | 业务域 | 路由 | 关键页面(行数) | 规模 | 状态 | |---|---|---|---|---| | 引导 / 注册 | `/guide` `/register` `/onboarding` | guide(194) · register(69) · onboarding(488) | 中 | 已开发 | | 岗位 | `/jobs` `/jobs/[id]` `/jobs/compare` `/jobs/rank` `/jobs/offer-compare` `/jobs/weight-settings` | jobs(756) · [id](612) · compare(306) · rank(753) · offer-compare(676) · weight-settings(285) | 大 | 已开发 | | 沟通 | `/chat` `/chat/[id]` | chat(556) · [id](750) | 大 | 已开发 | | 成长 | `/growth` `/game` `/game/level` `/progress` `/assessment` `/ability` `/holland*` `/interview*` `/resume*` `/job-preference` | 多页 300–600 行 | 大 | 已开发(逐关内容见四.2) | | 我的 / 设置 | `/profile` `/settings` 及 10 个子页 | profile(743) · settings(262) · about / account / account-cancel / privacy* / notification / delete-data / user-agreement / privacy-policy | 大 | 已开发 | | 增值 / 合规 / 工具 | `/legal*` `/tools/*` `/hr-evaluation` `/audit-report` `/appeal` `/operation-rules` `/enterprise-certification` `/personal-data-export` `/candidate-detail` `/article/[id]` `/history` `/favorites` `/job-evaluations` | 多页 300–720 行 | 大 | 已开发 | ### 3.2 企业端 | 业务域 | 路由 | 关键页面(行数) | 规模 | 状态 | |---|---|---|---|---| | 岗位管理 | `/job-management` `/job-publish` `/job-evaluations` | job-management(645) · job-publish(633) · job-evaluations(752) | 大 | 已开发 | | 人才 | `/talent-recommendation` `/candidate-detail` | talent-recommendation(684) · candidate-detail(719) | 大 | 已开发 | | 沟通 | `/enterprise-chat` | enterprise-chat(611) | 大 | 已开发 | | 我的 | `/enterprise-overview` | enterprise-overview(594) | 大 | 已开发(【设置】入口已接线 `/settings`;Tab 选中色已统一 `primary`) | > 企业端设置类页面复用根级 `app/settings/*`,符合「设置类统一收纳于【设置】页」规范。 --- ## 四、待开发 / 进行中 / 风险 ### 4.1 既定任务(来自项目规范,需逐项核实) - **企业端三项**(历史记录为待办,代码审查显示已基本完成): 1. Tab 选中色统一回 `primary` —— ✅ 代码已用 `colors.primary` 2. 我的页补【设置】入口 —— ✅ `enterprise-overview.tsx` 第 281 行已 `router.push('/settings')` 3. 统一快捷入口话术 —— ✅ 已完成(2026-09-10):`MOCK_ENTERPRISE_QUICK_ACTIONS` 与沟通页 `QUICK_ACTIONS` 中 6 处 label 已对齐规范词(人才库→人才推荐、评价管理→岗位评价管理、沟通→企业沟通、面试日程/面试安排→面试评分),与「我的」菜单、`/interview-score` 路由名一致。 4. 沟通页快捷操作接线 —— ✅ 已完成(2026-09-10):`QuickActions` 死入口已绑定 `onPress`;批量发邀请→`/talent-recommendation`、面试评分→`/interview-score`(根级自包含页、可返回)走跳转,批量发Offer / 群发通知无对应企业端页、按项目约定弹 `Alert '功能开发中'`。tsc/eslint 0 问题。 - **微信登录**:⚠️ **入口已于 2026-09-15 按产品决定摘除**(丁总:前端不做 QQ / 微信登录)。三分支(`new` / `active` / `deleted`)状态机、`P2W` / `B1` 页面、`src/lib/wechat-sdk.ts`、相关 hook / 类型 / 接口**全部保留未删**,仅 `P1Login` / `P2PhoneCode` 不再挂入口;`app/register.tsx` 的 `P2W` / `B1` case 亦保留。**接入路线图仍见 `微信登录接入方案.md`**(该文档开头已加摘除说明)。恢复入口 = 重新挂上按钮与 `useWechatLogin` 状态机。 - **登录方式(2026-09-15 改造)**:现为「手机号 + 验证码」(`P2`)·「手机号 + 密码」(`P2P`)·「运营商一键登录」(`P2Q`)三通道,微信入口已摘除。 - **修复了一个此前存在的严重缺口**:`P2PhoneCode` / `P2QuickLogin` 原先只 `navigate('P3')` 而**从不写入登录态**(`useLogin` / `loginByPhone` 写好却无人调用),全项目唯一写 token 的地方是微信链路 —— 入口一摘 `isTokenValid` 将恒为 false、每次冷启动都被打回登录页。现三条通道均已接入 `useAuth().login()`。 - **密码闭环**:`P2P` 密码登录 → `P2R` 设置/重置密码(**同页两阶段**:验证手机号 → 设新密码,成功后直接下发登录态)→ 设置页 `/settings/password` 修改密码(改完强制重登)。「登录即注册」的用户天生无密码,**首次获得密码即走「忘记密码」入口**,不额外插入注册步骤。 - 密码规则(8–20 位、需含字母和数字)在 `src/api/api.ts` 的 `validatePassword` 统一计算,错误码 → 文案映射在 `src/constants` 的 `PASSWORD_RULE_LABEL`;新增测试 `src/__tests__/password.test.ts`(24 例)。 - 新增公共件:`src/components/PasswordField.tsx`(密码输入 + 明文切换)、`src/hooks/useAgreementGate.ts`(协议门卫,此前逐字复制 3 份)、`src/utils/validators.ts`(手机号 / 验证码规则单一来源)。 - **按钮底色全局改深**:`components/onboarding/ui/PrimaryButton.tsx` 的 `colors.primary`(#2C83FD) → `colors.primaryBtn`(#1F6FDB)。原白字对比仅 **3.64:1,不满足 WCAG AA**;`primaryBtn` 为 **4.82:1**,且 AGENTS.md 配色规范本就要求 primary 不作白字按钮底。**副作用:整条注册流程按钮视觉变深,如已对外出图需重新确认。** - 账号与安全页的「微信账号」项已随入口一并移除(`MOCK_ACCOUNT_SECURITY`),避免留下有出口无入口的死项;「修改登录密码」项补上 `route: '/settings/password'`,此前点击无任何反应。 ### 4.2 功能完整性缺口(标注【待确认】为推断项,需与产品对齐) - **通关小游戏(7 关)**:框架页 `game.tsx` + `game/level.tsx` 已建,7 关(认识自己 → 能力 → 岗位 → 企业 → 简历 → 面试 → 签约)**内容与数据已完整**:`MOCK_GAME_LEVELS` 共 22 题(self 4 / 其余各 3),题型含单选/多选/输入,配套解析与 `tags` 用于岗位推荐。评分(≥60 通关)、解锁、进度聚合逻辑均在 `submitGameLevel` 实现。✅ 2026-09-10 完成内容增厚(每关由 1–2 题补至 3 题左右)。 - **阿布阿诺 AI 法律服务**:免费层(咨询 / 模板 / FAQ)页已建;付费层 `legal/upgrade.tsx` 已建,**付费 / 支付链路已接通(mock 支付闭环)**——`legal/checkout.tsx` 选套餐→选支付方式→模拟支付→会员态激活并 AsyncStorage 持久化;`legal.tsx` / `upgrade.tsx` 套餐卡可点击进入结算,会员态在返回时即时刷新。真实支付 SDK 后续仅换 `api.ts` 的 `createLegalOrder` / `payLegalOrder` 函数体。**付费后权益已兑现**:新增 `legal/service.tsx` 会员服务使用页——未开通点会员服务仍引导开通,已开通则进入使用页(服务说明 + 引导语 + 需求输入框 + 提交申请 + 申请记录持久化),`legal.tsx` 会员卡标签按态显示「会员 / 已开通」。 - **岗位评价发布(双向透明评价生态核心)**:✅ 2026-09-10 接通。此前 `jobs/[id].tsx` 底部发布按钮为「功能开发中」占位——产品核心定位即评价体系却无法发布。现已新增 `createJobReview`(AsyncStorage 键 `job:reviews`,按 jobId 归属),`fetchEmployeeReviews` 合并「本人发布(置顶)+ 内置评价」,`useJobDetail` 暴露 `publishReview`,页面接线含空输入拦截、发布中 loading、成功后清空输入框并自动刷新列表。 - **隐私政策 / 用户协议内容**:`src/api/api.ts` 中 `MOCK_PRIVACY_POLICY` / `MOCK_USER_AGREEMENT` 已含完整合规草案(各 9–10 章),**18 岁门槛已落地**(隐私政策第六章 + 用户协议前言)。仅余【待确认】占位待法务/产品替换:公司全称、注册地址、客服邮箱/电话、个人信息保护负责人邮箱、管辖法院。属**上架硬性审查项**,占位填妥即可提交审核。 ### 4.3 注册引导链闭环(2026-09-15 完成) **问题**:整条引导链在空转 —— `components/onboarding/screens/` 下 20+ 个引导屏**没有一处调用 API**,用户从 P2 登录到 P22 完成填的姓名 / 城市 / 期望职位 / 期望薪资 / 技能 / 学历 / 公司**全部只写进 `useOnboardingFlow` 的内存 state**(该 Provider 只包在 `/register` 路由内),进程一结束全部蒸发。主界面因此读不到用户填的任何东西:「我的」页偏好**恒为** mock 的「前端开发工程师 / 北京 / 25-35K」。 - **落库闭环**:新增持久化模型 `src/types/onboarding.ts`(`OnboardingProfile` 与含登录瞬时字段的 `OnboardingData` 拆开,`phone` / `code` / `agreed` / `wechatTicket` 不落库)+ API `saveOnboardingProfile` / `fetchOnboardingProfile` / `clearOnboardingProfile`(键 `youzhi:onboarding:profile`)。写入为**步骤粒度**(`navigate` / `back` / `finish` 各提交一次,不逐键写,避免每次按键都打存储);Provider 挂载时水合,流程仍从 P1 开始(按决定不做断点续填)。 - **主界面读取(让填的生效)**:`fetchJobPreference` 改为读持久化资料并映射(工作类型默认「全职」;未采集项显示 `UNSET_LABEL`「未设置」,不再返回 `MOCK_JOB_PREFERENCE` 常量);`fetchJobList` 在**默认推荐视图**(`sortBy==='comprehensive'` 且未显式筛选城市 / 关键词)叠加偏好加分(城市 +15 / 职位 +20 / 薪资区间重叠 +10,权重在 `src/constants`),**显式筛选一律不叠加偏好加分**;`fetchUserProfile` 的姓名 / 头像字 / 简介改用采集值(有资料就只用资料,**不再用 mock 的「李明 / 3年工作经验·本科·北京」补空**,空值由页面渲染「未设置」或隐藏该行)。`app/(tabs)/jobs.tsx` 默认城市由写死的「北京」改为「全部城市」—— 不改这一处,偏好加分永远不生效。 - **简历页接采集资料**:`fetchResume` 原先只覆盖姓名与头像,其余(学历「北京大学」、工作经历「字节跳动」、技能 Vue/React)仍是演示人设「李明」的 —— 用户会在自己的简历里看到别人的学历和公司。现改为 `buildResumeFromProfile` 逐项映射采集值:教育背景(P20 学校 + P18 学历/学习形式)、工作经历(P12 公司 + P11 职位 + P13 入职年月 + P16 工作内容,在职才写「至今」,离职时结束时间留空而不编)、技能(P14)、求职意向;**未采集项一律留空**(专业、入学毕业年份、项目经历、自我评价、手机号、邮箱),页面按空值隐藏对应行,不再拼出「· 本科」「2022.07 - 」这类半截内容,空板块渲染「未填写」。P9 的求职状态选项提为常量 `JOB_STATUS_OPTIONS`(`JOB_STATUS_INACTIVE` / `JOB_STATUS_EMPLOYED_PREFIX` 同处),简历「积极 / 不积极」与「是否至今」的判定与页面读同一份,避免两处文案分叉。 - **求职意向只认一个答案**:新增 `resolveJobPreference()`(显式编辑 > 引导资料推导 > 空值),「我的」页偏好卡片与简历「求职意向」都读它 —— 原先简历的求职意向只读引导资料,用户在求职意向编辑页改过之后,两个页面会给出不同答案。顺带把「基本信息的所在城市」留空:引导采集的是 P4「**期望**城市」,不能冒充「现居城市」(期望城市仍正确显示在求职意向里)。 - **「优化简历」不再把简历换成别人**:`optimizeResume` 原样返回 `MOCK_OPTIMIZED_RESUME`,即演示人设「李明 / 北京大学 / 字节跳动」的整份简历 —— 用户点一下按钮,自己的简历就变成了别人的。现改为在**用户自己的简历**上套用 mock 评分块;AI 改写的正文(经历描述 / 自我评价)mock 阶段生成不了,因此不替换用户真实填写的内容,真实后端就绪后由后端返回改写结果。 - **简历页「编辑 >」接上真实出口**:求职意向卡的「编辑 >」原先无 `onPress`,现指向已存在的 `/job-preference` 编辑页,并在简历页加 `useFocusEffect(refresh)` —— 该页保存的是显式偏好,返回时不重新取数就会显示旧值(`useAsyncData` 只在挂载时取数)。 - **清演示人设**:`INITIAL` 里预填的「杨幸幸 / 女 / 上海 / 前端开发工程师 / 本科」等全部清空。此前**用户全程不操作也能一路点到落地**,而采集出的「资料」没有一项是他选的。清空后逐页补空值兜底(P4 / P8 / P10 / P13 / P18 等页的 `active` 门槛;`YearMonthWheel` 改为「未选中 + 请滑动选择」,此前把空值钳到索引 0,滚轮显示「2026年」而 state 仍是空串,会静默写入错误年份);`agreed` 由 `true` 改 `false` —— 预勾选不构成《个人信息保护法》要求的明示同意,且它让协议弹窗(`useAgreementGate`)此前永不触发。 - **缺陷修复**:`navigate()` 去重压栈(修 P16 ↔ P17 往返后返回键错乱);P3「退出登录」补 `useAuth().logout()`(原先只清流程 state,token 未清,退出后重进仍被判为已登录);B3 企业认证重做(营业执照可点可传、联系人不再写进 `data.school`、提交走 `submitEnterpriseCertification`、去掉本地 `useState` 翻出的假「等待审核」态与开发口径文案);P21 头像接入 `expo-image-picker`(**新增依赖**)后拍照 / 相册 / 5 个预设头像各自生效。 - **死控件(点零反馈)逐项处理**:P6 / P11 搜索框接真实过滤(跨分类匹配 + 空结果提示);P6 写死的进度角标「3/3」改为按 `ONBOARDING_STEP_ORDER` 计算的「第 3 / 16 步」;P6 / P16 的「保存」文案改为「跳过,直接进入」(它实际调 `finish()` 结束注册,并不「保存」);P2 与 P2W 的「接收不到短信」改为可点并给出可执行指引;P1 / P2Q 页脚与 P2Q 条款链接按下条处理。 - **不编造数据(本轮的处理原则)**:页脚「服务热线 / 举报监督电话」改为常量 `SUPPORT_HOTLINE` / `SUPERVISION_PHONE`(**现为空,为空则整块不渲染**,填入号码即自动出现并接 `tel:` 拨号;与隐私政策的【待确认】客服电话同源,属同一批待补信息);「资质证照」入口一并去掉 —— 项目内**没有**该页面,`app/legal.tsx` 是「阿布阿诺法律服务」付费咨询页,指过去是误导(**需新建资质证照页 + 提供证照信息**);P2Q 原先以「本机号码」名义显示写死的 `150****9367`,改为无号码时只显示槽位标签「本机号码」;P2W 的手机尾号回落值 `9367` 同理去掉;P5「自动定位:上海」(不做定位却把「上海」写死成选中值)与 P2W「语音接听验证码」(无语音通道)两个入口**移除**。 - **删 P23 假首页**:`P23Home` 是写死上海 4 条职位的演示页,与真 `(tabs)/jobs` 重复,且含两颗绑同一回调的假权限弹窗;删除后 P22 与 P6 / P16 的快速通道由 `useRegisterFinish` 直接 `replace` 进真主界面。同步清理悬空接口 `saveAndEnter` / `canGoBack` / `meta`(`meta` 恒为 `{time:'',battery:0}`,21 个屏仍在传参、`ScreenShell` 收到即丢弃)与插画占位上的素材说明文字(`Illustration` 在 `source` 为 null 时不再渲染 `hint` —— 它会把「手持 OFFER 职场男性插画」这类开发备注直接画在用户界面上)。 - **测试**:新增 `job-match.test.ts`(14 例,锁定匹配规则与「显式筛选不叠加偏好」边界)、`salary-options.test.ts`(10 例)、`onboarding-profile.test.ts`(映射往返 + 字段漂移检测)、`resume-profile.test.ts`(12 例:采集齐全逐项映射、一路跳过时各板块为空、离职不写「至今」、求职意向读显式编辑过的偏好、AI 优化后仍是本人简历、「不再出现演示人设」字符串扫描)、`user-profile.test.ts`(6 例:姓名/头像字/简介映射、一路跳过时不显示演示人设、头像原始值按预设 key / 本地 URI 两类透传)、`onboardingStepLabel`(3 例)。`npm test` **97 例全绿**;`tsc` / `eslint` / `check-routes`(67 路由 / 0 失效跳转 / 0 孤立页)均 0 问题。 - **待验证 / 待补**:`expo-image-picker` 的原生权限文案(`app.config.js` 的 `photosPermission` / `cameraPermission`,并显式关掉插件默认申请的麦克风权限)**只在出包时生效**,Expo Go 用的是它自己的 `Info.plist`,需真机出包验证;手工路径(真机走完 P1→P22 →「我的」页偏好与所填一致 → `/jobs` 偏好岗位上浮;引导途中杀 App 重进数据仍在)**尚未在设备上执行**。 - **落地页仍为 mock / 仍是死控件(本轮未动,待产品拍板)**:① `/resume` 的「简历评分 72 分」与 5 条 AI 优化点、`/profile` 的投递 28 / 收藏 12 / 沟通 8 / Offer 3、八大维度评分与勋章,全是与用户无关的写死数据(AI 评分与统计属产品能力,需真实算法或后端)。**「评分保留展示、不加示例标注」已于 2026-09-15 拍板**(见 4.6 末条),但「写死 mock」这一事实不变;② ~~`/resume` 仍有 10 处 `Pressable` **无 `onPress`**~~ **已修(2026-09-15,见 4.4)**:简历域 26 处死控件全部接线,`check-affordances.mjs` 简历域命中 0;③ ~~P21 采集的头像落地后无消费方~~ **已修**:「我的」页头部改为显示用户真正选的头像 —— `UserProfile` 新增 `avatarKey`(**存原始值**:预设素材 key 或本地图片 URI,API 层只透传,解析在组件层 `isAvatarPreset()`,因为 `src/types` 不可 import `components/`),选过就渲染真实头像,没选过才回落到姓名首字。**注意**:拍照 / 相册得到的是 `expo-image-picker` 的缓存目录 URI,系统清缓存后该图片可能失效(渲染空白圆圈,不崩);接真实后端时应改为上传取回永久 URL。 ### 4.4 简历域功能补齐(2026-09-15 完成) **问题**:引导链闭环之后,简历成了新的瓶颈 —— `app/resume.tsx` 与 `app/resume/export.tsx` 共 **26 处** `Pressable` 点下去没有任何反应(顶部「保存」「导出」、「编辑基本信息」、四个「添加*」、「AI 优化 / AI 润色 / 一键全部优化」、模板 / 纸型 / 方向 / 预览 / AI 智能优化 / 分享简历),且**简历根本没有可写通道** —— `fetchResume()` 每次从引导资料现算派生,storage 里没有任何简历键。根因不是「忘了绑回调」,而是无处可写,所以先建数据层。处理口径:**实现功能,不是删按钮**。 - **覆盖层数据层**(新增类型 `ResumeOverride` / `ResumeItemInput` / `ResumeAIActionResult` / `ResumeExportOutcome`):简历按「**用户显式编辑过的板块 > 引导资料派生**」取值,键 `youzhi:resume:override`;**键出现(含 `[]`、`''`)即归用户,键不存在才用引导派生** —— 删光教育经历不会再冒出引导那条(`undefined` 被 `JSON.stringify` 丢弃,正好用 `??` 而非 `||` 区分「未编辑」与「清空」)。整表保存而非逐条写:逐条写会以空数组为基底,把引导派生的整块改丢。登出时一并清除该键(`clearResumeOverride`),避免换账号后看到上一个人的简历。 - **7 个可写接口 + 4 个新页面**:`saveResumeBasicInfo / saveResumeSelfEvaluation / saveResumeSkills / saveResumeEducation / saveResumeExperience / saveResumeProjects / saveResumeSnapshot`;页面 `/resume/basic`、`/resume/education`、`/resume/experience`、`/resume/project`(编辑)、`/resume/preview`(只读预览)。**多字段板块走独立编辑页,技能与自我评价走页面内弹窗**(按字段量分开处理:弹窗装不下教育/工作/项目的字段量,编辑页对单字段又太重)。新增条目**不带 `id` 字段**(不是给空串 —— 空串不是 nullish,会被当成「已有 id」写成空),id 由 API 层补,同毫秒连加两条也不会撞号。 - **头部「保存」补充功能**:把当前简历 6 个可编辑板块整体写入覆盖层 + 保存时间戳(`youzhi:resume:saved-at`),页面显示「上次保存:MM-DD HH:mm」(从未保存则不显示该行)。 - **AI 类按钮:保留入口 + 坦诚提示**(`optimizeResumeExperience` / `polishResumeSelfEvaluation` / `optimizeResumeWithJD`):mock 恒返回 `available:false` + 一句「AI 改写需要后端模型支持,当前版本暂未开放;你的简历内容不会被改动」,页面只提示、不改简历。**绝不在正文未改写时把 `aiOptimized` 置 true** —— 那会让列表显示「已优化」徽标,用户等于被自己的界面骗了。参数加 `_` 前缀,去掉下划线即表示能力已接入。 - **导出设置真正保存**:`saveResumeExportSettings` 原先是 `Object.assign(DEFAULT_EXPORT_SETTINGS, …)` —— 只改模块内常量,杀进程即丢,「保存」按钮实则保存不了,还会污染其它调用方读到的默认值。现改落 `youzhi:resume:export-settings`(读时用默认值补齐后加字段,存储损坏回落默认)。默认文件名原为演示人设「李明_前端工程师简历」,现为空串,由 `resolveExportFileName` 兜底成「姓名_期望职位简历」。模板 / 纸型 / 方向的选择弹层**就地在页内改设置、随「保存」落盘**(跳新页会出现「模板已存、格式没存」)。 - **导出与分享真做**(新增依赖 `expo-print` / `expo-sharing` / `expo-file-system`):「立即导出」原先返回写死的 `https://cdn.example.com/resume/xxx.pdf` —— 不产出文件、页面还丢弃返回值、不给任何提示,是纯假成功。现改为**本机真出 PDF**:`buildResumeExportHtml`(纯函数,空板块整段略去、用户文本全转义、模板 id 白名单校验)→ `printToFileAsync` 按纸型 / 方向的真实页宽页高打印 → 重命名成用户设的文件名(打印引擎生成的是随机临时名,不改的话分享面板里是一串乱码);「分享简历」拉起系统面板分享这份文件。**封面页、自荐信、隐藏联系方式、纸型 / 方向四个开关由此第一次真正生效**(此前全是装饰):自荐信以自我评价为正文,**自我评价为空时不生成**(没有素材就编一封自荐信属于伪造内容)。Word / 图片格式与导出密码需后端导出服务,在**选中的那一刻**与导出成功后都写明。 - **未做的验证(诚实口径)**:真机 smoke test(生成 PDF 并拉起分享面板)**尚未执行**;Expo Go 是否内置 `expo-print` / `expo-sharing` 本轮无法联网核实(Expo 文档被网络策略拦截)。已完成的替代验证:`expo-doctor` 20/21 通过(依赖版本与 SDK 57 要求一致)、`expo export --platform android` 打包通过(新模块 import 可解析)、两个原生封装失败时一律返回 `{ ok: false, message }` 并弹出原因,**不返回假链接**。若 Expo Go 确缺模块,症状是**启动即崩**(`requireNativeModule` 在 import 期执行,非静默失败),届时改用 development build 出包即可,代码无需改。 - **审计工具常驻**:新增 `scripts/check-affordances.mjs`(扫「无处理函数 / 空处理函数」的 `Pressable`,支持 `affordance-ok` 豁免标记),与 `check-routes.mjs` 配对,加 `npm run check:affordances` / `check:routes` / `typecheck` 三个 script(暂不进 CI 硬门槛:其余 22 处未修完,`--error` 会一直红)。全仓死控件由 **37 → 22**,**简历域 0 处**(**22 → 0 于 2026-09-17 完成,见 4.8**)。 - **测试**:新增 `resume-override.test.ts`(26 例:覆盖层语义、清空不回落到引导、编辑引导派生条目不改丢、同毫秒 id 不撞号、技能 trim 去重、存储损坏回落、AI 三接口调用前后简历深相等)、`resume-export.test.ts`(20 例:空板块不产出、隐藏联系方式、HTML 转义、模板 id 白名单、自荐信无素材不生成、纸型页宽、文件名清洗)。`npm test` **155 例全绿**;`tsc` / `eslint` / `check-routes`(72 路由 / 0 失效跳转)均 0 问题。 - **本轮未动(待续做,已探明落点;截至 2026-09-17 均已清完,见 4.8)**:`app/job-evaluations.tsx` 筛选与回复(`replyJobReview` 与 hook 的 `reply` **已存在**,接线即可)、`app/holland-result.tsx` 行动卡(`HollandAction.route` 已定义)、`app/(enterprise)/enterprise-chat.tsx` 待处理任务项、`app/interview-score.tsx` 历史与草稿、`app/(tabs)/chat.tsx` 4 处(加入会议 / 导航需 `Linking` 封装,仓库 0 处)、`app/(tabs)/jobs.tsx` 搜索框 state 未传给 `useJobList`(**真 bug**)与 `FILTERS` 三项未实现、`app/tools/*` 若干。**注意**:`jobs.tsx` 与 `resume.tsx` 里成对的 `filterPanel` / `modalSheet` 空 `onPress` 是**吞冒泡守卫,需保留**(已加豁免标记,不属死控件)。 ### 4.5 工程与上线 - **应用图标**:✅ 已完成(提交 `878710c`)。`assets/icon.png` 已替换为「有职」logo,实测 1024×1024,符合商店要求。 - **启动屏**:✅ 已接线。安装 `expo-splash-screen@57.0.9`,在 `app.config.js` 以 config plugin 形式配置 `assets/splash-icon.png`(1024×1024、`imageWidth: 200`、`resizeMode: 'contain'`、白底)。此前该资源为死资源且冷启动白屏。因应用为纯亮色(`userInterfaceStyle: 'light'`),未配置 `dark` 分支。**注意**:启动屏属原生配置,Expo Go 内不生效,需 `eas build` 出包后可见。 - **EAS / OTA**:`eas.json` 配置骨架(development / preview / production + submit)已建;`app.config.js` 的 `runtimeVersion` / `ios.bundleIdentifier` / `android.package` / `extra.eas.projectId` 字段已就位。**以下两项占位符会直接阻塞 `eas build` / `eas submit`,上架前必做**: 1. `owner: 'REPLACE_WITH_EAS_OWNER'` —— 需替换为你的 Expo 账号名; 2. `extra.eas.projectId: 'REPLACE_WITH_PROJECT_ID'` —— 需替换为真实 EAS 项目 ID。 两者均可由 `eas login` 后执行 `eas init` 自动注入,**不要手工填**。`ios.bundleIdentifier` / `android.package` 当前为 `com.youzhi.app`,可按开发者账号调整。 - **`updates`(OTA)块当前为注释状态,属有意为之**:见提交 `aa7b171`。占位的 `REPLACE_WITH_PROJECT_ID` 会让 Expo Go(开发期)启动时去拉取不存在的远程更新,报 "Failed to download remote update" 并蓝屏。待 `eas init` 注入真实 projectId 后再取消注释启用 OTA。 - **真实后端**:全量 mock(`src/api/api.ts` 7385 行),未切换真实 `fetch`。 - **技术债**:8 处 `any` 软警告已全部清理(图标断言改为 `keyof typeof Ionicons.glyphMap`、路由断言改为 `as string`),`eslint .` 现 0 error 0 warning,`tsc --noEmit` 保持 0 错误。 ### 4.6 后端接口预留约定(2026-09-15) **口径**(用户 2026-09-15 拍板):**需要展示数据的地方照常展示;每个数据函数都要把真实后端请求写好、注释着;接后端时放开注释即可。** - **落地**:`src/api/api.ts` 127 个 `export async function` **全部**在函数体内留了真实请求,分两式(约定写在该文件顶部): - **纯 mock 返回**的函数 → `// TODO: 真实请求(后端就绪后替换)` + 注释的 `const res = await fetch(...)` / `return res.json();` - **以本地存储为唯一真相源**的函数(引导资料、简历编辑覆盖层、导出设置、薪资记录等)→ 单行 `// TODO: 后端就绪后改为 await fetch(...);当前以本地存储为唯一真相源` —— 放开时要**把本地写换成请求**,不是加一行。 - **AI 能力**(简历三处):参数名带 `_` 前缀 = 尚未接入;**去掉下划线 + 放开注释** = 该能力上线。 - **本轮补齐**:原先 29 个函数(简历域 6 + 运营/企业端/游戏/支付/隐私设置 23)正文里连注释版请求都没有,现已全部补上;放宽的判定是「函数体里出现过 `fetch(`」,**注释着的也算**,因此不依赖注释写法。 - **常驻审计**:新增 `scripts/check-backend-stubs.mjs`(`npm run check:backend-stubs`,与 `check-routes` / `check-affordances` 配对)。确实不该接后端的函数,在函数体内标 `backend-stub-ok` 豁免。与 `check-affordances` 一样**暂不进 CI 硬门槛**。 - **不做的**:不为了「看起来有接口」而编造未定义的 URL —— 上表 29 个函数的 `@url` 在各自 JSDoc 里本来就已定义,本轮只是把**已定义的接口**落成注释代码。 - **沿用上轮口径**:`/resume` 的「简历评分 72 分」**保留展示、不加「示例」标注**(评分是产品的 AI 能力,接口已在 `fetchResume` 内预留注释版,后端就绪后由后端下发);但它目前仍是写死的 mock,与用户数据无关,这一点不变。 ### 4.7 引导页重做 / 运营商一键登录修复(2026-09-16 批次) > 该批次与 4.8 同属长期未提交的工作区改动,一并入库(见 4.8 末条)。 **① 一键登录取不到手机号 —— 根因不在取号,在页面根本没读结果。** `P2QuickLogin`(P2Q)有两个独立缺陷叠加,表现为「号码永远显示占位」: | # | 缺陷 | 现状态 | |---|---|---| | 1 | 页面读的是 `data.phone` —— 该字段**只有短信登录通道(P2)才写入**,走一键登录进来时恒为空 | ✅ 改读 `useCarrierPhone()` 的 `maskedPhone` | | 2 | `loginByCarrier()` 返回的 `phone` **被直接丢弃**,后端换了真号码也传不下去 | ✅ 登录成功后 `setData({ phone: result.phone })` 写回引导资料(P2P/P2R 预填、B3 联系人电话同源) | - **新增取号链路**:`fetchCarrierPhone()`(接口层取号 + `maskPhone` 脱敏)+ `useCarrierPhone`(进页面即预取,loading / error / refresh 齐备)+ `loginByCarrier(carrierToken)` 换登录态。**空令牌直接 reject**,页面据此置灰「一键登录」(空令牌调登录接口必然失败,不如不让点)。 - **补上失败分支**(此前不存在:取号失败也照样显示号码、按钮照样可点):号码位分「正在获取 / 未能获取」,并给出**重新获取**与**改用验证码登录**两个出口;`carrierToken` 空的判定取代了原先写死的「本机号码」占位。 - **不冒充的能力**:《xx认证服务条款》由运营商在认证页内提供,本仓库没有该文档,故只弹如实说明,不做成能打开的链接;页脚服务热线为空时整块不渲染。 - **真实取号是端上原生 SDK(Expo Go 跑不了)**,当前 `fetchCarrierPhone` 返回写死号码 `15000009367` 用于跑通页面链路。接入路线图、两个硬约束(端上号码不可信 / 令牌一次性 5 分钟)、6 步执行清单与手动验收路径见新增文档 **`运营商一键登录接入方案.md`**(`src/api/api.ts` 的 JSDoc 原先就指向该文件名,此前文档缺失,本轮补齐)。 **② 引导页(`/guide`)重做 + 补「重新查看引导」入口。** - **有出口就必须有入口**:引导页此前只能由首次启动时 `/` 的重定向进入,看过一次就再也进不去。`app/settings.tsx` 菜单已加 `{ id: 'guide', title: '查看引导页', route: '/guide' }`。 - **两种进入方式、出口不同**(写错会把已登录用户甩回注册流程):首页跳来 → 看完 `replace('/register')`;设置页回顾(`isTokenValid`)→ 只 `back()`,不改引导标记、不进注册流程。 - **内容与配图重做**:四屏文案全部重写(聚焦差异化能力),配图改走新增组件 `ImageSlot`。 - **配图位带文字标注**(按 2026-09-16 要求「要配图的地方页面留有文字标注放什么图」):素材未到位时页面上画虚线占位框 + 「配图待补」+ **该放什么图**的说明(取自 `assets.ts` 的 `hint`),补图后标注自动消失。新增 4 个引导页素材位 `guideEvaluate` / `guideWeightDim` / `guideGameMap` / `guideLegalAI`,**换真图只改 `assets.ts` 里对应一行的 `source`**。与 `Illustration` 的分工:那个是小图标位(头像 / Logo),占位只画色块;`ImageSlot` 是内容大图位,占位必须自解释。 - **待补素材**:`assets.ts` 注册 17 个素材位,其中 **16 个 `source` 仍为 `null`**(含引导页 4 张,另一张 `logo` 已是真图)。 - **「首次启动没出现」的核查结论**:`app/index.tsx`(本轮未改动)在 hydration 完成后按 `onboardingSeen` 放行 —— 没看过即 `Redirect /guide`,默认值 `false`(`AuthContext` 的 `memoryAuth`),存储里没有该键时不会误判为已看过。因此**跳过引导只可能来自「本机已看过一次」**(看完即 `completeOnboarding()` 写入 `youzhi:auth`),属设计行为;要复现真·首次启动需清应用数据或卸载重装。**该结论为代码走查所得,本机未复现(无设备可测)。** - **顺带**:设置页「清除缓存」的体积改为运行时读取(`useAppCache` + `src/lib/cache.ts`),不再写死数字;页脚服务热线抽成 `SupportFooter`(号码为空则整块不渲染)。 ### 4.8 全仓死控件清零(2026-09-17) **结果**:`node scripts/check-affordances.mjs` **9 处 → 0 处**,全仓再无「点了没反应」的 `Pressable`。上一轮遗留的 9 处逐项落地(口径仍是**实现功能,不是删按钮**;确实做不到的就删入口或写明原因): 1. **企业沟通 `app/(enterprise)/enterprise-chat.tsx`**:待处理任务卡(待回复 / 待评分)此前无 `onPress` → 现「待回复」切到未回复 Tab、「待评分」跳 `/interview-score`,去向由接口层 `PendingTask.tabKey` / `route` 下发;搜索框接真实过滤;Tab 计数与列表**同源** —— `buildCommunicationTabs` / `buildPendingTasks` 均由会话列表推导(此前页面写死的 12/3/2/1/4 与 3 条示例数据对不上,会出现「Tab 上写 3、列表 0 条」)。 2. **人才推荐 `app/(enterprise)/talent-recommendation.tsx`**:顶部「搜索 + 漏斗」两枚图标 —— 漏斗无对应页面 → **摘掉**;搜索改成真关键词过滤(姓名 / 职位 / 技能标签 / 学校)。「调整筛选条件」原为弹「功能开发中」→ 改为真复位(切回全部 + 清关键词)。Tab 计数同样由候选人列表推导(`matchTalentTab`:匹配度 ≥90 / 985-211 院校名单 / 经验 ≥5 年 / 硕士及以上)。「查看人才市场报告」仍无对应页面 → 保留入口 + 明确提示,不假装跳转。 3. **申诉举证 `app/appeal.tsx`**:原为空 `onPress` → 接真实选图(新增 `src/lib/imagePicker.ts` + `useImagePick`),可拍照 / 相册 / 移除缩略图,证据随 `submitAppeal` 一并提交。**PDF 类材料后端上传接口不存在,在页面上写明**,而不是给个点不动的按钮。 4. **候选人详情 `app/candidate-detail.tsx`**:「发起沟通」→ `/chat/[id]`(此前点不动)。 5. **面试评分 `app/interview-score.tsx`**:「保存草稿」接真实本地暂存(键 `youzhi:interview:score-draft:`,**按面试 id 分开不串台**),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。 6. **入职准备清单 `app/onboarding.tsx`**:勾选状态**落库**(此前退出再进入勾选全丢,`completed` 恒为 3/18);新增自定义待办分组(**只有这一组可删**,模板条目删了下次进页面还会出现,接口层直接 reject);「导出 / 分享」由 `buildChecklistText` 生成纯文本经系统分享面板发出(`shareText`)。 7. **微信验证码页 `components/onboarding/screens/P2WechatCode.tsx`**:点验证码输入区未聚焦 → 加 `inputRef` 聚焦。 **顺带修掉的两类「假入口」**(同属「不留假交互」口径): - **薪资计算器「更多工具」**原先三项都指回计算器自己(点了等于没点)→ 改由接口层 `fetchSalaryToolLinks` 下发真实路由(`/jobs/offer-compare`、`/jobs/rank`、`/tools/skill-cloud`);无路由的项渲染成纯文本、不显示箭头。 - **技能词云**:「生成学习路径」原无实现 → 接口层 `fetchSkillLearningPath` 按行业技能词权重取待补技能(条数按匹配度缺口换算、限定 3–6 条),页面弹层展示;词云搜索接真实过滤(`filterSkillWords`)。 **测试**:新增 3 个文件 **51 例** —— `onboarding-checklist.test.ts`(清单落库 / 自定义条目增删 / 导出文本 / 存储损坏回落)、`enterprise-filters.test.ts`(两页 Tab + 关键词过滤 / **Tab 计数与过滤结果逐项一致** / 学习路径规则)、`interview-score-draft.test.ts`(草稿往返 / 不串台 / 覆盖保存 / 幂等清除)。**存储 mock 由「恒返回 null」改为带状态的 Map** —— 否则「存了要能读回来」这类行为根本没被验证。`npm test` **219 例全绿**;`tsc --noEmit` / `eslint .` 0 问题。 **三个审计脚本全绿**:`check-routes`(72 路由 / 0 失效跳转)、`check-affordances`(**0 处**)、`check-backend-stubs`(**137** 个接口函数均留注释版真实请求)。 > 注:`check-routes` 另报 12 个「无入口指向的孤立页」(`/jobs/[id]`、`/chat/[id]`、`/legal/checkout`、`/settings/privacy/[id]`、`/growth`、`/profile` 等),已逐个核对**均为误报** —— 它们是动态路由或 Tab 页,入口由模板字符串(`router.push(\`/jobs/${id}\`)`)或 Tab 配置给出,静态扫描看不到。 **提交**:本批次(4.7 + 4.8)与前一日积压的工作区改动一并入库;`package-lock.json`(本项目统一用 `yarn.lock`)与调试截图 `shot-check.png` 加入 `.gitignore`,不入库。 --- ## 五、后续增量开发建议(优先级) - **P0(上架前必做)**:~~图标替换~~ **应用图标【已完成,`878710c`,1024×1024「有职」logo】** · ~~隐私 / 协议内容填充~~ **隐私/协议草案【已存在,含 18 岁门槛,待填【待确认】占位】** · ~~EAS 初始化~~ **EAS 配置骨架【已完成,`owner` / `projectId` 两处占位符阻塞 `eas build`,需 `eas init` 注入,详见 4.5】** · ~~18 岁门槛核对~~ **18 岁门槛【已在隐私政策第六章 + 用户协议前言落地】** · ~~微信登录接入评估~~ **微信登录接入评估【已完成 → 见 `微信登录接入方案.md`,SDK 接入阻塞于微信开放平台 AppID】** · ~~启动屏接线~~ **启动屏【已完成,`expo-splash-screen` + `splash-icon.png`,需出包验证】**。 - **P0(补充 · 2026-09-15 新发现)**:① ~~页脚服务热线 / 举报监督电话待填~~ **已填 `15154608969`**(`src/constants/index.ts` 的 `SUPPORT_HOTLINE` / `SUPERVISION_PHONE`,为空时页脚整块不渲染、填入即自动出现并接 `tel:` 拨号);**隐私政策里【待确认】的客服电话应与之对齐**(同一个号,两处别填成两个)。② **缺「资质证照」页** —— 项目内没有该页面(`app/legal.tsx` 是「阿布阿诺法律服务」付费咨询页,不可复用),上架前需新建并填入营业执照 / ICP / 人力资源服务许可证等信息。③ **注册引导链 + 简历导出的真机手工验证均未执行**(见 4.3 / 4.4 末条)。 - **P1(核心差异化)**:~~微信登录接入方案~~ **微信登录接入方案【已落文档,真实 SDK 接入阻塞于微信开放平台移动应用申请】** · ~~企业端快捷入口话术统一~~ **企业端快捷入口话术统一【已完成,6 处 label 对齐规范词;含沟通页快捷操作 onPress 接线,tsc/eslint 0 问题】** · ~~付费法律服务支付~~ **付费法律服务支付【已完成:mock 支付闭环,选套餐→结算→模拟支付→会员激活持久化,tsc/eslint 0 问题】** · ~~通关小游戏 7 关内容~~ **通关小游戏 7 关内容【已完成:结构原已建成,本次增厚至 7 关 22 题,tsc/eslint 0 问题】**。 - **P2(打磨)**:~~技术债清理~~ **技术债已清理(8 处 `any` → 明确类型,`eslint` 0 warning)** · ~~死控件清零~~ **死控件已清零(22 → 0,2026-09-17,见 4.8)** · 真实后端对接 · OTA 通道验证。 - **P1(补充 · 2026-09-17)**:① **运营商一键登录取号**需运营商号码认证服务 + EAS 自定义客户端,接入路线图见 `运营商一键登录接入方案.md`(端上换 1 处 SDK 调用 + 后端换 1 个接口,页面/hook/类型零改动);② **引导页 4 张配图**待提供素材(`assets.ts` 的 `guideEvaluate` / `guideWeightDim` / `guideGameMap` / `guideLegalAI`,换图只改 `source` 一行);③ **`assets.ts` 内 16 个素材位 `source` 仍为 `null`**(营业执照示例图、5 张预设头像、3 张场景插画等)—— 未补时按约定降级:引导页那 4 张显示「配图待补 + 该放什么图」标注,头像 / 插画类降级为色块 + 图标(可用的占位态,非空白)。 --- ## 六、附:审查命令速查 ```bash # 类型检查(硬门槛,必须 0 错误) npx tsc --noEmit # 代码规范 npx eslint . # 路由闭合审计(改动路由后必跑) node scripts/check-routes.mjs # 死控件审计(改动交互后必跑;豁免标记 affordance-ok) node scripts/check-affordances.mjs # 后端接口预留审计(新增/改接口后必跑;豁免标记 backend-stub-ok) node scripts/check-backend-stubs.mjs # 单元测试(src/__tests__ 下纯逻辑) npm test # 依赖与 SDK 一致性(动过依赖后跑,1 项预存问题:yarn.lock 与 package-lock.json 并存) npx expo-doctor # 打包自检(能过说明新 import 可解析,不替代真机验证) npx expo export --platform android --output-dir <临时目录> # 启动 npm start # 即 expo start ```