|
|
|
@ -1,10 +1,13 @@ |
|
|
|
# 有职 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) |
|
|
|
> 最后更新:2026-09-18(**修正 4.8「死控件清零」的错误结论** —— 该结论只覆盖了审计脚本的检测口径,另有 **37 处「点了只弹功能开发中」的假交互**未被发现,本轮全部处理;补上审计脚本盲区,修复 18 处四态缺口,企业端岗位加「下架 / 刷新」写通道,见 4.9。**另有:新建「资质证照」页并恢复设置 / 关于我们两处入口,修掉「关于我们」页两条点下去毫无反应的死链,见 4.10**。**再有:修掉「草稿」岗位被写成「重新上架」的状态文案缺陷(截图复看才发现),状态映射下沉到 API 层并加测试锁,见 4.11**。**再有:4.9 遗留的 6 处导出 / 分享全部真机验证通过(靠系统分享面板的文本预览核内容),并顺带查出霍兰德参与人数在介绍页与成长页是两个数(124,580 / 12,846),已收敛为单一常量 + 测试锁,见 4.12**。**再有:4.9 遗留的最后 6 处空态用「注入空数据 + 逐个截图」实测通过(关掉 4.9 第 8 条最后一项);过程中查出文书模板 `gavel-outline` 不是合法 Ionicons 字形名(全库 1357 个字形里没有 gavel,图标渲染为空白,被页面的 `as keyof typeof Ionicons.glyphMap` 断言吞掉了类型错误),已修并新增 `npm run check:icons` 门槛防复发;同时更正 4.12 对深链不路由的错误归因(不是 dev-client,是 Expo Go 只注册 `exp://`);并把 4.9 遗留的最后一项「`settings` 缓存读取失败态」结案 —— **结论不是「验不了」,而是 `readCacheBytes()` 把所有异常吞成 0、该分支根本不可达**,已加 12 例测试锁住这条契约;另补上第七处分享路径(入职清单导出)的真机验证,以及 4.9 同批 5 个分享拼装函数一直缺的 28 例测试锁(含把 4.12 那个真机证伪实验固化成断言);并顺带把「数据层引用外部词表」三类(图标名 / 路由 / 颜色)系统排查完,见 4.13**) |
|
|
|
> 上一版: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 例测试全绿),无需修正阻塞错误。 |
|
|
|
> 结论先行:**当前项目可运行**(`tsc` 0 错误、`eslint` 0 error、路由 0 失效跳转、假交互 0 处、图标名 0 处无效、302 例测试全绿),无需修正阻塞错误。 |
|
|
|
> |
|
|
|
> **读这份文档请先注意一条**:4.8 曾写「死控件清零」并据此把 P2 标记为已完成,**那个结论当时是错的** —— 审计脚本只查「无处理函数 / 空处理函数」,查不到「有处理函数但点下去只弹一句『功能开发中』」。详见 4.9 首条。凡是本文件里由某个审计脚本背书的「✅」,请连同**该脚本的检测口径**一起读。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
@ -14,10 +17,11 @@ |
|
|
|
|---|---|---| |
|
|
|
| 类型检查 | `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) | |
|
|
|
| 路由闭合 | `node scripts/check-routes.mjs` | ✅ 73 路由全部存在,0 失效跳转(12 个「孤立页」经核对为动态路由 / Tab 页误报,见 4.8) | |
|
|
|
| 假交互审计 | `node scripts/check-affordances.mjs` | ✅ **0 处**(2026-09-18 复跑,**口径已含弹窗式假交互**,见 4.9)<br>⚠️ 另有 **3 处知情保留的后端待办**(`--pending` 可列出,非疏漏) | |
|
|
|
| 后端接口预留审计 | `node scripts/check-backend-stubs.mjs` | ✅ **140** 个接口函数全部留有注释版真实请求(见 4.6) | |
|
|
|
| 图标名审计 | `node scripts/check-icons.mjs` | ✅ **0 处无效图标名**(2026-09-18 新增门槛,修掉 `gavel-outline` 后建立,见 4.13) | |
|
|
|
| 单元测试 | `npm test` | ✅ **23 个文件 302 例全绿**(2026-09-18) | |
|
|
|
| 入口接线 | 读 `app/_layout.tsx` / `app/index.tsx` | ✅ 根栈 → 入口围栏 → 双端 Tab 完整 | |
|
|
|
| 应用配置 | `app.config.js` | ✅ name「有职」、scheme `youzhi`、plugins 正确 | |
|
|
|
|
|
|
|
@ -103,7 +107,9 @@ |
|
|
|
- **清演示人设**:`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「语音接听验证码」(无语音通道)两个入口**移除**。 |
|
|
|
- **不编造数据(本轮的处理原则)**:页脚「服务热线 / 举报监督电话」改为常量 `SUPPORT_HOTLINE` / `SUPERVISION_PHONE`(**现为空,为空则整块不渲染**,填入号码即自动出现并接 `tel:` 拨号;与隐私政策的【待确认】客服电话同源,属同一批待补信息) |
|
|
|
⚠️ **2026-09-18 更正**:号码后来确实填了(`15154608969`),但**没有任何代码读这两个常量** —— |
|
|
|
所谓「填入即自动出现」并不成立,缺的是页脚组件本身。见 4.10 第 7 条。;「资质证照」入口一并去掉 —— 项目内**没有**该页面,`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 重进数据仍在)**尚未在设备上执行**。 |
|
|
|
@ -120,7 +126,7 @@ |
|
|
|
- **导出设置真正保存**:`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**)。 |
|
|
|
- **审计工具常驻**:新增 `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。 |
|
|
|
- **测试**:新增 `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` 是**吞冒泡守卫,需保留**(已加豁免标记,不属死控件)。 |
|
|
|
|
|
|
|
@ -202,12 +208,624 @@ |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### 4.9 假交互盲区 + 四态缺口 + 企业端写通道(2026-09-18 批次) |
|
|
|
|
|
|
|
#### 首条:4.8「死控件清零」是错误结论,错在审计口径 |
|
|
|
|
|
|
|
4.8 写「全仓死控件清零:9 → 0」,并据此把 P2 的「死控件清零」标记为已完成。**这个结论当时就是错的。** |
|
|
|
|
|
|
|
`scripts/check-affordances.mjs` 当时只查两类:控件**没有** `onPress`、`onPress` 是空函数。它查不到第三类 —— **有处理函数,但点下去只弹一句 `Alert.alert('提示', '功能开发中,敬请期待')`**。全仓按这个口径实测有 **37 处**,分布在 13 个页面。因为脚本报 0,文档就记成了 0,没有人再去数。 |
|
|
|
|
|
|
|
教训不是「上次漏了」,而是:**由脚本背书的结论,必须连同脚本的检测口径一起读**(本文件凡标 ✅ 的审计项都已按此补注)。 |
|
|
|
|
|
|
|
#### 1. 给审计脚本补盲区(本批次最有长期价值的一项) |
|
|
|
|
|
|
|
`scripts/check-affordances.mjs` 新增第 ② 类检测:**弹窗式假交互** —— `Alert.alert` 文案命中「功能开发中 / 敬请期待 / 暂未开放 / 尚未开放 / 即将上线 / …」即告警。 |
|
|
|
|
|
|
|
难的不是匹配文案,是**不误报**。`app/resume.tsx` / `app/resume/export.tsx` 那类「`await` 调完接口、拿到 `result` 后如实报告 AI 能力未接入」是 4.4 节确立的**正确做法**(保留入口 + 坦诚提示 + 不假装成功),文案与假交互一模一样。区分轴取「**处理函数里有没有真实动作**」:有 `await` / `.then(` / `async` 说明确实去问过后端,属如实报告;纯弹窗则判为假交互。 |
|
|
|
|
|
|
|
- **灵敏度已用夹具双向验证**(非「在当前仓库报 0」就算过):3 个反例全部抓出(内联箭头 / 命名函数 / 含同步计算的),3 个正例全部放过(含「`await` 与弹窗之间插一句同步赋值」这个易误报的形状)。夹具跑完即删。 |
|
|
|
- 新增 `affordance-pending-backend` 豁免标记,专给**知情保留的后端待办**(区别于 `affordance-ok` 的「有意为之」):标记后不告警,但 `--pending` 能一次列出全部欠账 —— 欠账不该靠告警数量为 0 来假装不存在。 |
|
|
|
- 新增 `--pending` 模式列出欠账清单(当前 3 处:聊天附件四图标 / 接受 Offer / 导出文件下载,后端就绪后按单兑现)。 |
|
|
|
- `--pending` 计数只认「注释内容以标记开头」的行 —— 否则连解释这个标记本身的说明文字也会被算成一条待办,**数出来的数字就是假的**,正是本批次要治的病。 |
|
|
|
|
|
|
|
#### 2. 37 处假交互的处理结果 |
|
|
|
|
|
|
|
口径按「**能真做的真做,不能做的删入口**」,三类落地: |
|
|
|
|
|
|
|
**① 能真做 → 真实现(不再弹窗)** |
|
|
|
|
|
|
|
| 页面 | 原状 | 现状态 | |
|
|
|
|---|---|---| |
|
|
|
| 我的页「评分申诉」 | 弹「功能开发中」 | `router.push('/appeal')` —— `app/appeal.tsx`(申诉页)**早就建好了**,纯属 4.8 批次漏网的接线 | |
|
|
|
| 岗位详情 / 霍兰德结果 / 面试配置 / 岗位排行 / Offer 对比 / 隐私设置 | 弹「功能开发中」 | 真导出:新增 5 个纯文本拼装函数(`buildJobShareText`、`buildHollandReportText`、`buildInterviewConfigText`、`buildRankReportText`、`buildPrivacySummaryText`、`buildOfferCompareText`)置于 **API 层**,经系统分享面板发出 | |
|
|
|
| 企业端岗位管理 搜索 / 筛选 / 排序 | 搜索框**无 `value`/`onChangeText`,是纯展示死输入框**(真缺陷);筛选 / 排序弹窗 | 全部真实现;Tab 过滤逻辑由页面 `useMemo` **下沉到 API 层**(原与 AGENTS.md「业务规则放 API 层」相悖) | |
|
|
|
|
|
|
|
**② 无落点 → 删入口(不留假按钮)** |
|
|
|
|
|
|
|
聊天页「查看全部预测题」「查看详细薪资报告」「右上角更多」、账号注销「去提现」、数据导出「查看更多历史」、Offer 对比「完整建议报告」(**summary/detail 页面早已全文展示,点进去没有任何更多内容**)、Offer 添加卡、人才市场报告、企业沟通「批量发 Offer / 群发通知」。删入口时**同步删掉 `src/api/api.ts` 里对应的下发项**,不留「数据里有、页面不渲染」的悬空字段(`reportAction`、`actionText`、`AITool` 的 id→route 页面映射等)。类型层同步收紧:`PendingTask` 改为 `route` / `tabKey` 二选一的判别联合,页面不再需要不可达的兜底分支。 |
|
|
|
|
|
|
|
**③ 需后端 → 保留入口 + 如实文案 + 标记欠账(3 处)** |
|
|
|
|
|
|
|
聊天输入区四图标(附件上传、表情资源需后端接口)、Offer 对比「接受 Offer」(需 `POST /api/offers/:id/accept`)、数据导出「下载文件」(`RecentExportRecord` 无下载地址字段)。文案由「功能开发中,敬请期待」改为如实的「**该操作需要后端支持,当前版本暂未开放**」,并打 `affordance-pending-backend` 标记。 |
|
|
|
|
|
|
|
> 这一条与「不能做的删入口」有张力:选择保留,因为聊天输入区四图标是行业标准布局,删掉会让输入区显得残缺。沿用 4.4 节「AI 按钮」先例(保留 + 坦诚提示 + 不假装成功)。 |
|
|
|
|
|
|
|
**顺带修掉作者已知模式却漏做的一处**:`app/(tabs)/growth.tsx` 的 AI 工具区用「id 比对 switch」分发路由且**无 else 分支**,加一个工具就点不动 —— 改为路由由 API 层随工具数据下发(`AITool.route` 设为必填,页面不再自己维护 id→路由映射)。 |
|
|
|
|
|
|
|
#### 3. 四态缺口 18 处(错误态 9 + 空态 9),其中 **8 处是真缺陷** |
|
|
|
|
|
|
|
**① error 被完全丢弃 —— 接口故障被静默渲染成业务空态(最严重)** |
|
|
|
|
|
|
|
- `app/legal/checkout.tsx`:`error` 从未解构 → 网络故障时页面显示「**套餐不存在,请从会员套餐页重新进入**」,把用户引向完全错误的自救路径。 |
|
|
|
- `app/legal/service.tsx`:`useLegalData()` 与 `useLegalServiceRequests()` 两个 hook 都提供 `error`,页面两个都没解构 → 同款误导。 |
|
|
|
|
|
|
|
**② hook 提供了 `refresh`,页面漏解构 —— 无重试入口,用户只能杀进程** |
|
|
|
|
|
|
|
`app/(enterprise)/enterprise-overview.tsx`、`app/candidate-detail.tsx`、`app/interview-score.tsx`、`app/job-publish.tsx`。统一改用既有 `src/components/StateView.tsx` 的 `<ErrorState onRetry={refresh} />`(**不新写样板**);`job-publish` 顺带把加载态的裸 `ActivityIndicator` 换成 `<LoadingState />`。 |
|
|
|
|
|
|
|
**③ `refresh` 已在作用域内却没接**:`app/job-evaluations.tsx`(一行修复)。 |
|
|
|
|
|
|
|
**④ 错误态语义错用**:`app/article/[id].tsx` 错误分支误用 `EmptyState` → 改 `ErrorState`(把错误态视觉规范收回一处维护)。 |
|
|
|
|
|
|
|
**⑤ 其余 error 静默**:`app/settings.tsx` 缓存读取失败原先静默显示 `--`(把「读失败」伪装成「本来就是空」)→ 改为「计算中 / 读取失败 / 实际体积」三态。 |
|
|
|
|
|
|
|
**空态补齐 9 处**(列表为空时页面一片空白,按文件数:`growth.tsx` 2、`legal.tsx` 2,`ability.tsx` / `hr-evaluation.tsx` / `jobs/[id].tsx` / `jobs/offer-compare.tsx` / `progress.tsx` 各 1):岗位详情员工评价、进度页空 Tab、成长页 AI 工具区 / 干货区、法律服务两个网格、Offer 对比列表、HR 评价、四维能力提升建议。其中 `app/legal.tsx` 与 `app/progress.tsx` 属**同文件内两套标准**(同页模板/FAQ/套餐三处都做了保护,服务网格与 companies 却漏了),`growth.tsx` 的「更多」入口在无文章时不渲染(否则点了没反应)。 |
|
|
|
|
|
|
|
> **另有 1 处不属此列**:`app/(enterprise)/job-management.tsx` 的「筛选后无结果」空态(`length === 0` 守卫共 10 处,减去上述 9 处即此 1 处)。它对应的是「筛选无匹配」而非「列表本来就空」,语义不同,故不计入本行。分开写是为了让 10 这个数能被对上,不至于出现「标题写 6、列表列 9」这类对不上的数字。 |
|
|
|
|
|
|
|
#### 4. 企业端岗位「下架 + 刷新」写通道 |
|
|
|
|
|
|
|
范式照抄入职清单(项目里最完整的写通道):**读旧值 → 合并 → `storageSet` 全量写回 → 返回完整聚合对象**。 |
|
|
|
|
|
|
|
- **API 层**:新增 `updateJobStatus(jobId, status)`(`PATCH /api/enterprise/jobs/:id/status`)与 `refreshJob(jobId)`(`POST /api/enterprise/jobs/:id/refresh`),均留注释版真实 `fetch`;存储键 `enterprise:jobs-status` / `enterprise:jobs-refreshed`。 |
|
|
|
- **修掉一处 mock 自相矛盾**:原 `MOCK_JOB_MANAGEMENT` 只有 3 个岗位,Tab 计数却写死 12 / 8 / 3 / 1 —— 点「已下线(3)」看到的是空列表。现扩为 **12 个自洽岗位**,且 **Tab 计数与「在招岗位」统计一律由列表推导**(`buildJobManagementTabs`),从机制上不可能再对不上。 |
|
|
|
- **类型层**:`JobManagementJob` 补 `publishedAt` / `refreshedAt?`(原无任何时间字段,「刷新」无法显示刷新时间)。 |
|
|
|
- **Hook 层**:`useJobManagement` 原无任何写方法 → 加 `setStatus` / `refreshJobItem` 与进行中标志 `mutatingId`(供页面禁用按钮防重复提交)。 |
|
|
|
- **页面层**:「下架」加破坏性二次确认,成功后用返回值替换本地镜像。 |
|
|
|
- **测试**:新增 `src/__tests__/job-management-write.test.ts` **22 例** —— 状态往返 / 计数随下架变化并在重新上架后复原 / 只改目标岗位 / 存储损坏回落 / 存储失败 reject / 刷新时间写入与幂等 / 空列表计数 / 关键词与城市·部门 AND 语义 / 四种排序 / **输入数组不被就地修改** / 空结果不回退成全量。测试常量一开始写错(把 3 个下线岗位当成 4 个),**是断言错、代码对**,已修正常量。 |
|
|
|
|
|
|
|
#### 5. 门槛复跑(2026-09-18 实测,非文档转述) |
|
|
|
|
|
|
|
`tsc --noEmit` **0 错误** · `eslint .` **0 error / 0 warning** · `npm test` **19 个文件 241 例全绿** · `check-routes` **0 失效跳转**(12 个孤立页仍为动态路由 / Tab 页误报)· `check-affordances` **0 处**(口径已含弹窗式假交互)· `check-backend-stubs` **139 个接口函数**均留注释版真实请求。 |
|
|
|
|
|
|
|
#### 6. 与既定方案的两处偏离(如实记录) |
|
|
|
|
|
|
|
- **Offer 对比的报告导出**:方案原定复用 `exportResumePdf`,但该函数签名是 `exportResumePdf(html, fileName, settings: ResumeExportSettings)` —— 复用意味着为「Offer 对比」这个与简历无关的域**编造一个 12 字段的简历设置对象**。改为**纯文本导出**(`buildOfferCompareText` + `shareText`),与其余 5 处导出一致。 |
|
|
|
- **`app/onboarding.tsx` 的空态未补**:该页分组来自代码级固定模板(恒为 4 组),空分支是**不可达的死 UI**,故不加。 |
|
|
|
|
|
|
|
#### 7. 模拟器实测结果(Android 模拟器 `emulator-5554`,非代码走查) |
|
|
|
|
|
|
|
**已实测通过** |
|
|
|
|
|
|
|
| 项 | 结果 | |
|
|
|
|---|---| |
|
|
|
| 企业端「下架」写通道 | `招聘中 8→7`、`已下线 3→4`、`在招岗位 8→7` 同步变化,被下架岗位从列表消失 | |
|
|
|
| **持久化(最关键一条)** | `adb shell am force-stop` 杀进程后重启,仍是 7/4,**未回退成 8/3** —— 证明写通道走的是 storage 而非改内存 mock | |
|
|
|
| Tab 计数与列表同源 | `全部(12)/招聘中(8)/已下线(3)/草稿(1)` 与 12 条数据自洽;草稿岗位显示「未发布,暂无排名」而非编造排名 | |
|
|
|
| 下架二次确认 | 弹窗点名岗位并说明影响,非泛泛「确定吗」 | |
|
|
|
| 错误态(注入故障触发) | `/legal/checkout` 显示「套餐加载失败,请重试」+ 重试;`/legal/service` 同。**修复前此处会显示「套餐不存在,请从会员套餐页重新进入」**,把用户引向无效路径 | |
|
|
|
| 空态(注入空数据触发) | 成长页「暂无可用工具」「暂无干货文章」带图标占位,且「更多」入口消失 | |
|
|
|
| 分享面板 | `/jobs/[id]` 分享真拉起系统面板,文本为真实拼装内容;代码未违反「`ok:true` 只代表面板已拉起」 | |
|
|
|
| 接线目标页 | `/appeal` 完整渲染真实申诉表单 | |
|
|
|
|
|
|
|
> 触发方式说明:mock 的 `fetchLegalPlans` 走 `mockRequest` **永不失败**,空态数据也非空,故 error 态与空态**在正常运行时不可达**。上表两项是**临时注入故障 / 空数据**后截图所验,注入代码已全部还原(已用 `grep` 确认无残留、`tsc` 复跑 0 错误、应用恢复正常态截图核对)。这里如实记录,是因为「注入后才可见」与「本来就可见」的验证强度不同。 |
|
|
|
|
|
|
|
**实测中发现并修掉的一个新缺陷(代码走查看不出来)** |
|
|
|
|
|
|
|
`app/jobs/[id].tsx` 头部:`headerBtn` 是 `position: 'absolute'` 但**未写 `left`/`right`**,在 `justifyContent: 'center'` 的父容器里返回键与分享键落到同一处彼此重叠 —— 截图里连返回箭头都看不见、标题上有一道错位横杠,**分享键实测点不到**。该样式**改动前就存在**(本批次只把假 `Alert` 换成真 handler),但它直接抵消本批次的工作:**加了真入口却点不到,等于没加**。已补 `left: 0` / `right: 0` 两条样式,修复后返回在左上、分享在右上、面板可正常拉起。 |
|
|
|
> 严格复查了全仓同类写法(解析每个 style 块的 `position: 'absolute'` 是否成对给出水平锚点),仅剩 3 处且均合理(`sliderThumb` 用 `marginLeft` 偏移、`savingToast` 用 `alignSelf` 居中、`pieSlice` 靠变换定位)—— **该文件是全仓唯一一处真缺陷**,不是撒网式改动。 |
|
|
|
|
|
|
|
**仍未验证(不要当成验过)** |
|
|
|
|
|
|
|
- ~~企业端「重新上架」的实际点击路径(写入逻辑与「下架」同源、有单测覆盖,但未在模拟器上点过)~~ → **2026-09-18 已实测通过,见 4.11** |
|
|
|
- ~~其余 5 处导出(霍兰德结果 / 面试配置 / 岗位排行 / 隐私摘要 / Offer 对比)的面板拉起 —— 只实测了岗位分享这一处~~ → **2026-09-18 已全部实测通过(含岗位分享共 6 处),内容经系统分享面板预览逐项核对,见 4.12** |
|
|
|
- ~~`app/settings.tsx` 缓存「读取失败」态:缓存体积不走 `mockRequest`(实测注入全局失败后仍正常读出 `381KB`),需文件系统级故障才能触发~~ → **2026-09-18 结案:结论是「不可达」而非「未验证」** —— `readCacheBytes()` 把所有异常吞成 `0`,`error` 恒为 `null`,该分支是死代码;已加测试锁,见 4.13 第 8 条 |
|
|
|
- ~~4.9 第 3 条其余空态(岗位详情员工评价 / 进度页空 Tab / 法律服务两网格 / Offer 对比 / HR 评价 / 四维能力建议):均为同一条件渲染写法,只实测了成长页两处~~ → **2026-09-18 已全部实测通过(注入空数据逐个截图),见 4.13** |
|
|
|
|
|
|
|
#### 8. 本批次未做的(明确记录,避免反复讨论) |
|
|
|
|
|
|
|
- **暗模式**:全仓 **3302 处**静态 `colors.X` 引用分布在 **124 个文件**,其中 **119 个文件**的样式是模块级 `StyleSheet.create`(2026-09-18 实测)—— 与运行时切主题根本不相容。属重构级工程(需逐个样式表改造 + 视觉回归手段,当前无自动化视觉回归)。**经决策本项目维持纯亮色**,`darkColors` 保留待用,已在 `AGENTS.md` 第九节记录。 |
|
|
|
- **企业端「编辑岗位」**:需给 `job-publish.tsx` 加编辑态 + `fetchJobPublishForm(jobId)` + `updateJob()`,而 `JobManagementJob` **缺 9 个表单字段**(职责 / 要求 / 福利 / 地址 / 工作类型等,且 `salary` 是展示串 `'25-35K'` 而表单要两个 number)→ 预填数据不完整,风险较大,暂缓。 |
|
|
|
- **StateView 全量替换 56 个页面**:改动面过大、会淹没本批次 diff,不利于 review → 只改本批次碰到的页。 |
|
|
|
- **`src/components` / `src/context` / `src/lib` 缺 `index.ts` barrel**:规格偏差属实(AGENTS.md 要求每个子目录统一导出),但纯属代码组织、与功能无关,不混入本批次。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### 4.10 平台资质证照页 + 死链修复 + 推送缺口(2026-09-18 批次) |
|
|
|
|
|
|
|
#### 0. 先分清两个都叫「营业执照」的东西 |
|
|
|
|
|
|
|
本批次新增的是**平台运营主体自身**的证照公示页(合规 + 应用商店上架审核用)。 |
|
|
|
它和企业端的 `EnterpriseCertificationForm`(`src/types/enterprise.ts`)**不是一回事**: |
|
|
|
后者是**企业用户上传**自己的营业执照以取得企业认证,主体是入驻企业、目的是拿认证。 |
|
|
|
两者都涉及「营业执照」,但主体 / 目的 / 入口全不同,已在类型文件顶部写明,避免后来者合并。 |
|
|
|
|
|
|
|
#### 1. 新建「资质证照」页(走完整四层链路) |
|
|
|
|
|
|
|
`app/settings/qualifications.tsx` 此前**不存在**,且 `app/legal.tsx` 是「阿布阿诺法律服务」付费咨询页,**不可复用**(这也是 2026-09-15 把该入口摘掉的原因)。 |
|
|
|
|
|
|
|
按 AGENTS.md 四层约定新建,无一层跳过: |
|
|
|
|
|
|
|
| 层 | 文件 | 内容 | |
|
|
|
|---|---|---| |
|
|
|
| types | `src/types/qualification.ts` | `QualificationLicense` / `QualificationInfo` / `QualificationLicenseType` | |
|
|
|
| api | `src/api/api.ts` `fetchQualifications()` | `GET /api/settings/qualifications`,留注释版真实 `fetch`(故 stubs 139 → **140**) | |
|
|
|
| hook | `src/hooks/useQualifications.ts` | 复用 `useAsyncData`,返回 `{ info, loading, error, refresh, isEmpty }` | |
|
|
|
| page | `app/settings/qualifications.tsx` | 四态齐全(`LoadingState` / `ErrorState` / `EmptyState` / 成功),颜色全取自 `src/theme` | |
|
|
|
|
|
|
|
公示三类证照:**营业执照 / ICP 备案 / 人力资源服务许可证**。 |
|
|
|
|
|
|
|
#### 2. 一张图都没有,所以逐张写明「此处需填充什么图」 |
|
|
|
|
|
|
|
按 AGENTS.md「需要填充图片的地方…请标明这里需要填充什么图片」,每张证照渲染虚线占位块 + 具体补图说明,而不是留空: |
|
|
|
|
|
|
|
- 营业执照 →「原件照片或彩色扫描件(须能辨认统一社会信用代码与登记机关公章)」 |
|
|
|
- ICP 备案 →「工信部备案系统查询结果截图(须含备案号与主办单位名称)」 |
|
|
|
- 人力资源服务许可证 →「原件照片或彩色扫描件(招聘平台合规所需)」 |
|
|
|
|
|
|
|
#### 3. 不编造证件编号 —— 并用测试把它锁死 |
|
|
|
|
|
|
|
**统一社会信用代码 / 备案号 / 许可证编号**这些只能由运营主体与法务提供,当前尚未取得,故 `number` / `issuer` / `validPeriod` 一律为 `null`,页面显示「待补充」。 |
|
|
|
|
|
|
|
> 这里可以「先填个像样的号让页面好看」,但编造的证件号既是不实信息,**也可能撞上某个真实主体的号**。故不填。 |
|
|
|
|
|
|
|
新增 `src/__tests__/qualifications.test.ts`(**10 例**)把这条原则锁住,其中最关键的一例是 |
|
|
|
「不编造证件编号:真实材料到位前 number 必须全部为 null」。 |
|
|
|
|
|
|
|
**并做了灵敏度验证(只跑通不算数)**:临时塞入 `91310000MA1FL1234X` 后,该例**如期失败**,其余 9 例仍通过;随后已完全还原并用 grep 复核。一个不会被违反的断言等于没写。 |
|
|
|
|
|
|
|
测试还锁住了两条一致性关系:运营主体名字与「关于我们」**同源**(本项目有「同一个名字被填成两个」的前科,故直接取 `MOCK_ABOUT_INFO.company`,不重复填写);占位说明必须**含证照名**(防止退化成一句通用废话)。 |
|
|
|
|
|
|
|
#### 4. 恢复两处入口(有页面出口就一定要有入口) |
|
|
|
|
|
|
|
- `app/settings.tsx`「通用」组 → 新增「资质证照」(置于「清除缓存」「关于我们」之间) |
|
|
|
- `app/settings/about.tsx` 的链接列表 → 新增「资质证照」 |
|
|
|
|
|
|
|
#### 5. 顺带修掉一类脚本看不见的假交互:「关于我们」页两条死链 |
|
|
|
|
|
|
|
`MOCK_ABOUT_INFO.links` 里「隐私政策」「用户服务协议」**没有 `route`**,而页面的处理函数是这样的: |
|
|
|
|
|
|
|
```ts |
|
|
|
if (link.route) { router.push(link.route); return; } |
|
|
|
// 协议类页面待开发:仅占位,避免死路由 |
|
|
|
``` |
|
|
|
|
|
|
|
即**有 `onPress`、点下去却什么都不发生** —— 既无跳转也无提示。审计脚本查不到这一类(它有处理函数,不是空函数),属 4.9 首条所说的同一盲区。 |
|
|
|
|
|
|
|
而这两个页面**早已存在**(`app/settings/privacy-policy.tsx` / `user-agreement.tsx`),且**同一批链接在设置菜单里本来就配了 route** —— 是漏接线,不是待开发。已补齐两条 route。 |
|
|
|
|
|
|
|
同时把页面的兜底分支由「静默什么都不做」改为**不做成可点**:无 route 的条目渲染成普通行 + 如实标注「待开放」,不再给一个静默失效的箭头。 |
|
|
|
|
|
|
|
**实测**:点「隐私政策」已能跳转并渲染全文(此前无任何反应)。 |
|
|
|
|
|
|
|
#### 6. 实测结果(Android 模拟器 `emulator-5554`,非代码走查) |
|
|
|
|
|
|
|
| 项 | 结果 | |
|
|
|
|---|---| |
|
|
|
| 页面渲染 | 三张证照卡 + 各自补图占位 + 页脚核验提示,均正常 | |
|
|
|
| 设置 → 资质证照 | 点击跳转正常 | |
|
|
|
| 关于我们 → 资质证照 | 点击跳转正常 | |
|
|
|
| 关于我们 → 隐私政策 | **原点了毫无反应,现正常跳转并渲染全文** | |
|
|
|
| JS 报错 | logcat 无 `ReactNativeError` / `RedBox` / `Invariant Violation` | |
|
|
|
|
|
|
|
**实测中发现并修掉一个布局缺陷**:证照信息行原写死 `infoLabel: { width: 96 }`,「统一社会信用代码」八个字放不下、**折成两行**,行高被撑开、右侧「待补充」位置错乱。改为按内容撑开(`flexShrink: 0`)+ `minWidth: 96` 让短标签左对齐。**这个缺陷只有截图才看得出来,代码走查发现不了。** |
|
|
|
|
|
|
|
#### 7. 另一处「文档与代码不符」(本批次发现,尚未处理) |
|
|
|
|
|
|
|
文档第 335 行①称合规页脚「已填 `15154608969`……**填入即自动出现并接 `tel:` 拨号**」。 |
|
|
|
实测:`SUPPORT_HOTLINE` / `SUPERVISION_PHONE` 在 `src/constants/index.ts` 有定义,但**全仓 grep 无任何消费者** —— 没有一处代码读它,所以**永远不会有页脚出现**。 |
|
|
|
|
|
|
|
即那句「填入即自动出现」目前是不成立的:缺的不是号码,是**页脚组件本身**。 |
|
|
|
要么补一个合规页脚(需产品定位置),要么把该条描述改回如实。**不擅自新建页脚**(涉及放哪一页、什么形态,属产品决策)。 |
|
|
|
|
|
|
|
#### 8. 提交约定补了一条:**提交之后必须推送** |
|
|
|
|
|
|
|
`AGENTS.md` 原约定只写「每天 17:50 **提交**」,没写「推送」。实测后果: |
|
|
|
**2026-09-11 至 09-17 整整一周的 12 个提交从未离开本机**,远端一直停在 `f6d4202`(09-10)。 |
|
|
|
影响:① 这段工作只存在于一台机器上,磁盘故障即丢失;② 后端同事看不到前端接口契约; |
|
|
|
③ 9:20 的「拉取」形同虚设(本地领先 12、落后 0,拉不到任何东西)。 |
|
|
|
|
|
|
|
已推送(`f6d4202..00522d5`,现 `0 0` 同步),并把约定改为**两步:`git commit` → `git push`**。 |
|
|
|
|
|
|
|
#### 9. 本批次门槛复跑(2026-09-18 实测) |
|
|
|
|
|
|
|
`tsc --noEmit` **0 错误** · `eslint .` **0 error** · `npm test` **20 个文件 251 例全绿** · `check-routes` **0 失效跳转** · `check-affordances` **0 处** · `check-backend-stubs` **140 个接口函数**。 |
|
|
|
|
|
|
|
#### 10. 本批次仍未做的 |
|
|
|
|
|
|
|
- **资质证照的真实内容**:三类证照的编号 / 发证机关 / 有效期 / 证照图**全部待法务与资质方提供**。页面与数据契约已就绪,材料到位后只改 `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.11 企业端状态文案缺陷 + 状态映射下沉(2026-09-18 批次) |
|
|
|
|
|
|
|
#### 0. 缺陷:草稿说自己是「重新上架」 |
|
|
|
|
|
|
|
4.9 第 4 条做的写通道,回模拟器复看时发现第一张卡片(标着**草稿**)的按钮是「**重新上架**」。溯源到 `app/(enterprise)/job-management.tsx`: |
|
|
|
|
|
|
|
```tsx |
|
|
|
const isActive = job.status === 'active'; |
|
|
|
... |
|
|
|
{isActive ? '下架' : '重新上架'} |
|
|
|
``` |
|
|
|
|
|
|
|
`JobManagementJobStatus` 有三个值(`active | offline | draft`),这个三元只分了两支 —— **草稿落进了「已下线」那一支**。确认框更明显:`重新上架「X」后,候选人可以再次看到该岗位。` 草稿**从未上架过**,「重新」「再次」两句都不成立。(同一处确认按钮写的是「上架」,说明作者当时自己也没统一。) |
|
|
|
|
|
|
|
#### 1. 修法:不是补个三元,是把映射移到 API 层 |
|
|
|
|
|
|
|
页面里再补一层 `status === 'draft' ? … : …`,只会让第二个同款缺陷更容易出现。按 AGENTS.md「业务规则放 API 层」,新增两个纯函数: |
|
|
|
|
|
|
|
- `buildJobStatusAction(job)`(`src/api/api.ts`)→ `{ label, icon, confirmTitle, confirmMessage, confirmButton, nextStatus, destructive }` |
|
|
|
- `getJobStatusLabel(status)`(`src/api/api.ts`)→ 角标文案,**从 `JOB_MANAGEMENT_TABS` 反查**,不另立一份 |
|
|
|
- `JobStatusAction`(`src/types/enterprise.ts`)→ 类型里写明「草稿与已下线必须给出不同文案」 |
|
|
|
|
|
|
|
| 状态 | 按钮 | 目标 | 破坏性 | 确认框正文 | |
|
|
|
|---|---|---|---|---| |
|
|
|
| 招聘中 | 下架 | `offline` | 是 | 下架「X」后,候选人将无法再看到该岗位,已投递的简历不受影响 | |
|
|
|
| 已下线 | 重新上架 | `active` | 否 | 重新上架「X」后,候选人可以**再次**看到该岗位 | |
|
|
|
| 草稿 | 上架 | `active` | 否 | 上架「X」后,候选人**即可**看到该岗位 | |
|
|
|
|
|
|
|
页面侧同时删掉了自己维护的 `STATUS_LABEL`(三个标签与 `JOB_MANAGEMENT_TABS` 逐字重复,会各改各的)。**严格说这不算用户可见缺陷,属顺带去重**,单独列出来是为了不夸大。 |
|
|
|
|
|
|
|
#### 2. 测试锁 + 灵敏度验证 |
|
|
|
|
|
|
|
`src/__tests__/job-management-write.test.ts` 新增 9 例。关键几条: |
|
|
|
|
|
|
|
- 草稿分支:`label` 不得含「重新」、`confirmMessage` 不得含「再次」 |
|
|
|
- 草稿与已下线的按钮/标题/正文**三者都必须不同**(两种处境不能共用一句话) |
|
|
|
- 角标文案与 `fetchJobManagement().tabs` 逐字一致(同源,不是又抄一份字面量) |
|
|
|
- 遍历全部 12 个岗位:`nextStatus` 与当前状态不同,且符合写死的目标契约 |
|
|
|
|
|
|
|
**灵敏度验证**:把草稿分支的 `label` 改回「重新上架」→ **2 例立刻转红** → 完整回退并 grep 确认还原。只跑通测试不能说明测试有效,这一步不能省。 |
|
|
|
|
|
|
|
#### 3. 实测结果(Android 模拟器 `emulator-5554`,非代码走查) |
|
|
|
|
|
|
|
| 项 | 结果 | |
|
|
|
|---|---| |
|
|
|
| 草稿卡片按钮 | 「上架」(修复前是「重新上架」) | |
|
|
|
| 草稿确认框 | 「上架岗位 / …候选人**即可**看到该岗位」,无「重新」无「再次」 | |
|
|
|
| 已下线卡片按钮 | 「重新上架」 | |
|
|
|
| 已下线确认框 | 「重新上架 / …候选人可以**再次**看到该岗位」 | |
|
|
|
| 点「上架」后的计数 | 草稿 1→0、招聘中 7→8、在招岗位统计 7→8、全部仍 12 | |
|
|
|
| 空掉的草稿 Tab | 正确显示空态「这个分类下还没有岗位」 | |
|
|
|
| JS 报错 | logcat 无 `ReactNativeError` / `RedBox` / `Invariant Violation` | |
|
|
|
|
|
|
|
#### 4. 两处如实记录 |
|
|
|
|
|
|
|
- **模拟器上的草稿样本被我用掉了**:验证「上架」写入时把那张草稿变成了在招,**已落库**。UI 只能在 `active`↔`offline` 之间切,**回不到 `draft`**;且「设置 → 清除缓存」只删文件系统缓存目录(`src/lib/cache.ts`),**不动 AsyncStorage**,清不掉 `youzhi:enterprise:jobs-status`。要把草稿样本变回来只能清应用数据(`adb shell pm clear`,会一并清掉登录态)。属本机开发数据,不影响仓库。 |
|
|
|
- **这不是又一个「文档说已清零」**:本缺陷是**代码走查没发现、复看截图才发现**的(与 4.10 第 6 条的标签折行同一类)。4.9 第 4 条当时只跑了 tsc/eslint/单测,这三道门槛都看不见「文案对不对」。 |
|
|
|
|
|
|
|
#### 5. 门槛复跑(2026-09-18 实测) |
|
|
|
|
|
|
|
`tsc --noEmit` **0 错误** · `eslint .` **0 error** · `npm test` **20 个文件 260 例全绿**(较 4.10 的 251 例 +9) · `check-routes` **0 失效跳转** · `check-affordances` **0 处** · `check-backend-stubs` **140 个接口函数**。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### 4.12 六处导出 / 分享真机验证 + 霍兰德参与人数两处不一致(2026-09-18 批次) |
|
|
|
|
|
|
|
#### 1. 为什么这次验证比前几次更硬 |
|
|
|
|
|
|
|
4.9 把 6 处「点了只弹功能开发中」改成了真导出,但一直**只有代码走查,没有设备验证**,在 4.9 第 8 条里如实挂着「未验证」。 |
|
|
|
|
|
|
|
这次能验得硬,是因为一个偶然的有利条件:**Android 系统分享面板会预览即将分享的文本**。所以截图不只是「面板弹出来了」,还能顺便读到**拼装出来的内容对不对**——数字、名称、状态是否与页面一致,一眼可核。 |
|
|
|
|
|
|
|
全仓 `shareText` 调用点共 **7 处**,本轮验了 6 处;另一处是 `app/onboarding.tsx`(入职清单导出)—— **该处已于 4.13 第 10 条实测通过,至此 7 处全部验过**。 |
|
|
|
|
|
|
|
#### 2. 实测结果(Android 模拟器 `emulator-5554`) |
|
|
|
|
|
|
|
| # | 页面 | 入口 | 分享面板预览(原文摘录) | 与页面是否一致 | |
|
|
|
|---|---|---|---|---| |
|
|
|
| 1 | `jobs/[id]` 岗位详情 | 右上分享 | 高级前端工程师|字节跳动 / 北京·朝阳区·3-5年·本科 / 薪资 25-40K·14薪… | ✅ | |
|
|
|
| 2 | `settings/privacy` 隐私设置 | 导出隐私设置摘要 | 我的隐私设置摘要(当前等级:较高)/ 你的数据保护状态良好… | ✅ 等级与页面一致 | |
|
|
|
| 3 | `interview` AI模拟面试 | 右上分享 | AI 模拟面试配置(高级前端工程师·字节跳动)/ 模式:全真模拟 / 难度:中等·时长:30分钟… | ✅ | |
|
|
|
| 4 | `jobs/offer-compare` | 导出对比报告 | 【Offer 对比报告】/ 参与对比 2 个 Offer… | ✅ 与页面 2 个 Offer 一致 | |
|
|
|
| 5 | `jobs/rank` 竞争力排名 | 导出分析报告 | 竞争力分析报告(高级前端工程师·字节跳动)/ 综合评分 88 分(优秀)/ 当前排名 127/1286·超越 90.1% 候选人·前 9.9… | ✅ 127 / 1286 / 90.1% / 9.9% / 88 分 全中 | |
|
|
|
| 6 | `holland-result` 测评结果 | 右上分享 | 霍兰德职业兴趣测评报告 / 类型代码:ISR·研究型+社会型+现实型… | ✅ 与页面 ISR 型一致 | |
|
|
|
|
|
|
|
**6 处全部真拉起系统面板,内容与页面一致,无一处谎报成功。** |
|
|
|
|
|
|
|
#### 3. 一个能证伪的实验:分享的到底是「本地选择」还是「接口默认值」 |
|
|
|
|
|
|
|
`app/interview.tsx` 的分享实现特意传入**页面上的当前选择**(`mode` / `questionTypes` / `specials`),而不是接口默认值 —— 但面板预览只显示 3 行,题型那行被截掉,光看截图证不了。 |
|
|
|
|
|
|
|
于是改一个**第 2 行可见**的字段做对照实验: |
|
|
|
|
|
|
|
| 步骤 | 面板预览第 2 行 | |
|
|
|
|---|---| |
|
|
|
| 模式选「全真模拟」后分享 | 模式:全真模拟 | |
|
|
|
| 同一页面改选「快速问答」后再分享 | 模式:**快速问答** | |
|
|
|
|
|
|
|
同一页面、同一接口数据,只有本地选择变了,分享文本随之变 —— **证明它读的是本地选择态,不是接口默认值**。这个结论只看代码得不出来。 |
|
|
|
|
|
|
|
#### 4. 顺带发现的新缺陷:霍兰德参与人数两处不一致(已修) |
|
|
|
|
|
|
|
验证第 6 条时经过霍兰德介绍页,看到「已有 **124,580** 人完成测评」;而成长页同一张卡片写的是「已有 **12,846** 人完成」。查证属实 —— `src/api/api.ts` 里有两个互不相干的 mock: |
|
|
|
|
|
|
|
- `MOCK_HOLLAND.totalParticipants = 12846`(测评会话,成长页卡片读它) |
|
|
|
- `MOCK_HOLLAND_INTRO.totalParticipants = 124580`(介绍页读它) |
|
|
|
|
|
|
|
**同一个统计量、同一个用户、隔一屏就是两个数。** 与之前修过的「全部 12 / 已下线 3」是同一类缺陷(一个数字两处来源),也正是本文件反复强调「数字必须对得上」的那一类。 |
|
|
|
|
|
|
|
**修法**:抽出 `HOLLAND_TOTAL_PARTICIPANTS` 单一常量,两处引用它。 |
|
|
|
|
|
|
|
**关于取值**:两个数都没有事实依据(mock),我取了 `124580`(介绍页那个,也是用户先看到的那个)。**这是判断不是事实**,真实人数应由后端下发;接后端时删掉该常量,两处改为各自读接口返回值。 |
|
|
|
|
|
|
|
**测试锁**:新增 `src/__tests__/holland.test.ts`(2 例),核心一条是「介绍页与测评页的人数必须一致」。 |
|
|
|
**灵敏度验证**:把测评页改回独立数字 `12846` → 该例立刻转红(`Expected: 12846 / Received: 124580`)→ 完整回退并 grep 确认两处都在引用常量、无裸字面量残留。 |
|
|
|
|
|
|
|
**设备复验**:改完后成长页显示「已有 **124,580** 人完成」,与介绍页一致。 |
|
|
|
|
|
|
|
#### 5. 两处「同页两套标准」(本轮发现,未改) |
|
|
|
|
|
|
|
| 文件 | 现象 | 为什么不改 | |
|
|
|
|---|---|---| |
|
|
|
| `app/jobs/offer-compare.tsx:243` | 错误态是**手写** UI(`<Text>加载失败,请重试</Text>` + 自绘重试按钮),而不是全项目统一的 `<ErrorState onRetry={refresh} />` | **功能是对的**(重试真的接了 `refresh`),只是写法不统一。按「StateView 先不全改,只改本轮碰到的页」的决定,本轮碰到的页本该顺手改,但改它会把 4.12 的 diff 混进样式调整,故单列记录、留待专门批次 | |
|
|
|
| `app/jobs/rank.tsx:266` | 加载态是裸 `<ActivityIndicator>`,不是 `<LoadingState />` | 同上,纯视觉规范统一 | |
|
|
|
|
|
|
|
两处都**不是缺陷**,不影响功能,登记在此是为了不丢失。 |
|
|
|
|
|
|
|
#### 6. 深链不路由:原因已查明(**2026-09-18 更正,原解释是错的**) |
|
|
|
|
|
|
|
验证过程中试过 `youzhi://jobs/1`、`youzhi://settings/privacy` 等深链,**均未跳转**,app 只是恢复了上次的路由。 |
|
|
|
|
|
|
|
这件事本节最初写的是「当前跑的是 dev build,scheme 很可能由 **expo-dev-client** 的 launcher 接管,生产包行为未必相同」。 |
|
|
|
**这个解释是错的**,必须更正 —— 一个错的归因比「未知」更危险:它会让人去查 dev-client 配置,而那里根本没有问题。 |
|
|
|
|
|
|
|
**实测证据(三条互不依赖)**: |
|
|
|
|
|
|
|
| # | 命令 / 检查 | 结果 | |
|
|
|
|---|---|---| |
|
|
|
| 1 | `grep expo-dev-client package.json` | **无此依赖**(只有 `expo: ~57.0.22`)—— 项目没装 dev-client | |
|
|
|
| 2 | `adb shell pm list packages -3` | 本机第三方包**只有 `host.exp.exponent`**(即 Expo Go),无本应用的原生包 | |
|
|
|
| 3 | 前台 Activity | `host.exp.exponent/.experience.ExperienceActivity` —— 跑在 Expo Go 里 | |
|
|
|
|
|
|
|
**根因(一条命令直接证死)**: |
|
|
|
|
|
|
|
```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 "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://`。 |
|
|
|
|
|
|
|
**结论**:这是 **Expo Go 的已知限制,不是本项目的缺陷**。所以 —— |
|
|
|
|
|
|
|
- 「`youzhi://` 在 Expo Go 里点不动」**永远测不出别的结果**,不必再试; |
|
|
|
- 生产包是否路由,**仍未验证**(本机没有原生包可测),但**成因已不再是谜**:只要出包后 AndroidManifest 里的 scheme 生效即可。**建议首次出包后专门验一次**,不要等上线才发现; |
|
|
|
- 潜在影响面如前述:微信登录回调、分享出去的内容被点开等,都依赖这个 scheme。 |
|
|
|
|
|
|
|
**顺带得到一个可复用的验证手段**:Expo Go 下可用 |
|
|
|
`adb shell am start -d "exp://127.0.0.1:8081/--/<路由>"` |
|
|
|
直接跳到任意路由。4.13 那 6 个空态就是靠它逐个抵达的(比一层层点进去快且不易点错)。 |
|
|
|
|
|
|
|
#### 7. 本轮本机副作用(如实记录) |
|
|
|
|
|
|
|
- 为够到 `holland-result`,在模拟器上答了 4 道题(该 mock 的 `nextIndex >= 3` 即跳结果页)。`submitHollandAnswer` 是空壳、不写存储,故**成长页仍显示「立即测评」**,没有留下持久状态。 |
|
|
|
- 为够到分享按钮,把 Expo 调试悬浮球从右上拖到了屏幕下方(否则它盖住所有页面右上角的分享图标)。 |
|
|
|
|
|
|
|
#### 8. 门槛复跑(2026-09-18 实测) |
|
|
|
|
|
|
|
`tsc --noEmit` **0 错误** · `eslint .` **0 error** · `npm test` **21 个文件 262 例全绿**(较 4.11 的 260 例 +2) · `check-routes` **0 失效跳转** · `check-affordances` **0 处** · `check-backend-stubs` **140 个接口函数**。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### 4.13 六处空态实测 + 无效图标名缺陷 + `check:icons` 新门槛 + `settings` 缓存失败态结案 + 分享文本测试锁(2026-09-18 批次) |
|
|
|
|
|
|
|
#### 1. 这一轮在补什么 |
|
|
|
|
|
|
|
4.9 第 8 条「仍未验证」清单里,4.11 关掉了「重新上架点击路径」、4.12 关掉了「6 处导出分享」,本节关掉最后两项:6 个空态(第 3 条),以及 `settings` 缓存读取失败态(第 8 条 —— 结论不是「验不了」,而是**那条分支根本不可达**)。 |
|
|
|
|
|
|
|
那 6 个空态此前只有一句代码走查式的推断:「均为同一条件渲染写法,只实测了成长页两处」。**推断不是验证** —— 本文件吃过太多次「代码看着对、设备上是错的」的亏,故补测。 |
|
|
|
|
|
|
|
**为什么这 6 处非测不可**:光读 `x.length === 0 ? <空态> : <列表>` 这行守卫,证明不了它**可达**。页面完全可能在更早的地方 `return`(`if (!data) return <LoadingState />`),或整块被父级条件包住 —— 那空态就是一坨**永远渲染不到的死代码**,跟没写一样。这类问题静态读代码读不出来,只能把数据真的弄空。 |
|
|
|
|
|
|
|
#### 2. 方法:注入空数据 → 截图 → 全量还原 |
|
|
|
|
|
|
|
mock 数据永远非空,所以空态**在正常运行中不可达**,只能注入。方法与 4.9 那次相同,但这次一次性注入 6 处、跑完再统一还原: |
|
|
|
|
|
|
|
- 注入前 `cp src/api/api.ts /tmp/api.ts.bak` 留底(**不做手工反向编辑**,避免漏还原) |
|
|
|
- 用 `node -e` 打补丁,**每处都断言「匹配次数 === 1」**,匹配数不对就整体中止不写盘(防止正则误伤别处) |
|
|
|
- 还原后核对 **md5 与注入前备份完全一致**,并 grep 扫描全部注入特征串 |
|
|
|
|
|
|
|
**还原证据**: |
|
|
|
|
|
|
|
``` |
|
|
|
$ md5sum /tmp/api.ts.bak src/api/api.ts |
|
|
|
956a855445b9392886c47a9804b056e7 */tmp/api.ts.bak |
|
|
|
956a855445b9392886c47a9804b056e7 *src/api/api.ts ← 逐字节一致 |
|
|
|
|
|
|
|
$ grep -n "slice(0, 0)\|companies: \[\]\|reviews: \[\]\|improvements: \[\]\|offers: \[\], scores: \[\]\|as LegalService\[\]" src/api/api.ts |
|
|
|
✅ 无残留 |
|
|
|
``` |
|
|
|
|
|
|
|
> 这里用 **md5 相等**而不是「grep 没搜到残留」作主证据:grep 只能证明「这几串没了」,md5 能证明「整个文件与注入前一个字节不差」。前者是抽样,后者是全集。 |
|
|
|
|
|
|
|
#### 3. 实测结果(Android 模拟器 `emulator-5554`) |
|
|
|
|
|
|
|
| # | 页面 | 注入成空的数据 | 实测渲染 | 同页其它内容是否受影响 | |
|
|
|
|---|---|---|---|---| |
|
|
|
| 1 | `progress` 求职进度 | `companies` | 「该阶段暂无投递记录 / 投递岗位后,进度会在这里按阶段更新」+ 公文包图标 | ✅ 常驻标题「求职中 2」仍在,下方 Offer 区块正常 | |
|
|
|
| 2 | `legal` 法律服务 | 服务网格 | 「暂无免费法律服务」「暂无会员法律服务」**两个网格各自独立**出空态 | ✅ 文书模板 / FAQ / 会员套餐均正常 | |
|
|
|
| 3 | `hr-evaluation` HR评价 | `reviews` | 空态卡 + 对话气泡图标 | ✅ 且标题计数显示为 **「HR评价(0)」** | |
|
|
|
| 4 | `ability` 四维能力 | `improvements` | 「完成测评后,这里会给出针对性的提升建议」+ 灯泡图标 | ✅ 四维分数、趋势图正常(注入精准,未误伤) | |
|
|
|
| 5 | `jobs/offer-compare` | `offers` | 「暂无可对比的 Offer」+ 副文案 | ✅ **下方三个区块正确消失** | |
|
|
|
| 6 | `jobs/[id]` 岗位详情 | `reviews` | 「暂无员工评价 / 成为第一个分享这家公司真实情况的人」+ 气泡图标 | ✅ 常驻标题「员工评价」仍在 | |
|
|
|
|
|
|
|
**6 处全部按设计渲染,无一处是死代码。** 第 3 条附带证明了一件事:标题里的 `(0)` 是**由数据推导**的,不是写死的(数据空 → 计数跟着变 0)。 |
|
|
|
|
|
|
|
第 5 条尤其值得记:这个守卫的**目的**就是「offers 为空时,下方三个对比区块会渲染成空壳」—— 实测确认三个区块确实消失了,说明守卫挡在了正确的位置。 |
|
|
|
|
|
|
|
#### 4. 注入过程中发现的新缺陷:`gavel-outline` 不是合法图标名(已修) |
|
|
|
|
|
|
|
在 `legal` 页注入空数据时,屏幕底部弹出一条此前从未见过的报错: |
|
|
|
|
|
|
|
``` |
|
|
|
"gavel-outline" is not a valid icon name for family ... |
|
|
|
``` |
|
|
|
|
|
|
|
**这是个真缺陷,且一直存在** —— 它跟注入无关,只是 4.9 那批空态此前**从未真正渲染过**,所以这页没被认真走过。 |
|
|
|
|
|
|
|
根因在 `src/api/api.ts` 的文书模板数据里: |
|
|
|
|
|
|
|
``` |
|
|
|
name: '劳动仲裁申请书', icon: 'gavel-outline' ← 原值 |
|
|
|
``` |
|
|
|
|
|
|
|
核对 Ionicons 全库 **1357 个字形,`gavel` 一个都没有**(`gavel-outline`、`gavel` 均不存在,含 "gavel" 的字形 0 个)。后果:「劳动仲裁申请书」这一条**图标渲染为空白**。 |
|
|
|
|
|
|
|
**为什么 `tsc` 0 错误也拦不住** —— 这是本节最值得记的一点。页面写的是: |
|
|
|
|
|
|
|
```tsx |
|
|
|
<Ionicons name={template.icon as keyof typeof Ionicons.glyphMap} size={22} ... /> |
|
|
|
``` |
|
|
|
|
|
|
|
这个 `as` 断言把「任意字符串」硬骗成合法字形名,**编译器就此闭嘴**。而图标名来自数据层(`api.ts` 的 mock),类型本就是 `string`,静态类型无从校验。**类型系统在这里是盲的,不是「没查到」,是「查不了」。** |
|
|
|
|
|
|
|
**修法**:换成 `hammer-outline`(Ionicons 里真实存在)。选它的理由:原作者的意图明显是「法槌」,而 Ionicons 没有法槌,锤子是语义最接近的等价物;且同组另两条也各按语义取图标(`checkbox-outline` / `exit-outline`)。 |
|
|
|
|
|
|
|
**顺带做了全仓同类扫描**,结果分两类: |
|
|
|
|
|
|
|
| 命中 | 判定 | |
|
|
|
|---|---| |
|
|
|
| `gavel-outline` | **真缺陷**,已修 | |
|
|
|
| `🏅 🎯 ⭐ 🏆 📮 📚`(6 个) | **误报** —— 这是 `MOCK_BADGES` 的勋章图标,由 `<Text>{badge.icon}</Text>` 直接当字符渲染,本就该是 emoji 不是 Ionicons 名 | |
|
|
|
|
|
|
|
**设备复验**:修复后重启,「劳动仲裁申请书」的**锤子图标正常渲染**(蓝色),且 `adb logcat` 中 `not a valid icon name` **零条**。 |
|
|
|
|
|
|
|
#### 5. 新增门槛 `npm run check:icons`(防复发) |
|
|
|
|
|
|
|
只修这一处是治标 —— 只要数据层还能塞任意字符串、页面还在用 `as` 断言,同类问题就会再来。故把这次的人工扫描**固化成脚本** `scripts/check-icons.mjs`: |
|
|
|
|
|
|
|
- 字形表**直接从装的包里读**(`@expo/vector-icons` 的 `Ionicons.json`),**不另存一份** —— 另存一份必然与真实图标库脱节,那就是又一个「两处来源」 |
|
|
|
- 查 `<Ionicons name="...">` 字面量与 `icon: '...'` 两种写法 |
|
|
|
- emoji 白名单判据取**「是否含非 ASCII 字符」**,不硬编码那 6 个值(以后加勋章不必回来改脚本) |
|
|
|
- 与其它门槛一致,`--error` 模式有命中即 `exit 1` |
|
|
|
|
|
|
|
**灵敏度验证(必须做,否则「通过」毫无意义)**:把 `hammer-outline` 注入回 `gavel-outline` → 脚本**准确报出 `src\api\api.ts:6166`、命中名与命中行、exit 1** → 完整还原 → 脚本复跑转绿、`grep gavel` 仅剩注释里的说明文字(无代码引用)。 |
|
|
|
|
|
|
|
**已知边界**(如实记录):只查**静态字面量**。运行时拼出来的图标名(`` icon: `tab-${i}` ``)查不了 —— 本仓目前没有这种写法。 |
|
|
|
|
|
|
|
#### 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 已知限制,非项目缺陷。 |
|
|
|
|
|
|
|
#### 7. 本轮本机副作用(如实记录) |
|
|
|
|
|
|
|
- 为凑齐 6 个空态,在模拟器上注入过空数据并**已全量还原**(md5 逐字节核对,见第 2 节)。还原后继续做图标修复,之后**重启过一次应用**用于复验图标,**当前处于正常的非空数据态**。 |
|
|
|
- 截图产物 `inj-*.png` / `fix-*.png` 为验证中间物,**不留在仓库**(已 `rm` 并确认无残留)。 |
|
|
|
- 另:`/tmp/api.ts.bak` 是 api.ts 的注入前留底,在系统临时目录,**不在仓库内**。 |
|
|
|
|
|
|
|
#### 8. 最后一项的真相:`settings` 缓存「读取失败」态**不可达**,不是「未验证」 |
|
|
|
|
|
|
|
4.9 第 8 条把这项记成「需**文件系统级故障**才能触发,`mockRequest` 造不出来,故未验证」。这个记载**不准确** —— 顺着调用链读下去发现,它不是「没有条件验」,而是**任何条件下都走不到**: |
|
|
|
|
|
|
|
``` |
|
|
|
app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCacheBytes) |
|
|
|
↑ |
|
|
|
try { ... } catch { return 0 } ← 故障在这里被吞掉 |
|
|
|
``` |
|
|
|
|
|
|
|
`src/lib/cache.ts` 的 `readCacheBytes()` 把 **所有** 异常吞掉并返回 `0`(目录不存在、无权限、原生模块缺失,一律如此)。`0` 是合法体积值,调用方无从区分「真的是 0」与「读失败了」。因此 `useAsyncData` 永远拿不到 rejection → `error` **恒为 `null`** → 页面 `cacheError ? '读取失败'` 分支是**死代码**。 |
|
|
|
|
|
|
|
**这解释了 4.9 那次注入实验为什么失败**:当时注入全局失败后「仍正常读出 381KB」,不是因为故障不够「文件系统级」,而是故障在这条链上**根本没有出口**。 |
|
|
|
|
|
|
|
**这是可证的,不需要设备** —— 新增 `src/__tests__/cache.test.ts`(12 例),用 mock 直接构造「会抛错的文件系统」,锁住两条契约: |
|
|
|
|
|
|
|
| 锁住的契约 | 对应用例 | |
|
|
|
|---|---| |
|
|
|
| `readCacheBytes()` 永不抛错:构造失败 / 目录不存在 / 正常路径,分别返回 0 / 0 / 体积 | 4 例 | |
|
|
|
| `fetchCacheSize()` / `clearAppCache()` **resolve 而非 reject**(哪怕文件系统在抛错) | 2 例,即「error 恒为 null」的直接证据 | |
|
|
|
| 清缓存的容错:单个条目删不掉不阻断其余;列目录失败返回 0 | 3 例 | |
|
|
|
|
|
|
|
**灵敏度验证(必须做)**:把 `readCacheBytes` 的 `try/catch` 摘掉、让故障上抛 → **恰好 3 例转红**,且正是上面那三条例外契约的用例(`构造目录抛错时返回 0` / `fetchCacheSize 仍然 resolve` / `clearAppCache 仍 resolve`)→ 从备份完整还原(**md5 逐字节一致**)→ 复跑 12 例全绿。 |
|
|
|
|
|
|
|
**判定:这不是缺陷,是一项需要如实记载的设计取舍。** `cache.ts` 的注释写明了理由(「清缓存本身不该因为读不到体积而失败」)—— 读不到体积时显示 `0B` 而非报错,是有意为之。**但由此产生的后果必须写清楚**:`settings` 页那个「读取失败」分支**永远不会显示**,不要把它当成已覆盖的错误态。 |
|
|
|
|
|
|
|
> 与 `app/onboarding.tsx` 空态的对比(同属「不可达」但成因不同): |
|
|
|
> 那个是**已知不可达、故干脆没写**(4.9 记录在案);这个是**写了、但不可达**。后者更危险 —— 它看起来像「错误态已覆盖」,实际是个摆设。 |
|
|
|
|
|
|
|
#### 9. 仍未验证(不要当成验过) |
|
|
|
|
|
|
|
- **生产包的 `youzhi://` 深链**:本机无原生包,测不了(成因已查明,见 4.12 第 6 条)。**这是唯一一项真正的「未验证」。** |
|
|
|
- 4.9 第 8 条其余项目已全部关闭:企业端「重新上架」见 4.11,6 处导出分享见 4.12,6 处空态见本节第 3 条,`settings` 缓存失败态见本节第 8 条(结论是「不可达」而非「未验证」)。 |
|
|
|
|
|
|
|
#### 10. 第七处分享路径实测 + 5 个拼装函数补上测试锁 |
|
|
|
|
|
|
|
**(1)第七处分享路径:`app/onboarding.tsx` 入职清单导出** |
|
|
|
|
|
|
|
4.12 验了 6 处,唯独漏了这处。本次补上(`exp://` 深链直达 `/onboarding`,点右上分享): |
|
|
|
|
|
|
|
| 核对项 | 分享面板预览 | 与页面是否一致 | |
|
|
|
|---|---|---| |
|
|
|
| 首行 | `入职准备清单(字节跳动 · 高级前端工程师)` | ✅ 公司、职位一致 | |
|
|
|
| 次行 | `入职日期 2026-09-06 · 已完成 3/18` | ✅ 日期与「总体进度 3/18」一致 | |
|
|
|
|
|
|
|
**7 处分享路径至此全部实测通过。** |
|
|
|
|
|
|
|
**(2)补测试锁:4.9 方案里写了「并补单测」,当时只补了范式那一个** |
|
|
|
|
|
|
|
`buildChecklistText` 有 3 例测试(它是范式),而 4.9 同批写的另外 5 个拼装函数**一直是 0 覆盖**: |
|
|
|
|
|
|
|
`buildJobShareText` · `buildHollandReportText` · `buildInterviewConfigText` · `buildRankReportText` · `buildPrivacySummaryText` |
|
|
|
|
|
|
|
**为什么这事不能只靠真机验证糊过去**:真机验的是**当时那组 mock 值** —— 面板预览里数字对上了,只说明「这一次拼对了」,不说明「以后一直拼得对」。而分享文本会**离开本机、给到别人看**:页面显示错自己看得见,发出去的内容错了看不见。这几个又都是纯函数,是单测最划算的地方。 |
|
|
|
|
|
|
|
新增 `src/__tests__/share-text.test.ts`(**28 例**),重点锁三类容易悄悄坏掉的地方: |
|
|
|
|
|
|
|
| 类别 | 举例 | 例数 | |
|
|
|
|---|---|---| |
|
|
|
| **空值守卫** | 无评分数据不输出该行(不输出 `null`/`0 分`);薪资月数为 0 不写「0薪」;真实薪资样本数 0 不展示;福利为空不留「福利:」空壳;评价数 0 说「暂无员工评价」而非「员工评价 0 条」 | 约 14 例 | |
|
|
|
| **取的是本地选择态** | 面试配置分享**只输出被选中的题型**,未选中的不得出现 | 6 例 | |
|
|
|
| **对外可见的格式** | 首行格式、`—— 来自「有职」` 签名、不留悬空分隔符、空分组不产生连续空行 | 约 8 例 | |
|
|
|
|
|
|
|
其中「只输出被选中的题型」这条,是把 **4.12 那个真机证伪实验(改选后再分享、面板预览随之变)固化成不依赖设备的断言** —— 今后谁把它改回「输出接口默认值」,不必再上模拟器,跑测试就会红。 |
|
|
|
|
|
|
|
**灵敏度验证(两处注入,分别对应两个类别)**: |
|
|
|
|
|
|
|
| 注入的回归 | 结果 | |
|
|
|
|---|---| |
|
|
|
| 让面试配置**忽略传入的选择**、输出接口全部题型(即 4.12 证伪过的那个错误写法) | **恰好 3 例转红**:核心那条「只输出被选中的题型」+「未知题型 code 被忽略」+「没选题型时该行不出现」 | |
|
|
|
| 摘掉 `buildJobShareText` 的 `overallScore` 空值守卫 | **1 例转红**,正是「没有评分数据时整行不出现」 | |
|
|
|
|
|
|
|
两次注入均从备份完整还原(**md5 逐字节一致**),复跑 28 例全绿。 |
|
|
|
|
|
|
|
> 写测试时自己写错过一条断言,如实记一笔:本想断言「建议没有描述时不留悬空的 `——`」,写成对**整段文本** `not.toContain('——')` —— 而签名行 `—— 来自「有职」` 本身就含 `——`,必然失败。**是断言写错了,不是代码错了**;改为断言**那一行**等于 `· 补足项目数据`。留着这段是因为它说明一个常见坑:断言的**粒度**要落在被测行为上,对整段文本做否定断言很容易被无关内容打脸。 |
|
|
|
|
|
|
|
#### 11. 「数据层引用外部词表」的系统性排查(本轮顺带做完) |
|
|
|
|
|
|
|
4.13 第 4 条的图标缺陷暴露出一类共同形状:**字符串写在数据层,却必须与某个外部词表对得上,而类型系统看不见**。既然找到了这类形状,就顺手把同类词表全查一遍,避免只修个案: |
|
|
|
|
|
|
|
| 词表 | 排查方式 | 结论 | |
|
|
|
|---|---|---| |
|
|
|
| **Ionicons 字形名** | 新建 `check:icons` 扫全仓 | ❌ 曾有 1 处(`gavel-outline`),**已修**;现 0 处 | |
|
|
|
| **路由字符串** | `check-routes` **确认能覆盖数据层**:注入一个拼错的数据层路由 `/favourites-typo` → 脚本准确报出 `src/api/api.ts` 并计数 1 → 还原 | ✅ 已被既有门槛覆盖,**无需新增**(此前未验证过它是否覆盖数据层,本次补验) | |
|
|
|
| **图标字体家族** | 全仓 grep `from '@expo/vector-icons'` | ✅ **只有 `Ionicons` 一家**,故 `check:icons` 覆盖了全部图标面 | |
|
|
|
| **硬编码色值** | `grep '#[0-9A-Fa-f]{3,8}'`(排除 `src/theme/colors.ts`) | ✅ **0 处** —— AGENTS.md 那条「组件内不写死色值」真的成立 | |
|
|
|
| **`rgba()` / 具名颜色** | grep 后逐个看 | ✅ 6 处 `rgba` 全是半透明白/黑叠加层(白卡上的蒙版等),4 处 `'transparent'`;**都不是品牌色**,无对应 token,属正常 | |
|
|
|
| **颜色出现在数据层** | `grep 'color: #...'` in `api.ts` | ✅ **0 处** | |
|
|
|
| **语义色键**(可疑但实为正确设计) | `thumbnailColor: 'violet' \| 'orange' \| 'green'` | ✅ 这是**联合类型的语义键,不是色值**;两个页面各自 `Record<键, 主题token>` 映射。将来加第 4 个键,两处 Record 都会因类型不完整而**编译报错** —— 类型系统正好守住了这个失败模式 | |
|
|
|
|
|
|
|
**结论:三类词表(图标名 / 路由 / 颜色)现已全部被门槛或类型系统覆盖**,其中图标名是本轮新补的。排查过程与结论一并记在此,避免以后重复做。 |
|
|
|
|
|
|
|
#### 12. 门槛复跑(2026-09-18 实测) |
|
|
|
|
|
|
|
`tsc --noEmit` **0 错误** · `eslint .` **0 error** · `npm test` **23 个文件 302 例全绿**(较 4.12 的 262 例 +40:`cache.test.ts` 12 例 + `share-text.test.ts` 28 例) · `check-routes` **0 失效跳转** · `check-affordances` **0 处** · `check-backend-stubs` **140 个接口函数** · `check:icons`(**本轮新增**)**0 处无效图标名**。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 五、后续增量开发建议(优先级) |
|
|
|
|
|
|
|
- **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 末条)。 |
|
|
|
- **P0(补充 · 2026-09-15 新发现)**:① ~~页脚服务热线 / 举报监督电话待填~~ **已填 `15154608969`** —— ⚠️ **但 2026-09-18 实测发现该常量全仓无消费者,页脚从未落地,「填入即自动出现」目前不成立,缺的是页脚组件本身,见 4.10 第 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)** · 真实后端对接 · OTA 通道验证。 |
|
|
|
- **P2(打磨)**:~~技术债清理~~ **技术债已清理(8 处 `any` → 明确类型,`eslint` 0 warning)** · ~~死控件清零~~ **死控件已清零(22 → 0,2026-09-17,见 4.8)** · ~~假交互清零~~ **假交互已清零(37 → 0,2026-09-18,**但 4.8 当时的「已完成」结论是错的,见 4.9 首条**)** · 真实后端对接 · OTA 通道验证。 |
|
|
|
- **P2(补充 · 2026-09-18)**:**3 处知情保留的后端待办**(聊天附件四图标 / 接受 Offer / 导出文件下载),后端接口就绪后按 `npm run check:affordances:pending` 列出的清单逐条兑现;另**已做的真机验证与仍未验证的项**见 4.9 第 8 条(已由 4.11 / 4.12 / 4.13 逐项关闭:`settings` 缓存失败态的最终结论是**不可达死代码**,见 4.13 第 8 条)—— 不要把没验过的当成验过了。 |
|
|
|
- **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 张显示「配图待补 + 该放什么图」标注,头像 / 插画类降级为色块 + 图标(可用的占位态,非空白)。 |
|
|
|
|
|
|
|
--- |
|
|
|
@ -224,12 +842,21 @@ npx eslint . |
|
|
|
# 路由闭合审计(改动路由后必跑) |
|
|
|
node scripts/check-routes.mjs |
|
|
|
|
|
|
|
# 死控件审计(改动交互后必跑;豁免标记 affordance-ok) |
|
|
|
node scripts/check-affordances.mjs |
|
|
|
# 假交互审计(改动交互后必跑;**已设为硬门槛 —— 有命中即 exit 1**) |
|
|
|
# 检测①无处理函数/空处理函数 ②点了只弹「功能开发中/暂未开放」的弹窗式假交互 |
|
|
|
# 豁免标记:affordance-ok(有意为之,如弹层吞冒泡) |
|
|
|
# affordance-pending-backend(知情保留的后端待办) |
|
|
|
npm run check:affordances # 等价 node scripts/check-affordances.mjs --error |
|
|
|
npm run check:affordances:pending # 列出知情保留的后端待办清单(当前 3 处) |
|
|
|
|
|
|
|
# 后端接口预留审计(新增/改接口后必跑;豁免标记 backend-stub-ok) |
|
|
|
node scripts/check-backend-stubs.mjs |
|
|
|
|
|
|
|
# 图标名审计(改动图标后必跑;**已设为硬门槛 —— 有命中即 exit 1**) |
|
|
|
# 查传给 Ionicons 的无效字形名(如 gavel-outline —— 全库 1357 个字形里没有 gavel) |
|
|
|
# 页面用 `as keyof typeof Ionicons.glyphMap` 断言会吞掉这类类型错误,故需要此脚本 |
|
|
|
npm run check:icons # 等价 node scripts/check-icons.mjs --error |
|
|
|
|
|
|
|
# 单元测试(src/__tests__ 下纯逻辑) |
|
|
|
npm test |
|
|
|
|
|
|
|
|