154 KiB
有职 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的配色分歧,未擅自统一,留给产品定夺) 上一版:2026-09-20 · 第二批(把同一类缺陷「同一个事实被写了两份、两处取值不同」排查完剩下的另一半,见 4.15。① 又查出并修掉 10 组同源不同值的数据 —— 求职进度统计卡五项之和 30 ≠ 总数 28、月度图表投递合计 52 / 面试合计 16 而卡片写 28 / 6、我的页收藏徽标 12 与列表 3、勋章「投递满100次 已获得」而实际只投了 28 次、匹配横幅 3 家而matched标记只有 2 条、岗位详情综合分恒为 92(9 个岗位只有第 1 个对得上)、对比页把产品经理与 UI 设计师的分正好写反、候选人 4–8 号点进去全叫「张明远」、岗位 1 的公司所在地在列表与详情各写一个城区、待回复卡片写 3 人而它跳转的 Tab 只有 2 条;修法一律是从权威源派生而非手工对齐两个字面量,使矛盾在结构上无法复现,新增 22 例回归锁并逐条注入验证;② 顺带修掉求职进度页标题「求职中 {stats.interviewing}」压在 4 行列表上、切到「面试中」Tab 只剩 1 行的同屏自相矛盾;③ 5 处rgba(255,255,255,x)按实际角色(不是同一个 token)收敛为 4 个语义 token,值等于现状、一像素未改;④ 补上 lint 盲区 ——no-hardcoded-hex-color此前拦不住rgba()/hsl(),已扩到函数式颜色并加模板字符串访问器,注rgba(1,2,3,0.5)验证确实报错;⑤ 企业端看板内联的.toFixed(1)k与霍兰德参与人数的toLocaleString()下沉到src/utils。本批次两处如实记录:一次python静默失败导致「注入其实没发生」,以及一条测试自己写错了的断言(把「累计面试邀请」与「当前面试中」当成同一个事实),均已订正并写进文档**) 上上版:2026-09-20(**修掉一类「同一个事实被写了两份、两处取值不同」的缺陷**:① 引导采集的求职意向——期望薪资 / 求职状态 / 期望城市三组选项,引导页与「设置求职意向」页各有一份词表,**三组全部匹配不上**,走完引导进来的用户看到三组 chip 一个都没选中,像资料丢了;更严重的是在那里选「暂不考虑」保存后,简历**仍把他记成积极求职中**(判定常量与页面词表不是同一个字符串)。三组选项已下沉到src/constants单一来源,新增withCurrentOption处理 P7 阶梯能拼出、快捷档覆盖不到的薪资组合,**真机实测四组 chip 全部正确选中**;② 顺带查出四处数据自相矛盾:当前用户 Offer 数(我的页 3 / 求职进度页 2)、求职进度每月 Offer 之和(3 /offerCount为 2)、企业端在招岗位(总览写死 1280 / 岗位管理算出 8)、企业端评价数(1280 / 28 / 128 三个数)、企业名(全称 / 简称两个值),均已收敛为单一来源并实测;③ **撤回 4.10 第 7 条关于合规页脚的错误结论** —— 4.10 断言「常量无消费者、缺页脚组件」,实际该组件早一天就已存在并挂在两页上,页脚一直渲染。见 4.14**) 上上上版: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) 范围:项目健康审查 + 已开发 / 待开发清单 结论先行:**当前项目可运行**(tsc0 错误、eslint0 error、路由 0 失效跳转、假交互 0 处、图标名 0 处无效、**365 例测试全绿**),无需修正阻塞错误。读这份文档请先注意一条:4.8 曾写「死控件清零」并据此把 P2 标记为已完成,那个结论当时是错的 —— 审计脚本只查「无处理函数 / 空处理函数」,查不到「有处理函数但点下去只弹一句『功能开发中』」。详见 4.9 首条。凡是本文件里由某个审计脚本背书的「✅」,请连同该脚本的检测口径一起读。
一、健康审查结论
| 检查项 | 命令 / 方式 | 结果 |
|---|---|---|
| 类型检查 | npx tsc --noEmit |
✅ 0 错误(Exit 0) |
| 代码规范 | npx eslint . |
✅ 0 error / 0 warning(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) ⚠️ 另有 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 |
✅ 25 个文件 339 例全绿(2026-09-20) |
| 入口接线 | 读 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 · compare(306) · rank(753) · offer-compare(676) · weight-settings(285) | 大 | 已开发 |
| 沟通 | /chat /chat/[id] |
chat(556) · id | 大 | 已开发 |
| 成长 | /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 既定任务(来自项目规范,需逐项核实)
- 企业端三项(历史记录为待办,代码审查显示已基本完成):
- Tab 选中色统一回
primary—— ✅ 代码已用colors.primary - 我的页补【设置】入口 —— ✅
enterprise-overview.tsx第 281 行已router.push('/settings') - 统一快捷入口话术 —— ✅ 已完成(2026-09-10):
MOCK_ENTERPRISE_QUICK_ACTIONS与沟通页QUICK_ACTIONS中 6 处 label 已对齐规范词(人才库→人才推荐、评价管理→岗位评价管理、沟通→企业沟通、面试日程/面试安排→面试评分),与「我的」菜单、/interview-score路由名一致。 - 沟通页快捷操作接线 —— ✅ 已完成(2026-09-10):
QuickActions死入口已绑定onPress;批量发邀请→/talent-recommendation、面试评分→/interview-score(根级自包含页、可返回)走跳转,批量发Offer / 群发通知无对应企业端页、按项目约定弹Alert '功能开发中'。tsc/eslint 0 问题。
- Tab 选中色统一回
- 微信登录:⚠️ 入口已于 2026-09-15 按产品决定摘除(丁总:前端不做 QQ / 微信登录)。三分支(
new/active/deleted)状态机、P2W/B1页面、src/lib/wechat-sdk.ts、相关 hook / 类型 / 接口全部保留未删,仅P1Login/P2PhoneCode不再挂入口;app/register.tsx的P2W/B1case 亦保留。接入路线图仍见微信登录接入方案.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不落库)+ APIsaveOnboardingProfile/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:拨号;与隐私政策的【待确认】客服电话同源,属同一批待补信息) 更正(2026-09-20):4.10 第 7 条曾据一次漏看的 grep 断言「没有任何代码读这两个常量、 缺的是页脚组件本身」。该断言是错的 ——SupportFooter.tsx于 2026-09-17(00522d5)已存在, 早于该断言一天,且已挂在 P1Login / P2QuickLogin 上。「填入即自动出现」这句是成立的。 见 4.14 第 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 test97 例全绿;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」这一事实不变;②已修(2026-09-15,见 4.4):简历域 26 处死控件全部接线,/resume仍有 10 处Pressable无onPresscheck-affordances.mjs简历域命中 0;③P21 采集的头像落地后无消费方已修:「我的」页头部改为显示用户真正选的头像 ——UserProfile新增avatarKey(存原始值:预设素材 key 或本地图片 URI,API 层只透传,解析在组件层isAvatarPreset(),因为src/types不可 importcomponents/),选过就渲染真实头像,没选过才回落到姓名首字。注意:拍照 / 相册得到的是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-doctor20/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。 - 测试:新增
resume-override.test.ts(26 例:覆盖层语义、清空不回落到引导、编辑引导派生条目不改丢、同毫秒 id 不撞号、技能 trim 去重、存储损坏回落、AI 三接口调用前后简历深相等)、resume-export.test.ts(20 例:空板块不产出、隐藏联系方式、HTML 转义、模板 id 白名单、自荐信无素材不生成、纸型页宽、文件名清洗)。npm test155 例全绿;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.tsx4 处(加入会议 / 导航需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,上架前必做:owner: 'REPLACE_WITH_EAS_OWNER'—— 需替换为你的 Expo 账号名;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.ts7385 行),未切换真实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.ts127 个export async function全部在函数体内留了真实请求,分两式(约定写在该文件顶部):- 纯 mock 返回的函数 →
// TODO: 真实请求(后端就绪后替换)+ 注释的const res = await fetch(...)/return res.json(); - 以本地存储为唯一真相源的函数(引导资料、简历编辑覆盖层、导出设置、薪资记录等)→ 单行
// TODO: 后端就绪后改为 await fetch(...);当前以本地存储为唯一真相源—— 放开时要把本地写换成请求,不是加一行。 - AI 能力(简历三处):参数名带
_前缀 = 尚未接入;去掉下划线 + 放开注释 = 该能力上线。
- 纯 mock 返回的函数 →
- 本轮补齐:原先 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 处逐项落地(口径仍是实现功能,不是删按钮;确实做不到的就删入口或写明原因):
- 企业沟通
app/(enterprise)/enterprise-chat.tsx:待处理任务卡(待回复 / 待评分)此前无onPress→ 现「待回复」切到未回复 Tab、「待评分」跳/interview-score,去向由接口层PendingTask.tabKey/route下发;搜索框接真实过滤;Tab 计数与列表同源 ——buildCommunicationTabs/buildPendingTasks均由会话列表推导(此前页面写死的 12/3/2/1/4 与 3 条示例数据对不上,会出现「Tab 上写 3、列表 0 条」)。 - 人才推荐
app/(enterprise)/talent-recommendation.tsx:顶部「搜索 + 漏斗」两枚图标 —— 漏斗无对应页面 → 摘掉;搜索改成真关键词过滤(姓名 / 职位 / 技能标签 / 学校)。「调整筛选条件」原为弹「功能开发中」→ 改为真复位(切回全部 + 清关键词)。Tab 计数同样由候选人列表推导(matchTalentTab:匹配度 ≥90 / 985-211 院校名单 / 经验 ≥5 年 / 硕士及以上)。「查看人才市场报告」仍无对应页面 → 保留入口 + 明确提示,不假装跳转。 - 申诉举证
app/appeal.tsx:原为空onPress→ 接真实选图(新增src/lib/imagePicker.ts+useImagePick),可拍照 / 相册 / 移除缩略图,证据随submitAppeal一并提交。PDF 类材料后端上传接口不存在,在页面上写明,而不是给个点不动的按钮。 - 候选人详情
app/candidate-detail.tsx:「发起沟通」→/chat/[id](此前点不动)。 - 面试评分
app/interview-score.tsx:「保存草稿」接真实本地暂存(键youzhi:interview:score-draft:<id>,按面试 id 分开不串台),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。 - 入职准备清单
app/onboarding.tsx:勾选状态落库(此前退出再进入勾选全丢,completed恒为 3/18);新增自定义待办分组(只有这一组可删,模板条目删了下次进页面还会出现,接口层直接 reject);「导出 / 分享」由buildChecklistText生成纯文本经系统分享面板发出(shareText)。 - 微信验证码页
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,不入库。
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.ts22 例 —— 状态往返 / 计数随下架变化并在重新上架后复原 / 只改目标岗位 / 存储损坏回落 / 存储失败 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→ 2026-09-18 结案:结论是「不可达」而非「未验证」 ——app/settings.tsx缓存「读取失败」态:缓存体积不走mockRequest(实测注入全局失败后仍正常读出381KB),需文件系统级故障才能触发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.tsbarrel:规格偏差属实(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,而页面的处理函数是这样的:
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. 另一处「文档与代码不符」 (结论有误,2026-09-20 撤回)
撤回说明(4.14 第 7 条):本条原断言「
SUPPORT_HOTLINE/SUPERVISION_PHONE全仓 grep 无任何 消费者,缺的是页脚组件本身」。该断言经复核为错误:components/onboarding/ui/SupportFooter.tsx于 2026-09-17(00522d5)已创建 —— 早于本条一天 —— 它读这两个常量、号码非空才渲染、支持 点击拨号,并已挂在P1Login.tsx:67与P2QuickLogin.tsx:130。号码已填,故页脚是渲染出来的。 原「文档与代码不符」的其实是本条自己:一次没查全的 grep 得出的结论。 以下保留原文,仅作错误留档,不要按它去「补页脚」(页脚已存在,那样会做出第二个)。
文档第 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 用注入空数据实测通过。已于 4.13 结案 —— 结论是「不可达」而非「未验证」。至此 4.9 第 8 条全部收口,仅剩一项本机无法验证的外部依赖项(生产包settings缓存读取失败态youzhi://深链,见 4.12 第 6 条)。
4.11 企业端状态文案缺陷 + 状态映射下沉(2026-09-18 批次)
0. 缺陷:草稿说自己是「重新上架」
4.9 第 4 条做的写通道,回模拟器复看时发现第一张卡片(标着草稿)的按钮是「重新上架」。溯源到 app/(enterprise)/job-management.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 里 |
根因(一条命令直接证死):
$ 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 错误也拦不住 —— 这是本节最值得记的一点。页面写的是:
<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 处无效图标名。
4.14 求职意向词表统一 + 四处数据自相矛盾(2026-09-20 批次)
1. 起点:一次「找同类缺陷」的排查,不是一次功能开发
4.13 把「数据层引用外部词表」的三类(图标名 / 路由 / 颜色)排查完了。本轮换了个方向问: 这个项目反复吃亏的那类缺陷,还有哪些没被扫过? 已发生过的先例是「同一个业务事实被写了两份, 且两处取值不同」——霍兰德参与人数(124,580 / 12,846)、运营主体公司名、客服电话。 沿这条线扫全仓,找到两组问题,均已修复并实测。
2. 缺陷一:引导采集的求职意向,在「设置求职意向」页一个 chip 都选不中
这是本轮最严重的一条,且每个走完引导的用户都会撞上。
resolveJobPreference()(api.ts)在用户未显式保存过偏好时,走 deriveJobPreference(profile)
从引导资料推导;而 app/job-preference.tsx 的 chip 用 opt === value 严格相等判断选中。
问题在于两处各有一份选项词表,且三组都匹配不上:
| 字段 | 引导产出(api.ts) |
该页选项(改前) | 后果 |
|---|---|---|---|
| 期望薪资 | 10K-20K(SALARY_MIN_OPTIONS 阶梯带 K) |
10-20K(最低值不带 K) |
无 chip 选中 |
| 求职状态 | 离职-随时到岗 等 4 项(JOB_STATUS_OPTIONS) |
积极求职中 等 3 项,零重合 |
无 chip 选中 |
| 期望城市 | P5 的 16 城 | 该页 10 城 | 天津 / 重庆 / 青岛 / 长沙 / 郑州 / 东莞 6 城无选中 |
用户刚在引导里填完,进来看到「期望职位」(纯文本输入)有值、其余三组全空 —— 像是资料丢了。
并且这不只是显示问题。 简历求职意向的积极 / 不积极判定是:
status: preference.status === JOB_STATUS_INACTIVE ? 'inactive' : 'active'
而 JOB_STATUS_INACTIVE 是引导词表的 '在职-暂不考虑'。用户在求职意向页选它自己的
「在职暂不考虑」(没有那两个短横)并保存后,覆盖值写进 user:job-preference,
与判定常量不是同一个字符串,比较恒为 false —— 简历仍把他记成积极求职中。
3. 修法:三组选项表下沉到 src/constants,两页共用一份
按 AGENTS.md「同一个事实只有一份来源」处理,不是把两处数字调成一样:
| 新增常量 | 说明 |
|---|---|
EXPECTED_CITY_OPTIONS |
期望城市 16 城;P5City 与求职意向页共用(原两处各一份) |
HOT_CITY_OPTIONS |
P5 置顶的热门城市 |
EXPECTED_SALARY_OPTIONS |
快捷档,写法与引导 P7 一致(最低K-最高K) |
WORK_TYPE_OPTIONS |
工作类型;顺带由测试锁住「PREFERENCE_DEFAULT_WORK_TYPE 必须取自本列表」 |
薪资那一组有个绕不开的点:P7 是两个独立阶梯(最低 / 最高各 9 档),能拼出快捷档覆盖不到的
组合(如 8K-12K、10K-15K)。所以不可能靠「把档位写全」解决。新增纯函数
withCurrentOption(options, current)(src/utils):当前值不在列表里时插在首位(通常是「不限」)
之后并选中,保证存了什么就显示什么。空串(未设置)不补 —— 「未设置」不该变成一个可选项。
CITIES(8 城)没有合并进来:它是岗位筛选用的列表,城市得有 mock 岗位与社保基数才有意义,
与「用户想去的城市」不是一回事。已在注释里写明两者的区别,避免后来者顺手合并。
4. 实测(Android 模拟器 emulator-5554,非代码走查)
该机恰有一份引导资料,构成了完美的复现样本:期望职位「运维工程师」、期望城市「北京」、 期望薪资「10K-15K」(不在快捷档里)、求职状态「离职-随时到岗」(旧词表完全没有这一项)。
「我的」页 → 求职偏好 → 编辑,改前四组中三组会全空;改后实测:
| 组 | 实测结果 |
|---|---|
| 期望城市 | 16 城全部渲染,「北京」选中(改前无选中) |
| 期望薪资 | 快捷档已是 5K-10K / 10K-20K / 20K-30K / 30K-50K 写法;用户的 10K-15K 被补在「不限」之后并选中 —— 这条同时证明了 withCurrentOption 在真机上生效,而不是只过了单测 |
| 工作类型 | 「全职」选中 |
| 求职状态 | 词表已换成 离职-随时到岗 / 在职-月内到岗 / 在职-考虑机会 / 在职-暂不考虑,「离职-随时到岗」选中(改前无选中) |
5. 缺陷二:四处「同一个事实两个数」
| 事实 | 改前 | 改后 | 依据 |
|---|---|---|---|
| 当前用户的 Offer 数 | 我的页 3;求职进度页 2 | 2 | MOCK_JOB_PROGRESS 内部即有 4 处指向 2(offerCount / stats.offer / offers 数组实为 2 条 / 时间轴「已收获 2 个」),3 是孤例 |
| 求职进度「每月 Offer 数」之和 | 3(5/7/8 月各 1) | 2 | 与同对象的 offerCount 自相矛盾,等于把本页结论自己推翻一半 |
| 企业端「在招岗位」 | 总览写死 1280;岗位管理由列表算出 8 | 两处均 8 | buildJobManagementStats 的注释本就写着「不能是写死的 12」;改由 countActiveJobs() 一处计算,两边共用 |
| 企业端「评价数」 | 总览「评价人数」1280、同屏四宫格「累计评价」28、岗位评价管理 128 | 三处均 136 | 由 MOCK_JOB_EVALUATION_SUMMARIES 的 reviewCount 汇总(42+38+25+31)—— 明细才是单一来源,三个总数都是派生值 |
| 企业名 | 总览「北京智联科技有限公司」;其余 3 页「智联科技」 | 均 智联科技(ENTERPRISE_DISPLAY_NAME) |
数据层没有「全称 / 简称」字段,这两个是同一事实的两个值,不是刻意的简称设计 |
企业名的 6 处字面量此前写在页面里(3 个企业端页面的加载 / 错误态 Header,因为那时拿不到数据),
属「页面写死业务数据」。现统一读常量,全仓该字符串只剩 1 处。
实测确认:企业总览页「评价人数 136 / 累计评价 136 / 在招岗位 8」,同屏不再自相矛盾; 岗位管理页「招聘中(8)」与总览页的 8 一致,企业名两页均为「智联科技」。
6. 回归锁:15 例测试 + 两处灵敏度验证
新增 src/__tests__/job-preference-options.test.ts(15 例),分四组:
- 可表征性:P7 两个阶梯的任意组合(9×9)拼出的字符串,经
withCurrentOption后都能被选中; 引导的四个求职状态、16 个城市逐个能选中;PREFERENCE_DEFAULT_WORK_TYPE必须在WORK_TYPE_OPTIONS内。 - 端到端:写入一份引导资料(城市「天津」、薪资
10K/20K、状态「在职-月内到岗」—— 三项都是改前会落空的取值),fetchJobPreference()的产物逐项可选中。 - 简历判定:在求职意向页选
JOB_STATUS_INACTIVE并保存,fetchResume().jobIntention.status必须是inactive;选其余三项必须是active。这条锁的就是上面那个「选了暂不考虑仍被记成积极求职」的缺口。 - 接线锁:直接读源码,断言
app/job-preference.tsx从 constants 取四组选项、且不再声明 本地选项数组;P5City.tsx同理。
第 4 组是必要的:前三组锁的是「常量之间自洽」,而原缺陷的形态是页面自己又写了一份 —— 常量没毛病、页面不读它,照样对不上。故补一条源码级断言锁住接线。 老实标出它的边界:这是源码级断言,不是渲染断言(本项目 25 个测试文件均为纯逻辑测试, 未引入组件测试)。它能拦住「改回本地字面量」,拦不住「读了常量后又做一层转换」。
灵敏度验证(只跑通不算数,会失败的断言才是断言):临时注入两处缺陷 ——
① 把 EXPECTED_SALARY_OPTIONS 的 10K-20K 改回 10-20K;② 把求职意向页改回本地
选项数组 + 使用本地 CITIES。重跑:2 例如期失败(「常见组合直接命中快捷档」与「接线锁」),
其余 13 例仍通过。随后 cp 还原并 cmp 逐字节比对确认与注入前一致,grep 无残留。
7. 更正 4.10 第 7 条的一处反向错误:页脚一直是存在的
4.10 第 7 条与「五、P0(补充)」①都写着:
SUPPORT_HOTLINE/SUPERVISION_PHONE…全仓 grep 无任何消费者 … 缺的是页脚组件本身
这个结论是错的,且是朝「把已完成的当成未完成」的方向错。 实际:
components/onboarding/ui/SupportFooter.tsx 在 2026-09-17(00522d5)就已存在 ——
比 4.10 写下这句话(09-18)早一天。该组件读这两个常量、号码非空才渲染、支持点击拨号,
并已挂在 P1Login.tsx:67 与 P2QuickLogin.tsx:130 两处。号码已填,故页脚是渲染出来的。
也就是说:「填入即自动出现」这句话成立,4.10 那条 ⚠️ 建立在一次漏看的 grep 上。 本批次已把这两处描述改回如实。
这类错误和本文件反复强调的那类(把没验的当成验了)是同一枚硬币的两面: 一次没查全的 grep,和一次没做的验证一样,都会让文档失真。
8. 本批次门槛复跑(2026-09-20 实测)
tsc --noEmit 0 错误 · eslint . 0 error / 0 warning · npm test 24 个文件 317 例全绿
(较 4.13 的 302 例 +15) · check-routes 0 失效跳转 · check-affordances 0 处 ·
check:icons 0 处无效图标名 · check-backend-stubs 140 个接口函数。
9. 本批次仍未做
CITIES(8 城)与EXPECTED_CITY_OPTIONS(16 城)仍是两份,见第 3 条 —— 这是有意的,不是漏改。若产品要求两处一致,需先给这 6 个城市补 mock 岗位与社保基数。→ (2026-09-20 已做一半,见 4.15)formatViews与薪资拼接未下沉job-management.tsx内联的job.views >= 1000 ? ...k : job.views已下沉为formatCompact(刻意保持2.0k带.0的现状口径,与formatViews的「万」并存,理由是两种场景的 可读性取舍不同,不是漏统一);app/holland.tsx的toLocaleString()也换成了formatThousands, 与成长页同源(Hermes 无完整 ICU 时toLocaleString()可能不加千分位,两页会显示成两个样子)。 仍未做:职位详情与列表页各自拼{salaryMin}-{salaryMax}K,无formatSalaryRange。 改动面涉及薪资在 4 个页面的展示口径,需产品确认是否需要统一。列出以待下轮,不当作已完成。app/resume.tsx:1086的rgba(0,0,0,0.4):src/theme/colors.ts已有同用途的colors.scrim(10+ 处已正确使用),此处是重复造值。另 5 处(2026-09-20 更正,见 4.15) 「5 处 = 缺一个 token」这个判断下得太浅:逐处看下来它们 不是同一个 token 被写重 5 次,而是 4 个不同的角色(进度条轨道 / 卡片底 / 分割线 / 弱底色), 收敛成 1 个会把这几个原本有区别的用法抹平。已按角色拆成 4 个 token。rgba(255,255,255,x)属缺失 token。eslint 的(2026-09-20 更正) 这条成立,且确实是漏洞,已在 4.15 补上并做注入验证。no-hardcoded-hex-color规则只匹配纯 hex,拦不住rgba(。- 4.10 第 10 条列出的外部依赖项(资质证照真实内容 / EAS 占位符 / 隐私政策【待确认】)均无变化。
4.15 数据自相矛盾批量收敛(第二组)+ 半透明色 token 化 + lint 补盲区(2026-09-20 批次,接 4.14)
1. 起点:4.14 结案后,把「找同类缺陷」继续做完
4.14 只覆盖了排查到的一半。把剩下的一半做完后,又查出 10 组「同一个事实两个数」, 其中多数是跨页面 / 跨层级的(列表卡与详情页、我的页与求职进度页、统计卡与它正下方的列表), 比 4.14 那批更隐蔽 —— 因为它们不在同一屏上,肉眼翻页时很难对照。
2. 修法:一律从权威源派生,不做两个字面量的手工对齐
这 10 组如果只是「把 A 改成和 B 一样」,下次改数据照样分叉。故修法统一为: 让重复的那一份在接口层由权威源算出来,使矛盾在结构上无法复现。
| # | 事实 | 改前两处取值 | 权威源与派生方式 |
|---|---|---|---|
| 1 | 求职进度投递总数 | stats.total 28 / 时间轴文案「向 28 家」/ deliveryCount 28,但 stats 五项之和 = 6+2+2+20 = 30 |
ended 由其余四项相减得出,求和关系成为结构性的 |
| 2 | 月度图表三列 | 投递合计 52、面试合计 16,卡片写 28 / 6 | 六个月的三个数重排为 2+3+4+5+6+8=28 / 0+1+1+1+1+2=6 / 0+0+0+0+0+2=2 |
| 3 | 我的页收藏数 | 徽标 12 / 收藏列表 3 | favorites: MOCK_FAVORITES.length |
| 4 | 我的页勋章 | 「投递满100次」显示已获得,而实际只投了 28 次 | obtained/progress 由 buildUserStats() 派生 |
| 5 | 匹配成功横幅 | 横幅写 3 家 / 会话列表 matched: true 只有 2 条 |
total 由 MOCK_CONVERSATIONS.filter(matched) 派生 |
| 6 | 岗位详情综合分 | 恒 92 分·优秀(void id;),9 个岗位里只有第 1 个对得上 |
jobScoreById(id),雷达图八维按该岗位实际分等比缩放 |
| 7 | 对比页「综合评分」 | 产品经理 85 / UI 设计师 78,与列表里的 78 / 85 正好互换 | 「综合评分」这一维改由 jobScoreById 取 |
| 8 | 候选人详情身份 | 4–8 号候选人点进去全叫「张明远」 | candidateIdentityOf(id) 取推荐列表里的那一条 |
| 9 | 岗位 1 的公司所在地 | 列表卡 海淀区 / 详情页 朝阳区 | 撤掉详情页对该字段的覆盖,列表成为唯一来源 |
| 10 | 待回复卡片 | 卡片写 3 人,但它点下去切到的「未回复」Tab 只有 2 条 | 改用 status === 'pending',与去向同源 |
第 10 项值得单独说:原先统计的是「有未读消息的会话数」,而它跳转的目标 Tab 是 「未回复」。两个集合本来就不同,于是点进去条数凭空变少。判据应是「与去向一致」, 不是「某个内部集合的长度」 —— 这条也写进了测试注释。
3. 顺带修掉一处同屏矛盾:求职进度页的标题
app/progress.tsx 的段落标题原为 求职中 {stats.interviewing},而它正下方是按 Tab 过滤的
companies —— mock 里只有 4 条(28 条投递的抽样)。于是标题写 2、下面列 4 行;
切到「面试中」Tab 只剩 1 行,卡片仍写 2;切到「已结束」Tab,1 行列表压在「求职中 2」下面。
「面试中几家」这个事实当时有三份副本(统计卡 / 标题 / Tab 列表)。已去掉标题这一份, 标题只说明「这是哪一段的列表」,数字以统计卡为唯一来源。
未解决、如实记录:
companies是 28 条投递的抽样(4 条),所以统计卡上的 「面试中 2 / 待反馈 6 / 已结束 18」在 Tab 列表里永远对不上行数。这是 mock 数据 全量 vs 抽样的结构问题,不是可以靠对齐两个字面量解决的。两条路(把companies扩成 28 条并让stats由它派生 / 明确标注为「近期流程」抽样并去掉会误导的统计数字) 都会改变用户看到的页面,需产品定夺,本批次未动。这不是推断,是逐个 Tab 截图看到的(见第 8 条实测):统计卡「待反馈 6」→ 点进 Tab 1 行; 「面试中 2」→ 1 行;「已结束 18」→ 1 行。三处全部对不上。
4. 半透明色:5 处 rgba(255,255,255,x) 收敛为 4 个 token(值不变)
src/theme/colors.ts 新增,值等于现状,一像素未改:
| token | 值 | 角色 |
|---|---|---|
onBrandSurface |
rgba(255,255,255,0.2) |
品牌底上的卡片 / 徽标底 |
onBrandSurfaceWeak |
rgba(255,255,255,0.15) |
品牌底上的更弱底色 |
onBrandLine |
rgba(255,255,255,0.2) |
品牌底上的分割线 |
onBrandTrack |
rgba(255,255,255,0.35) |
品牌底上的进度条轨道 |
另 app/resume.tsx:1086 的 rgba(0,0,0,0.4) 改用已有的 colors.scrim(10+ 处已正确使用)。
5. lint 补盲区:no-hardcoded-hex-color 此前拦不住 rgba()
4.14 就记下了这个漏洞(「规则只匹配纯 hex」),本批次补上:
eslint.config.js 的函数式颜色匹配扩到 rgb|rgba|hsl|hsla,并加了一个 TemplateElement 访问器
—— 否则模板字符串里的颜色( background: `${x})照样漏网。
注入验证(会失败才算数):临时把 rgba(1, 2, 3, 0.5) 写进 app/holland.tsx,eslint . 报
173:31 error 禁止写死颜色字面量 "rgba(1, 2, 3, 0.5)",请从 src/theme/colors.ts 导入颜色 token local/no-hardcoded-hex-color
EXIT=1,规则确实生效;随后还原,grep -c __probe = 0,eslint . EXIT=0。
一次险些得出错误结论的过程,如实记录:第一次注入用
python -c做字符串替换, 脚本静默失败(python是 Windows 商店的占位存根,非交互调用时什么都不做、也不报错), 文件其实没被改动。若没先grep __probe确认注入物存在,就会拿一次「未注入」的运行去证明 「规则有效」—— 和 4.14 第 7 条那个漏看的 grep 是同一类错误。注入之后必须先确认注入物在文件里。
6. 回归锁:22 例测试 + 三处注入验证
新增 src/__tests__/data-consistency.test.ts(22 例)。写法上刻意把断言写成
「A 必须等于 B」的关系而非「A 等于 28」:这样以后调整 mock 数据不会让测试变红,
只有把派生关系改回写字面量才会红 —— 锁的正是要防的那件事。
灵敏度验证:一次性注入三个缺陷,确认命中的断言各不相同 ——
① favorites 改回字面量 12 → 收藏数 1 例红;
② fetchJobOverallScore 改回恒定 92 → 评分 2 例红;
③ candidateIdentityOf 改为恒取第 0 条 → 身份 2 例红。
合计 5 例如期失败,其余 17 例仍通过;逐字还原后 grep -c __probe = 0。
另一处「测试自己写错了」的如实记录(比测试通过更有价值):本文件初版把
interviewCount(累计收到 6 场面试邀请,与月度图表 interviews 之和、时间轴「收到 6 场面试邀请」同源)
和 stats.interviewing(当前面试中 2 家)当成了「同一份被写重的值」,断言两者相等并因此变红。
红的是测试,不是数据。 已在文件里保留一条反向断言把它钉死
(interviewCount >= interviewing 且 not.toBe),并在注释里写明这个坑 —— 否则下一个人会再犯一次。
7. 工具函数下沉(与 4.14 第 9 条同一类别)
formatCompact(n):企业端看板内联的n >= 1000 ? (n/1000).toFixed(1)+'k' : n下沉到src/utils。 刻意保留.0(2000 →2.0k),与formatViews去掉.0的做法不一致且是有意的 —— 改口径会动到看板现在的显示,属另一批事。formatThousands:app/holland.tsx的toLocaleString()换成它,与成长页同源。
8. 模拟器实测结果(Android 模拟器 emulator-5554,非代码走查)
用 Expo Go 的深链约定 exp://127.0.0.1:8081/--/progress 直接路由到目标页,逐屏截图核对:
| 页面 | 实测看到 | 说明 |
|---|---|---|
| 求职进度 · 统计卡 | 28 / 6 / 2 / 2 / 18 | 6+2+2+18 = 28,求和关系成立(改前是 6+2+2+20 = 30 ≠ 28) |
| 求职进度 · Offer 提醒 | 「你有 2 个 Offer 即将到期」 | 与统计卡 Offer 2 一致 |
| 求职进度 · 月度图表 | 6月 5/1/0 · 7月 6/1/0 · 8月 8/2/2 | 三列合计 28 / 6 / 2,与顶部计数一致 |
| 求职进度 · 段落标题 | 「求职中」/「待反馈」/「面试中」/「已结束」 | 标题随 Tab 变化,不再出现与列表矛盾的数字 |
| 我的 · 统计行 | 投递 28 / 收藏 3 / 沟通 8 / Offer 2 | 收藏 12 → 3(与收藏列表同源);投递、Offer 与求职进度页一致 |
| 我的 · 勋章 | 求职达人 28/100、面试高手 6/10、诚信之星 95/100 | 改前「投递满100次」显示「已获得」,而实际只有 28 次投递 |
其中「诚信之星 95/100」是一条同屏可自查的交叉验证:同页上方「八大维度评分 → 诚信记录」 显示的就是 95 —— 勋章的分与维度的分是同一处来源,肉眼即可对照。
深链注意:
exp://…/--/<path>会触发 App 整包 reload(不是热跳转),每换一页约需 20 秒; 这与 4.12 第 6 条查明的结论一致 —— 项目自定义 scheme 未注册,只有 Expo Go 的exp://生效。没验到的:企业端「候选人详情」(第 2 表第 8 项:4–8 号不再叫「张明远」)、岗位详情评分 (第 6 项)、对比页评分互换(第 7 项)、收藏列表条数(第 3 项的列表侧)均有测试锁盖住, 但本轮未逐屏截图。按本文件一贯口径记为未做真机验证,不当成已验证。
9. 本批次门槛复跑(2026-09-20 实测)
tsc --noEmit 0 错误 · eslint . 0 error / 0 warning · npm test
25 个文件 339 例全绿(较 4.14 的 317 例 +22) · check-routes 0 失效跳转(12 个「孤立页」为
已知误报,见本文件开头说明) · check-affordances 0 处 · check:icons 0 处无效图标名
(扫描 194 个文件 / 1357 个有效字形) · check-backend-stubs 140 个接口函数。
10. 本批次仍未做
companies全量 vs 抽样(见第 3 条),需产品定夺(已由第 8 条实测证实:三个 Tab 全部对不上)。- 薪资区间拼接未下沉
formatSalaryRange(见 4.14 第 9 条,涉及 4 个页面的展示口径)。 chats: 8(我的页沟通数)与勋章 6「完成10次测评」的4/10仍是字面量 —— 全仓找不到可派生的计数源(没有「测评次数」计数器)。不编造来源,故保留字面量并在此标注; 它们目前不与任何其它数字冲突,但一旦有了计数源就应改回派生。- 4.14 第 9 条列出的
CITIES/EXPECTED_CITY_OPTIONS两份词表,状态不变(有意保留)。 - 4.10 第 10 条的外部依赖项(资质证照真实内容 / EAS 占位符 / 隐私政策【待确认】)均无变化。
4.16 重复声明批量收敛(同一张表 / 同一段规则在多个页面各写一份)(2026-09-20 批次,接 4.15)
1. 起点:4.14 / 4.15 治的是「取值不同」,本批治的是「写法重复」
前两批处理的形态是同一个事实两处取值不同(数字对不上、词表匹配不上)。 本批是它的另一半:取值当时相同,但同一张映射表被抄了多份。 这类缺陷平时看不出来,代价有两个:
- 改一处漏一处:产品说「良好」换个蓝,页面里躺着五份同值映射,改哪份全凭记性;
- 测试锁不住:
jobLevelTextColors有测试,但页面不读它,测试锁的是没人渲染的那份。
本批不改任何取值(改前后逐值对照过),是纯重构。判据也从「两个数是否相等」 换成「这段定义是否只有一处」,故新增的锁以源码级接线锁为主。
2. 修法表:14 处本地声明 → 6 张单一来源的表
| # | 事实 | 原先几份 / 在哪 | 现在 |
|---|---|---|---|
| 1 | 评级 → 文字色 | 5 份:growth 的 LEVEL_TEXT_COLOR、jobs 的 LEVEL_COLOR、ability 与 assessment 各一份同名 LEVEL_TEXT_COLOR、hr-evaluation 的中文键版 |
theme 的 jobLevelTextColors(+ 派生的 levelLabelTextColors) |
| 2 | 霍兰德六型 → 强调色 | 3 份:holland.tsx / holland/question.tsx / holland-result.tsx 各一份 TYPE_COLORS |
theme 的 hollandTypeColors |
| 3 | 语义色名 → 色值 | 3 份:enterprise-overview / interview-score / interview 各一份 COLOR_TOKENS |
theme 的 semanticColorTokens |
| 4 | 文章缩略图 底色 / 图标色 | 2 种写法:成长页一个 {violet:{bg,color}} 对象表;详情页拆成 THUMBNAIL_BG + THUMBNAIL_COLOR 两份平行 Record |
theme 的 articleThumbnailColors |
| 5 | 社保基数 clamp 到城市上下限 | 3 份:api.ts:computeSalaryResult(权威)、计算器页 clampSocialBase、计算器页 handleSelectCity 内联 |
接口层 clampSocialBase(city, value),两处调用 |
| 6 | 时间戳 → 「月-日 时:分」 | 2 份:utils.formatDateTime(收 ISO 串)与计算器页 formatRecordTime(收时间戳) |
formatDateTime(value: string | number) |
| 7 | 手机号脱敏 | 2 份:api.maskPhone 与登录页逐字节相同的内联表达式 |
页面调用 maskPhone |
| 8 | 学历 / 工作类型选项 | 发布页自持两份:EDUCATION_OPTIONS(与 constants 逐字相同)、WORK_TYPE_LABELS(同一组中文标签的另一种形状) |
均取自 src/constants |
放在哪个文件是按职责定的,不是就近塞:
- 颜色映射进
src/theme/colors.ts—— 该文件自己就写着「色值是本文件独占的职责」, 且已有templateAccentColors这个先例; - clamp 进
src/api/api.ts—— AGENTS.md 规定业务规则在接口层,而权威的那一份本来就在那里; - 时间格式化进
src/utils—— 日期格式化是 utils 的既有职责(该文件已有formatDateTime); - 选项词表进
src/constants—— 4.14 定的单一来源。
3. 一处必须派生而不是并列复写:levelLabelTextColors
hr-evaluation 那份的键是中文评级('优秀'),不是 JobLevel。
根因在数据层:DimensionScore.level 是 JobLevel 联合类型,而 EvaluationDimension.level
声明为 string 且直接下发中文评级。所以这不是「同一张表抄两遍」,而是同一个事实、
两个键域。若只是把字面量搬个地方,等于把「两份同值」换成「两份同值、只是搬了家」。
故写成按引用派生:
export const levelLabelTextColors: Record<string, string> = {
优秀: jobLevelTextColors.excellent,
良好: jobLevelTextColors.good,
中等: jobLevelTextColors.medium,
};
将来「良好」换色只改 jobLevelTextColors 一处,这张表自动跟随 —— 两张表各自写着同一个色值
才是分叉的起点。回归锁断言的是关系:「每一档与它同键的那档同值」,
而不是「优秀等于 #175AB0」。
4. clamp 提取时的一个坑:city 必须变成显式参数
三段 clamp 看着一模一样,直接「简化成调用 helper」会静默算错城市:
// 页面上那个 helper 读的是 inputs.city —— 也就是**旧城市**
const clampSocialBase = (v: number) => { /* 用 inputs.city 的上下限 */ };
// 而选城市时它必须用**新城市**的上下限,于是页面只好把同一段 Math.min/Math.max 内联重写
const handleSelectCity = (city: string) => { /* 用 city 的上下限,另写一遍 */ };
把 city 提成显式参数后,两处调用都说得清「按哪个城市」:
export function clampSocialBase(city: string, value: number): number {
const profile = SOCIAL_SECURITY_CITY_PROFILES[city] ?? DEFAULT_SOCIAL_PROFILE;
return Math.min(profile.socialBaseUpper, Math.max(profile.socialBaseLower, value));
}
这不是「顺手美化」,而是重复的成因:正因为 helper 拿不到目标城市,才被迫复制一份。 回归锁用一个前提校验保证不空转 —— 先断言深圳上限 > 北京上限(否则这例证明不了什么), 再断言「在深圳拉到上限后切北京,结果必须落到北京上限」。
5. 一条未被统一的配色分歧(需产品定夺,未擅自改)
app/(tabs)/profile.tsx 的 DIMENSION_LEVEL 里,good.text = colors.textSecondary(灰);
而上面第 1 项那五处对「良好」一律用 colors.primary(蓝)。
两处对同一个档位取不同的色,是一个真实存在的分歧,但本批没有动它:
- 它键的是
DimensionScore['level'](只有两档),与岗位评级的三档表不是同一张表; - 「哪个对」是产品判断(个人页的维度条要不要跟岗位标签同一个蓝),不是重构能决定的;
- 后果有限:两处不在同一屏,用户不会同屏看到矛盾(与 4.15 那些「同屏两个数」不同)。
若产品确认应统一,改法是让 DIMENSION_LEVEL 的 good.text 取
jobLevelTextColors.good,一行即可。在此之前不要擅自统一。
6. 顺带查出的一条死值:JobExperienceRequirement 的「1年以内」
'1年以内' 全仓只出现在类型声明里一次:mock 岗位数据一次都没用到它,
岗位筛选的 EXPERIENCE_OPTIONS(constants)里也没有它 —— 即这个取值
既筛不到、也没有样本,只有发布页能选出来。
本批刻意不合并两张经验表,因为它们不是同一个事实,名字像而已:
| 表 | 用途 | 含「不限」? | 含「1年以内」? |
|---|---|---|---|
constants.EXPERIENCE_OPTIONS |
岗位筛选项 | ✅(表示不加限制) | ❌ |
job-publish 的本地 EXPERIENCE_OPTIONS |
企业发布岗位的必填要求 | ❌(必须明确要求) | ✅(与联合类型逐项对应) |
强行合并会让发布页多出一个不该有的「不限」选项。已加反向锁把这个差异写死, 防止后来者按名字想当然地合并。至于「1年以内」这个死值该留还是该删,属数据取舍,需产品定夺。
7. 如实记录:一条被证伪的子代理结论
本轮开工前有一份子代理报告,列了若干「重复声明」。逐条核对的结果:
| 报告的说法 | 核验结果 |
|---|---|
DIMENSION_LEVEL 重复声明 |
不成立。全仓只有 profile.tsx 一处 |
THUMBNAIL_* 重复「×2」 |
描述不精确。不是两份同名表,是一个对象表 + 两份平行 Record(两种写法) |
clamp 在 :302-305 与 :294-296 |
行号不准。实际三份,第三份在 api.ts 里(报告没提,而那才是权威那份) |
这与本文件一贯口径一致:代理报告是模型输出,不是证据。
每一条都独立 grep + 逐行读过才动手,没有一条是照报告直接改的。
8. 回归锁与注入验证
新增 src/__tests__/shared-color-maps.test.ts(26 例)。断言分两类:
- 关系(「A 必须等于 B」):中文评级表与
jobLevelTextColors同键同值、 六型 / 语义色 / 缩略图各色互不相同(撞色会让雷达图六顶点或四宫格看不出区分)、 计算器的养老金额可由clampSocialBase反推(证明两处走同一条规则)、formatDateTime两种入参格式一致; - 接线锁(读源码):14 处旧声明名一律
not.toMatch,且页面必须toContain新表名 —— 值对了但页面不读表,缺陷照旧。
注入验证(本批 6 处,一次性注入、6 例如期全红、逐字还原):
| 注入 | 期望变红的锁 | 结果 |
|---|---|---|
ability.tsx 加回 const LEVEL_TEXT_COLOR |
「四个页面读 theme」 | ✅ 红 |
levelLabelTextColors['良好'] 改回字面量 |
「中文评级表由 jobLevelTextColors 派生」 | ✅ 红 |
api.ts 把 input.city 换成 '北京' |
「计算器与页面走同一条 clamp 规则」 | ✅ 红 |
计算器页内联 Math.min(cityProfile.socialBaseUpper, …) |
「计算器页面不再自己 clamp」 | ✅ 红 |
| 登录页恢复内联脱敏 | 「登录页调用 maskPhone」 | ✅ 红 |
发布页加 label: '全职' 字面量 |
「发布页取自 constants」 | ✅ 红 |
一处如实记录:本文件里那条 clamp 的接线锁第一版是写错的 —— 断言写成
「页面不得出现 socialBaseUpper」,而页面本来就该读它(用于展示
「基数范围 ¥6,821 – ¥33,891」这行提示)。断言本身错了,不是代码错了;
已改为精确锁「上下限字段与 Math.min/Math.max 同现」(那才是旧版内联 clamp 的形状)。
9. 真机实测(Android 模拟器 emulator-5554,截图核对,非代码走查)
本批取值未变,故真机的价值不在「颜色对不对」,而在没崩、没落到兜底:
| 页面 | 核对内容 | 结果 |
|---|---|---|
| 成长中心 | 能力等级色 + 三个缩略图色系全部解析(紫 / 橙 / 绿各一组) | ✅ 三者都渲染出各自的色,无 undefined |
| HR 评价详情 | 中文评级表:六维里「优秀」「良好」都有色 | ✅ 无一落到兜底灰 |
| 企业端总览 | 语义色名表:统计四宫格与快捷入口 | ✅ 五个色各不相同(蓝/紫/绿/红/橙)—— 若有键写错会静默全变蓝,此处多色并存即证明未落兜底 |
| 发布岗位 | 学历 5 项(含「不限」,「本科」选中)+ 经验 6 项(含「1年以内」,无「不限」)+ 工作类型 4 项(「全职」选中) | ✅ 与文档所述一致,值↔标签配对未被打乱 |
| 薪资格算器 | 切城市:北京 → 深圳,提示更新为「¥4,492 – ¥43,659(深圳)」,基数 25000 在区间内保持不变 | ✅ handleSelectCity 生效 |
没验到的(按一贯口径记为「未做真机验证」,不当成已验证): ① clamp 本身未在真机上制造出越界值 —— 社保基数输入框是逐键 clamp 的 (
onChange每次都过 clamp),手工输入根本无法停留在目标值上,属既有交互、非本批改动; 故 clamp 的语义以单测 + 注入为准(注入'北京'那例即为该语义的灵敏性证明)。 ② 岗位列表 / 能力 / 测评三页的评级色,霍兰德三页的六型色, 面试评分 / AI 面试两页的语义色,以及登录页的掩码手机号与薪资格算器历史记录的时间格式 —— 均有类型约束(键是字面量联合,写错即编译不过)或测试锁盖住,但本轮未逐屏截图。
10. 本批次门槛复跑(2026-09-20 实测)
tsc --noEmit 0 错误 · eslint . 0 error / 0 warning · npm test
26 个文件 365 例全绿(较 4.15 的 339 例 +26) · check-routes 0 失效跳转 ·
check-affordances 0 处 · check:icons 0 处无效图标名(194 文件 / 1357 字形) ·
check-backend-stubs 140 个接口函数。六处注入物已 grep 确认为 0 残留。
11. 本批次仍未做
- 第 5 条的「良好」配色分歧(需产品定夺)。
- 第 6 条的「1年以内」死值该留该删(需产品定夺)。
- 数据层
color: string未收紧成联合类型:semanticColorTokens的键因此只能保持Record<string, string>,代价是数据层写错色名会静默落到调用方的?? colors.primary。 根治要把 6 个类型字段(EnterpriseStat.color等)收紧成联合类型并同步全部 mock 数据, 属另一批。本批已在代码注释与测试里把「这是刻意的」写死,防止后来者顺手收紧又放开。 - 4.15 第 10 条列的各项(
companies全量 vs 抽样、formatSalaryRange未下沉、chats: 8与测评勋章仍是字面量、CITIES/EXPECTED_CITY_OPTIONS两份词表有意保留)状态不变。 - 早前批次提到的
WORK_TYPE_OPTIONS标签重复已在本批修掉(见第 2 表第 8 项)。
4.17 全站卡片圆角统一 8px + 去卡片阴影(2026-09-20 批次,接 4.16)
1. 起点:一条新增的硬规则,来自用户而非本轮排查
AGENTS.md 于 2026-09-20 14:12 新增一行(用户改动,本轮首次实现):
整个项目所有卡片的圆角都必须是8px,无阴影
同一次编辑还把提交规则从「每天 17:50 提交」改成「提交并推送到远端」。两处都写进本批。
为什么不能靠 grep 'borderRadius' 一把梭:项目从来没有「卡片」的样式键约定 ——
同一屏里 card / box / panel / section / wrapper 混用,cardBtn(胶囊按钮)、
cardTag(标签)、cardIcon(圆形图标底)也共用 card 前缀。先立判定口径,再动手,
否则「统一 8px」会连胶囊按钮和圆形头像一起压成方角,这是本批最大的一次性风险。
2. 判定口径(本轮定义,后续沿用)
| 判为「卡片」 | 判为「非卡片」 |
|---|---|
样式键为 card 或以 Card 结尾/内嵌(scoreCard、emptyCard、preambleCard) |
同名前缀的子件:CardBtn/CardBullet/CardDot/CardBadge/CardIcon/CardWrap/CardTag/CardChip |
逐条读过块体、确认是「独立承载一组内容的面板」的 17 个非 card 命名样式(emptyBox/emptyList/emptyHint/searchEmpty/pathSummary/section/cardBase/cardOffer/salaryBox/draftBanner/matchBox/aiTip) |
按钮、标签、chip、输入框、Tab、列表行(row/*Item)、头像与圆点 |
圆 / 胶囊排除规则:半径 × 2 ≥ min(字面量宽, 字面量高) 时判为圆或胶囊,不动。
命中并被正确跳过的实例:cardBtn 48 高 / 半径 24、radioOuter 18×18 / 半径 9、
countdownCard 60×60 / 半径 30、Holland 结果的类型序号圆标。
3. 改动规模(git diff 实测,非估计)
| 项 | 数量 |
|---|---|
| 圆角半径改为 8 | 147 处(分布于 50 个 .tsx/.ts) |
删除的阴影行(shadowColor/shadowOffset/shadowOpacity/shadowRadius/elevation) |
54 行,7 个文件 |
其中:删除模块级 const shadow = {…} 常量 + 其 ...shadow 展开点 |
app/jobs/[id].tsx 共 7 行 |
| 卡片类样式总数 / 其中仍非 8px | 167 / 0 |
改动前 167 个卡片类样式里,130 个由脚本改、37 个本来已是 8;
另有 17 个走人工复核的面板命名,130 + 17 = 147 与 git diff 行数逐一对上。
4. 两个审计脚本自身的缺陷(差点改错别人的圆角,如实记录)
审计脚本(临时工具,不入库)第一版把单行样式块当成普通块解析:
modalCard: { backgroundColor: colors.white, borderRadius: 16, padding: 20 },
这类样式在本行内就闭合了,脚本却继续往下读,把下一个样式块的值算成了它的值。
后果不是漏改,是可能取到邻居的半径然后改到邻居头上。发现的路径是抽样复核时看到
countdownCard 被报成「半径 30 / 60×60 的圆」(它实际是单行 borderRadius: 16,
那个 60×60 是隔壁 countdownItem 的),以及 P3Identity 的 card 被误判成
「100%×48 的胶囊」而整块漏掉 —— 修掉解析后它才被正确识别并改成 8。
第二个缺陷更隐蔽:shadowOffset: { width: 0, height: 2 } 里的 width/height
被当成元素尺寸,于是所有带阴影的卡片都满足「半径×2 ≥ 尺寸」而被判为胶囊、
整类静默跳过 —— 而带阴影的恰恰是最该改的卡片。修法是取尺寸前先剔掉 shadowOffset
与全部 shadow* 行。修复后重新审计,P3Identity 等一批真卡片才浮出来。
两个缺陷都属于「报错方式为静默漏判」而非崩溃,所以本批不采信任何一次性的脚本输出,
而是用 verify-radius.js 对 git diff -U0 的每一行新增半径反查它落在哪个样式块内
(结论:147 行中 130 行在 card 命名样式内、17 行在人工复核的面板白名单内、0 行在名单外)。
5. 一次被拒绝的批量回滚,以及随之改走的「前向验证」
第一版脚本在 app/onboarding.tsx:580 崩溃(单行块无独立 borderRadius 行),
此时已写入 79 处改动。我尝试用 git diff --name-only | grep -v AGENTS.md | xargs git checkout --
整体回滚重来,被权限策略拒绝(判定为对未提交工作的不可逆本地销毁)。
我没有绕过它,而是改走前向验证:不回滚,直接证明这 79 处(连后续的 68 处)全部合法。
事后看这个方向更对 —— 那次回滚还会连带抹掉用户自己刚写的 AGENTS.md 编辑,
而当时我并不知道那份编辑的存在(它不在我这条命令的视野里)。教训:把「回到干净状态」
当作修复手段之前,先确认干净状态里没有别人的东西。
6. 刻意不动的两处(范围边界,不是遗漏)
components/onboarding/ui/SliderCaptcha.tsx:294-297的滑块阴影保留: 它是控件不是卡片,去阴影会让滑块与轨道在视觉上糊成一体。全库elevation:/shadowOpacity:现仅此一处(已grep全库确认)。src/theme/colors.ts的shadow: '#000000'token 保留:colors.test.ts:11仍在断言它。 代价是「卡片不再用阴影」这条约定在 token 层看不出来,属已知的文档-代码缝隙,未加注释(避免 为一个零引用的 token 增加维护面)。
7. 覆盖边界:116 条「像卡片但不叫 card」的样式未动
以「白/浅底 + 内边距或描边 + 圆角 ≠ 8 + 非圆非胶囊」反查,全库仍有 116 条这样的样式 (口径见审计脚本注释)。逐类看过:几乎全是按钮、标签、chip、输入框、Tab 项、列表行、头像底 —— 它们不是卡片,压成 8 反而是错的。这不是「差不多改完了」的余数,是本轮口径的边界; 若将来真要把某一类纳入,需要单独一次「这一类算不算卡片」的判定,不能顺手改。
8. 真机实测(Android 模拟器 emulator-5554,截图核对,非代码走查)
| 页面 | 看什么 | 结果 |
|---|---|---|
jobs 岗位列表 |
卡片原本带阴影(searchBox 5 行) |
已平;8px 直角;仍能与 pageBg #F6F8F9 区分 |
jobs/1 岗位详情 |
3 张卡片 + 模块级 shadow 常量已删 |
已平;层级靠白底与描边区分,未塌成一片 |
(tabs)/profile 求职端 |
statsCard/enterpriseCard |
8px;语义色(蓝/橙/绿)未受影响 |
(enterprise)/enterprise-overview |
4 张卡片共删 15 行阴影(本批最多) | 已平;数据块仍可辨认 |
job-publish |
card 删 5 行阴影 |
已平;表单分区仍清晰 |
tools/skill-cloud |
4 张卡由 14 → 8 | 8px;注:行业 Tab 行存在既有的纵向裁切/重叠,与本批无关,未追 |
holland-result |
渐变横幅 + 3 张类型卡 | 8px;内部序号圆标与标签胶囊未误伤 |
resume/export |
预览卡 / 格式卡 / 模板卡 | 8px;模板缩略图内框圆角独立,未被压平 |
未实测:components/resume/EditKit.tsx、components/onboarding/ui/Modal.tsx、
components/onboarding/screens/P3Identity.tsx、LegalDocScreen 的圆角改动只有代码级验证
(它们嵌在弹层/引导流/长文档里,深链直达需先走完前置流程),未截图。
9. 本批次门槛复跑(2026-09-20 实测)
tsc --noEmit 0 错误 · eslint . 0 error / 0 warning · npm test
26 个文件 365 例全绿 · check-routes 0 失效跳转 · check-affordances 0 处 ·
check:icons 0 处无效图标名(194 文件 / 1357 字形) · check-backend-stubs 140 个接口函数。
10. 本批次仍未做
- 第 7 条的 116 条同名前缀样式是否要纳入「卡片」口径(需产品/设计定夺,不擅自扩大)。
- 4.16 第 5/6 条的两项产品定夺(「良好」取色分歧、
'1年以内'死值)状态不变。 - 4.15/4.16 列的其余开放项状态不变(
companies全量 vs 抽样、formatSalaryRange未下沉、chats: 8与测评勋章字面量、CITIES/EXPECTED_CITY_OPTIONS两份词表有意保留、 数据层color: string未收紧)。 - 4.16 第七节列出的 A–E 五条重复声明发现(skill-cloud 行业 Tab 6 选 3 有数据、
GROUP_LABEL≡SKILL_GROUP_LABEL、发岗表单选项表未上锁、「全部行业」同音异义、 jobs 页三个筛选 chip 落在假交互脚本盲区)本批未实现,已分析完,留待下一批。
五、后续增量开发建议(优先级)
- 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,且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→ 明确类型,eslint0 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 通道验证。 - P1(补充 · 2026-09-20 第三批,需产品定夺):① 「良好」这一档在个人页与其余五处取色不同 ——
app/(tabs)/profile.tsx的DIMENSION_LEVEL.good.text是textSecondary(灰), 而岗位评级那五处一律primary(蓝)。两处不同屏、后果有限,故未擅自统一; 若确认要统一,让DIMENSION_LEVEL.good.text取jobLevelTextColors.good即可,一行。详见 4.16 第 5 条。 ②JobExperienceRequirement的「1年以内」是死值 —— 全仓仅见于类型声明, mock 岗位数据没用过、筛选表也没这一项(既筛不到也无样本),只有发布页能选出来。 留还是删需产品定夺,详见 4.16 第 6 条。 - P1(补充 · 2026-09-20,需产品定夺):求职进度页
companies是「全量」还是「抽样」 —— mock 里只有 4 条,而统计卡写「投递 28 / 待反馈 6 / 面试中 2 / Offer 2 / 已结束 18」,于是任何 Tab 点进去行数都对不上卡片。这不是能靠对齐字面量解决的问题,两条路(① 把companies扩到 28 条、stats由它派生;② 明确标注为「近期流程」抽样、并撤掉会误导的统计数字)都会改变用户看到的页面,故本轮只去掉了标题里那份矛盾的副本(见 4.15 第 3 条),主体未动。不要在没有产品结论的情况下自行选一条。 - 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 张显示「配图待补 + 该放什么图」标注,头像 / 插画类降级为色块 + 图标(可用的占位态,非空白)。
六、附:审查命令速查
# 类型检查(硬门槛,必须 0 错误)
npx tsc --noEmit
# 代码规范
npx eslint .
# 路由闭合审计(改动路由后必跑)
node scripts/check-routes.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
# 依赖与 SDK 一致性(动过依赖后跑,1 项预存问题:yarn.lock 与 package-lock.json 并存)
npx expo-doctor
# 打包自检(能过说明新 import 可解析,不替代真机验证)
npx expo export --platform android --output-dir <临时目录>
# 启动
npm start # 即 expo start