@ -1,4 +1,4 @@
# 有职 App 开发进度与规划文档
# 轻松聘 App 开发进度与规划文档
> 生成日期:2026-09-10
> 最后更新:2026-09-20 · 第三批(**把同一类缺陷的另一半「同一张映射表 / 同一段业务规则在多个页面各写一份」收敛掉**,见 4.16。共 **14 处**本地重复声明压成 **6 张单一来源的表** :评级→文字色 **5 份 → 1** (growth / jobs / ability / assessment 四处同名同值,hr-evaluation 因数据层在那里下发的是**中文评级**而非 `JobLevel` 、键的域不同而另立一份,故第六处是**派生**而非并列复写)、霍兰德六型色 3 → 1、语义色名→色值 3 → 1、文章缩略图 2 种写法(对象表 vs 两份平行 `Record` )→ 1、社保基数 clamp 3 → 1(**下游那份在接口层,才是业务规则该在的层**;页面内联的那份是因为它要用**新城市**的上下限而 helper 读的是旧城市,故提取时把 `city` 变成显式参数,特殊情况随之消失)、`formatRecordTime` 与登录页内联脱敏 → 复用 `formatDateTime` / `maskPhone` ;另把企业端发布页自持的学历 / 工作类型选项表接回 `src/constants` 。**取值一律等于现状、一像素未改**,属纯重构。新增 **26 例回归锁**并做**6 处注入验证** (6 例如期变红、逐字还原、grep 确认无残留)。**两条如实记录**:① 子代理报来的 `DIMENSION_LEVEL` 「重复声明」经查**不成立**(全仓只有一处),其 `THUMBNAIL_*` 与 clamp 的行号描述亦不精确 —— 与「代理报告不是证据」这一贯口径一致,**每条都独立核对过**;② 查出 `profile.tsx` 的「良好」用 `textSecondary` 、而另外五处用 `primary` 的**配色分歧,未擅自统一**,留给产品定夺)
@ -26,7 +26,7 @@
| 图标名审计 | `node scripts/check-icons.mjs` | ✅ **0 处无效图标名** (2026-09-18 新增门槛,修掉 `gavel-outline` 后建立,见 4.13) |
| 单元测试 | `npm test` | ✅ **25 个文件 339 例全绿** (2026-09-20) |
| 入口接线 | 读 `app/_layout.tsx` / `app/index.tsx` | ✅ 根栈 → 入口围栏 → 双端 Tab 完整 |
| 应用配置 | `app.config.js` | ✅ name「有职」、scheme `youzhi ` 、plugins 正确 |
| 应用配置 | `app.config.js` | ✅ name「轻松聘」、scheme `qingsongpin ` 、plugins 正确 |
**判定**:项目处于可启动状态(`expo start` 即可运行),未发现阻塞运行的错误。本次未进入"修正"环节。
@ -101,7 +101,7 @@
**问题**:整条引导链在空转 —— `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 开始(按决定不做断点续填)。
- **落库闭环** :新增持久化模型 `src/types/onboarding.ts` (`OnboardingProfile` 与含登录瞬时字段的 `OnboardingData` 拆开,`phone` / `code` / `agreed` / `wechatTicket` 不落库)+ API `saveOnboardingProfile` / `fetchOnboardingProfile` / `clearOnboardingProfile` (键 `qingsongpin :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「**期望**城市」,不能冒充「现居城市」(期望城市仍正确显示在求职意向里)。
@ -124,11 +124,11 @@
**问题**:引导链闭环之后,简历成了新的瓶颈 —— `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`),避免换账号后看到上一个人的简历。
- **覆盖层数据层** (新增类型 `ResumeOverride` / `ResumeItemInput` / `ResumeAIActionResult` / `ResumeExportOutcome` ):简历按「**用户显式编辑过的板块 > 引导资料派生**」取值,键 `qingsongpin :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」(从未保存则不显示该行)。
- **头部「保存」补充功能** :把当前简历 6 个可编辑板块整体写入覆盖层 + 保存时间戳(`qingsongpin :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` 兜底成「姓名_期望职位简历」。模板 / 纸型 / 方向的选择弹层**就地在页内改设置、随「保存」落盘**(跳新页会出现「模板已存、格式没存」)。
- **导出设置真正保存** :`saveResumeExportSettings` 原先是 `Object.assign(DEFAULT_EXPORT_SETTINGS, …)` —— 只改模块内常量,杀进程即丢,「保存」按钮实则保存不了,还会污染其它调用方读到的默认值。现改落 `qingsongpin :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**)。**「不进硬门槛」的理由已于 2026-09-18 消失**(假交互同批清零,脚本并补上了弹窗式假交互的检测口径),`check:affordances` 已改为带 `--error` 的硬门槛,见 4.9。
@ -137,13 +137,13 @@
### 4.5 工程与上线
- **应用图标** :✅ 已完成(提交 `878710c` )。`assets/icon.png` 已替换为「有职 」logo,实测 1024×1024,符合商店要求。
- **应用图标** :✅ 已完成(提交 `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` ,可按开发者账号调整。
两者均可由 `eas login` 后执行 `eas init` 自动注入,**不要手工填**。`ios.bundleIdentifier` / `android.package` 当前为 `com.qingsongpin .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 错误。
@ -184,7 +184,7 @@
- **内容与配图重做** :四屏文案全部重写(聚焦差异化能力),配图改走新增组件 `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` ),属设计行为;要复现真·首次启动需清应用数据或卸载重装。**该结论为代码走查所得,本机未复现(无设备可测)。**
- ** 「首次启动没出现」的核查结论**:`app/index.tsx`(本轮未改动)在 hydration 完成后按 `onboardingSeen` 放行 —— 没看过即 `Redirect /guide` ,默认值 `false` (`AuthContext` 的 `memoryAuth` ),存储里没有该键时不会误判为已看过。因此**跳过引导只可能来自「本机已看过一次」**(看完即 `completeOnboarding()` 写入 `qingsongpin :auth` ),属设计行为;要复现真·首次启动需清应用数据或卸载重装。**该结论为代码走查所得,本机未复现(无设备可测)。**
- **顺带** :设置页「清除缓存」的体积改为运行时读取(`useAppCache` + `src/lib/cache.ts` ),不再写死数字;页脚服务热线抽成 `SupportFooter` (号码为空则整块不渲染)。
### 4.8 全仓死控件清零(2026-09-17)
@ -195,7 +195,7 @@
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>` ,**按面试 id 分开不串台**),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。
5. **面试评分 `app/interview-score.tsx`** :「保存草稿」接真实本地暂存(键 `qingsongpin :interview:score-draft:<id>` ,**按面试 id 分开不串台**),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。
6. **入职准备清单 `app/onboarding.tsx`** :勾选状态**落库**(此前退出再进入勾选全丢,`completed` 恒为 3/18);新增自定义待办分组(**只有这一组可删**,模板条目删了下次进页面还会出现,接口层直接 reject);「导出 / 分享」由 `buildChecklistText` 生成纯文本经系统分享面板发出(`shareText`)。
7. **微信验证码页 `components/onboarding/screens/P2WechatCode.tsx`** :点验证码输入区未聚焦 → 加 `inputRef` 聚焦。
@ -448,7 +448,7 @@ if (link.route) { router.push(link.route); return; }
- **资质证照的真实内容** :三类证照的编号 / 发证机关 / 有效期 / 证照图**全部待法务与资质方提供**。页面与数据契约已就绪,材料到位后只改 `src/api/api.ts` 里 `MOCK_QUALIFICATIONS` 的字段(`number` / `issuer` / `validPeriod` / `imageUrl` ),并把 `qualifications.test.ts` 中锁 `null` 的断言一并更新。
- **合规页脚** :见第 7 条,需产品定位置与形态。
- 4.9 第 8 条列出的「仍未验证」项:~~企业端「重新上架」点击路径~~ **已于 4.11 在模拟器实测通过** ;~~其余 5 处导出分享~~ **已于 4.12 全部实测通过** (另加 `jobs/[id]` 岗位分享,共 6 处);~~其余 6 处空态~~ **已于 4.13 用注入空数据实测通过** 。~~`settings` 缓存读取失败态~~ **已于 4.13 结案 —— 结论是「不可达」而非「未验证」** 。**至此 4.9 第 8 条全部收口**,仅剩一项本机无法验证的外部依赖项(生产包 `youzhi ://` 深链,见 4.12 第 6 条)。
- 4.9 第 8 条列出的「仍未验证」项:~~企业端「重新上架」点击路径~~ **已于 4.11 在模拟器实测通过** ;~~其余 5 处导出分享~~ **已于 4.12 全部实测通过** (另加 `jobs/[id]` 岗位分享,共 6 处);~~其余 6 处空态~~ **已于 4.13 用注入空数据实测通过** 。~~`settings` 缓存读取失败态~~ **已于 4.13 结案 —— 结论是「不可达」而非「未验证」** 。**至此 4.9 第 8 条全部收口**,仅剩一项本机无法验证的外部依赖项(生产包 `qingsongpin ://` 深链,见 4.12 第 6 条)。
---
@ -507,7 +507,7 @@ const isActive = job.status === 'active';
#### 4. 两处如实记录
- **模拟器上的草稿样本被我用掉了** :验证「上架」写入时把那张草稿变成了在招,**已落库**。UI 只能在 `active` ↔`offline` 之间切,**回不到 `draft` **;且「设置 → 清除缓存」只删文件系统缓存目录(`src/lib/cache.ts`),**不动 AsyncStorage**,清不掉 `youzhi :enterprise:jobs-status` 。要把草稿样本变回来只能清应用数据(`adb shell pm clear`,会一并清掉登录态)。属本机开发数据,不影响仓库。
- **模拟器上的草稿样本被我用掉了** :验证「上架」写入时把那张草稿变成了在招,**已落库**。UI 只能在 `active` ↔`offline` 之间切,**回不到 `draft` **;且「设置 → 清除缓存」只删文件系统缓存目录(`src/lib/cache.ts`),**不动 AsyncStorage**,清不掉 `qingsongpin :enterprise:jobs-status` 。要把草稿样本变回来只能清应用数据(`adb shell pm clear`,会一并清掉登录态)。属本机开发数据,不影响仓库。
- **这不是又一个「文档说已清零」** :本缺陷是**代码走查没发现、复看截图才发现**的(与 4.10 第 6 条的标签折行同一类)。4.9 第 4 条当时只跑了 tsc/eslint/单测,这三道门槛都看不见「文案对不对」。
#### 5. 门槛复跑(2026-09-18 实测)
@ -581,7 +581,7 @@ const isActive = job.status === 'active';
#### 6. 深链不路由:原因已查明(**2026-09-18 更正,原解释是错的**)
验证过程中试过 `youzhi://jobs/1` 、`youzhi ://settings/privacy` 等深链,**均未跳转**,app 只是恢复了上次的路由。
验证过程中试过 `qingsongpin://jobs/1` 、`qingsongpin ://settings/privacy` 等深链,**均未跳转**,app 只是恢复了上次的路由。
这件事本节最初写的是「当前跑的是 dev build,scheme 很可能由 **expo-dev-client** 的 launcher 接管,生产包行为未必相同」。
**这个解释是错的**,必须更正 —— 一个错的归因比「未知」更危险:它会让人去查 dev-client 配置,而那里根本没有问题。
@ -597,18 +597,18 @@ const isActive = job.status === 'active';
**根因(一条命令直接证死)**:
```bash
$ adb shell am start -a android.intent.action.VIEW -d "youzhi ://jobs/1"
Error: Activity not started, unable to resolve Intent { ... dat=youzhi ://jobs/... }
$ adb shell am start -a android.intent.action.VIEW -d "qingsongpin ://jobs/1"
Error: Activity not started, unable to resolve Intent { ... dat=qingsongpin ://jobs/... }
$ adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:8081/--/jobs/1"
Starting: Intent { ... dat=exp://127.0.0.1:8081/... } # 正常启动
```
`unable to resolve Intent` 的含义是 **Android 在整台设备上找不到任何能处理 `youzhi ://` 的应用** —— 不是「应用收到了但忽略了」。因为**没有任何已安装的原生包注册过这个 scheme**:`app.config.js` 里那句 `scheme: 'youzhi '` 要出**原生包**才会写进 AndroidManifest;Expo Go 只注册 `exp://` 。
`unable to resolve Intent` 的含义是 **Android 在整台设备上找不到任何能处理 `qingsongpin ://` 的应用** —— 不是「应用收到了但忽略了」。因为**没有任何已安装的原生包注册过这个 scheme**:`app.config.js` 里那句 `scheme: 'qingsongpin '` 要出**原生包**才会写进 AndroidManifest;Expo Go 只注册 `exp://` 。
**结论**:这是 **Expo Go 的已知限制,不是本项目的缺陷** 。所以 ——
- 「`youzhi ://` 在 Expo Go 里点不动」**永远测不出别的结果**,不必再试;
- 「`qingsongpin ://` 在 Expo Go 里点不动」**永远测不出别的结果**,不必再试;
- 生产包是否路由,**仍未验证**(本机没有原生包可测),但**成因已不再是谜**:只要出包后 AndroidManifest 里的 scheme 生效即可。**建议首次出包后专门验一次**,不要等上线才发现;
- 潜在影响面如前述:微信登录回调、分享出去的内容被点开等,都依赖这个 scheme。
@ -725,7 +725,7 @@ name: '劳动仲裁申请书', icon: 'gavel-outline' ← 原值
#### 6. 更正 4.12 第 6 条的错误归因
详见 4.12 第 6 条(已就地更正):深链 `youzhi ://` 不路由**不是** expo-dev-client 造成的 —— 本项目**根本没装 dev-client**,当前跑在 **Expo Go** 里(本机第三方包只有 `host.exp.exponent` ),Expo Go 只注册 `exp://` ,故 Android 报 `unable to resolve Intent` (**整机没有任何应用能处理该 scheme**)。属 Expo Go 已知限制,非项目缺陷。
详见 4.12 第 6 条(已就地更正):深链 `qingsongpin ://` 不路由**不是** expo-dev-client 造成的 —— 本项目**根本没装 dev-client**,当前跑在 **Expo Go** 里(本机第三方包只有 `host.exp.exponent` ),Expo Go 只注册 `exp://` ,故 Android 报 `unable to resolve Intent` (**整机没有任何应用能处理该 scheme**)。属 Expo Go 已知限制,非项目缺陷。
#### 7. 本轮本机副作用(如实记录)
@ -764,7 +764,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa
#### 9. 仍未验证(不要当成验过)
- **生产包的 `youzhi ://` 深链** :本机无原生包,测不了(成因已查明,见 4.12 第 6 条)。**这是唯一一项真正的「未验证」。**
- **生产包的 `qingsongpin ://` 深链** :本机无原生包,测不了(成因已查明,见 4.12 第 6 条)。**这是唯一一项真正的「未验证」。**
- 4.9 第 8 条其余项目已全部关闭:企业端「重新上架」见 4.11,6 处导出分享见 4.12,6 处空态见本节第 3 条,`settings` 缓存失败态见本节第 8 条(结论是「不可达」而非「未验证」)。
#### 10. 第七处分享路径实测 + 5 个拼装函数补上测试锁
@ -794,7 +794,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa
|---|---|---|
| **空值守卫** | 无评分数据不输出该行(不输出 `null` /`0 分`);薪资月数为 0 不写「0薪」;真实薪资样本数 0 不展示;福利为空不留「福利:」空壳;评价数 0 说「暂无员工评价」而非「员工评价 0 条」 | 约 14 例 |
| **取的是本地选择态** | 面试配置分享**只输出被选中的题型**,未选中的不得出现 | 6 例 |
| **对外可见的格式** | 首行格式、`—— 来自「有职 」` 签名、不留悬空分隔符、空分组不产生连续空行 | 约 8 例 |
| **对外可见的格式** | 首行格式、`—— 来自「轻松聘 」` 签名、不留悬空分隔符、空分组不产生连续空行 | 约 8 例 |
其中「只输出被选中的题型」这条,是把 **4.12 那个真机证伪实验(改选后再分享、面板预览随之变)固化成不依赖设备的断言** —— 今后谁把它改回「输出接口默认值」,不必再上模拟器,跑测试就会红。
@ -807,7 +807,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa
两次注入均从备份完整还原(**md5 逐字节一致**),复跑 28 例全绿。
> 写测试时自己写错过一条断言,如实记一笔:本想断言「建议没有描述时不留悬空的 `——` 」,写成对**整段文本** `not.toContain('——')` —— 而签名行 `—— 来自「有职 」` 本身就含 `——` ,必然失败。**是断言写错了,不是代码错了**;改为断言**那一行**等于 `· 补足项目数据` 。留着这段是因为它说明一个常见坑:断言的**粒度**要落在被测行为上,对整段文本做否定断言很容易被无关内容打脸。
> 写测试时自己写错过一条断言,如实记一笔:本想断言「建议没有描述时不留悬空的 `——` 」,写成对**整段文本** `not.toContain('——')` —— 而签名行 `—— 来自「轻松聘 」` 本身就含 `——` ,必然失败。**是断言写错了,不是代码错了**;改为断言**那一行**等于 `· 补足项目数据` 。留着这段是因为它说明一个常见坑:断言的**粒度**要落在被测行为上,对整段文本做否定断言很容易被无关内容打脸。
#### 11. 「数据层引用外部词表」的系统性排查(本轮顺带做完)
@ -1161,7 +1161,7 @@ EXIT=1,规则确实生效;随后还原,`grep -c __probe` = 0,`eslint .`
- 颜色映射进 `src/theme/colors.ts` —— 该文件自己就写着「**色值是本文件独占的职责**」,
且已有 `templateAccentColors` 这个先例;
- clamp 进 `src/api/api.ts` —— AGENTS.md 规定业务规则在接口层,而**权威的那一份本来就在那里**;
- 时间格式化进 `src/utils` —— 日期格式化是 utils 的既有职 责(该文件已有 `formatDateTime` );
- 时间格式化进 `src/utils` —— 日期格式化是 utils 的既轻松聘 责(该文件已有 `formatDateTime` );
- 选项词表进 `src/constants` —— 4.14 定的单一来源。
#### 3. 一处必须**派生**而不是并列复写:`levelLabelTextColors`
@ -1684,9 +1684,98 @@ deep-link 只能到 P1。**临时**把初始步改成目标页截图,**看完
---
### 4.20 改名「轻松聘」 + 引导页改三屏满屏主题色(2026-09-20 批次,接 4.19)
**起因**:产品确定软件名从「有职」改为「轻松聘」;引导页从四屏改为三屏,标题由产品给定,
整页背景用页面主题色,底部一个「开启体验」按钮。
**1. 改名范围(`grep -o | wc -l` 实测,不是估计)**
| 面 | 文件数 | 出现次数 |
|---|---|---|
| 代码 + 配置里的显示名「有职」→「轻松聘」 | 13 | 50 |
| 标识符 `youzhi` → `qingsongpin` | 17 | 17 |
| 文档(`项目开发状态.md` / 两份接入方案 / `screenshots/` ) | 5 | 有职 12 / youzhi 19 |
| **合计** | **27 个文件** (含重叠) | 有职 62 / youzhi 36 |
改法是**带断言的白名单脚本**(`/tmp/mdj/rename.js`):每个文件先写死「改之前应该出现几次」,
对不上就抛错、**一个文件都不写**。跑第一步就有用 —— `项目开发状态.md` 我数的是 18 处,
脚本报实际 19 处(有 2 处挤在同一行),拦住了一次目测不准的替换。
**2. 三处**不是纯文本、是**行为**变化,单独列出**
- ** `app.config.js` 的 `slug` / `scheme` / `ios.bundleIdentifier` / `android.package` **
`youzhi` → `qingsongpin` (`com.youzhi.app` → `com.qingsongpin.app` )。
**现在改无代价** (从未上架、`owner` / `projectId` 还是占位符);**一旦上架就改不动了**。
- ** `src/lib/storage.ts` 的 `KEY_PREFIX` :`'youzhi:'` → `'qingsongpin:'` **。
前缀变了 = 本机**已存的登录态 / 简历覆盖层 / 引导采集答案 / 草稿全部读不到**(不是清空,是再也找不到)。
本机只有开发数据、无真实用户,可接受;**但若线上已有用户数据,这一步必须写迁移,不能直接换前缀**。
副作用正好可用:改完前缀本机回到「首次启动」,`/` 直接落到引导页 —— 顺手完成了端到端验证(见第 5 条)。
- ** `screenshots/capture-manifest.json` 的 `scheme` 必须与 `app.config.js` 同步**,否则截图流水线的深链失效。已同步。
- (另有 `app/(tabs)/chat.tsx` 高德 URL 的 `src=youzhi` → `src=qingsongpin` ,仅来源标识,无功能影响。)
**3. 引导页三屏重做(`app/guide.tsx`)**
| 屏 | 标题(产品给定) | 副标题(我写的) |
|---|---|---|
| 1 | 求职开挂 AI护驾 | AI 简历优化、AI 模拟面试、AI 合同咨询,从投递到签约都有人接话。 |
| 2 | 真实能力 一测便知 | 霍兰德兴趣测评 + 能力自评,先看清自己的底牌,再决定投什么岗。 |
| 3 | 一起解锁 职场秘籍 | 成长页的职场文章与闯关任务,把该准备的提前准备好。 |
- **副标题只写仓库里查得到的已上线模块** (`'AI简历优化'` / `'AI面试官真实场景演练'` / 法务页
`'免费的 AI 咨询与合同模板'` / `app/holland` / `app/(tabs)/growth.tsx` 的 article 与 `app/game` ),
没有编造未上线能力 —— 现在写进去的每一句都能在代码里指到出处。
- **底色没有复用现成的 `headerGradient*` 三支** ,新增 token `onboardingGradient`
= `[primary, primaryBtn, primaryDark]` (#2C83FD → #1F6FDB → #175AB0 )。理由写进了 `colors.ts` :
现成三支末端是青色 `#4FD1C5` ,白字压上去只有约 **1.9:1** ,它们能用是因为白字只压在**顶部**深色区;
引导页是**整页**底色、正文在中下部,整条渐变必须始终深于白字阈值。配 `locations={[0, 0.34, 1]}`
把 4.8:1 那一档提前到 34% 处,正文实际落在 5.2:1 以上。
- **按钮反过来做** :白底 + `primaryText` (#104686 白底 9.4:1 AAA)。没有用品牌蓝底白字 ——
`primary` 白字只有 3.6:1,`primaryBtn` 白字 4.8:1 也只为大字准备。
- **状态栏加了 `<StatusBar style="light" />`** :根布局是 `style="dark"` ,整页深蓝底会让图标变成黑压黑。
旧引导页是白底所以没暴露;**任何深色头部页面都会中招**,属既有缺陷,本页先行修掉。
- **按钮文案是我的判断,不是用户明说的** :用户只说「页面底部写上开启体验按钮」。
三屏都用「开启体验」的话后两屏等于摆设,故按**前两屏「下一步」、第三屏「开启体验」**处理;
若要求三屏一致,改 `isLast` 那一个三元表达式即可。
- 配图仍是三个素材位(`guideAiCopilot` / `guideAbilityTest` / `guideGrowthUnlock` ),
`hint` 直接画在页面上(ImageSlot 的「配图待补 + 该放什么图」)。
**4. 需要补的图片(我生成不了,按「没有图片请标明」逐项列出)**
改名后**所有带字标的位图都过期了**,已逐张打开确认,不是推测:
| 文件 | 现状(已打开看过) | 要换成 |
|---|---|---|
| `assets/icon.png` | 1024×1024,蓝底白字**「有职」**+上箭头 | 「轻松聘」同规格 1024×1024 应用图标 |
| `assets/brand-logo.jpg` | 同上字标,登录页 `BrandHeader` 与 `ASSET_REGISTRY.logo` 在用 | 「轻松聘」字标 |
| `assets/splash-icon.png` | 启动屏,冷启动第一眼 | 「轻松聘」字标(同规格) |
| `assets/android-icon-foreground.png` / `-background.png` / `-monochrome.png` | 自适应图标三分层 | 由新图标重新导出 |
| 引导页三张插画 | 仍是占位框 | 见 `components/onboarding/assets.ts` 里三条 `hint` |
(注:`项目开发状态.md` 第五章 P0 里「应用图标已完成」那条指的是 `878710c` 做的**「有职」**图标,
本批改名后该结论已过期,待新图到位再更新。)
**5. 验证(模拟器 `emulator-5554` ,像素测量,非目测)**
- 端到端:`exp://127.0.0.1:8081/--/` 进入 → 因 `KEY_PREFIX` 改名本机回到首次启动 → **自动落到引导页** ✓
- 底色渐变实测(`x=540`):y=70 `#2c82fc` (= primary)· y=1300 `#1c68cd` · y=2300 `#175ab1` (= primaryDark),
与 `locations=[0,0.34,1]` 的理论值逐点吻合。
- CTA 按钮:左边界 **x=42px = 16.0 dp** 、距右 **16.0 dp** 、纵向 **y=2132~2263 = 50.0 dp** ,
文字色实测 ** #104686 **(= `primaryText` )✓
- 滑到第 3 屏:标题「一起解锁 职场秘籍」、指示点第 3 个点亮、按钮变「开启体验」✓
- 门槛:`tsc` 0 · `eslint` 0 · `npm test` 26 套件 / 365 用例全过 · `check:spacing` 卡片 196 全 8px ·
`check:routes` · `check:affordances` · `check:icons` · `check:backend-stubs` 全绿。
- **上下各 24dp 的黑/白条是系统栏** (状态栏黑、手势导航栏白),在 Expo Go 里由 Expo Go 自己的主题决定,
不是本页画的;桌面上截同一位置可看到系统栏是透明的,可佐证。非本批引入。
**6. 仍未做 / 待定夺**
- **改名只改了「能改的」** :字标位图(第 4 条)必须由设计出图;两份接入方案里
「微信开放平台 / 运营商那边登记的包名」若已提交过,也需要同步改(文档已改,**外部登记状态我无从核实**)。
- 引导页三屏按钮文案是否统一为「开启体验」,见第 3 条末 —— 一行改动,等一句话。
- 4.19 第 9 条的三项(主应用 footer 16 vs 卡片 8、非卡片间距、设计令牌层)**均未动**。
---
## 五、后续增量开发建议(优先级)
- **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(上架前必做)** :~~图标替换~~ **应用图标【`878710c` 做的 1024×1024 是「有职」字标,2026-09-20 改名后已过期,须重出图,见 4.20 第 4 条 】** · ~~隐私 / 协议内容填充~~ **隐私/协议草案【已存在,含 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`** ,且 ** `SupportFooter` 已挂在 P1Login / P2QuickLogin 两页上,页脚实际渲染**(2026-09-20 复核;4.10 第 7 条那句「无消费者 / 缺页脚组件」已撤回,见 4.14 第 7 条);**隐私政策里【待确认】的客服电话应与之对齐**(同一个号,两处别填成两个)。② ~~缺「资质证照」页~~ **页面【已完成,见 4.10】—— 四层链路齐备(`src/types/qualification.ts` / `fetchQualifications` / `useQualifications` / `app/settings/qualifications.tsx`),设置页与「关于我们」页两处入口已恢复,三类证照的补图占位已逐张标明;**但证照的真实编号 / 发证机关 / 有效期 / 证照图仍全部待法务与资质方提供** (材料到位后只改 `MOCK_QUALIFICATIONS` 字段 + 更新测试断言),**未编造任何证件编号**。③ **注册引导链 + 简历导出的真机手工验证均未执行** (见 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)** · ~~假交互清零~~ **假交互已清零(37 → 0,2026-09-18,**但 4.8 当时的「已完成」结论是错的,见 4.9 首条** )** · ~~卡片圆角统一 8px + 去阴影~~ **已完成(147 处圆角 + 54 行阴影,覆盖边界见 4.17;新增卡片须从 `src/theme` 出圆角常量这一层尚未建立)** · 真实后端对接 · OTA 通道验证。