# 职来(有职)前端 × 后端 联调问题清单(致后端同事) > 生成时间:2026-09-30 > 最近复测:2026-09-30(第二轮:权重设置域接通 + 覆盖率审计脚本修正) > 前端仓库:`mydreamjob`(React Native + Expo,包管理器 yarn) > 后端基址:`https://job.youthome.com.cn/api/v1` > 文档口径:以《职来后端接口.md》(248 个端点)为准。`scripts/openapi.json` 已过期(仅 220 个,缺 `/auth/identity/select` 等 28 个),后续请以 md 为准。 > 产生方式:① 覆盖审计(`scripts/audit_coverage.py`:md 端点 ↔ 前端 `request()` 双向比对);② 实网探测(`scripts/live_probe.py`:真实学生账号登录后批量 GET);③ 写入类端点实网试跑(本轮新增:PUT 变体逐个试,定位出接口文档的 body 示例错误)。 --- ## 0. 结论速览 | 指标 | 数值 | |---|---| | 后端端点总数 | 248 | | 前端已接入 | **85**(上轮 83) | | 前端未接入 | **163**(上轮 165) | | 前端「幽灵调用」(调了后端不存在的端点) | **0** | | 实网可探测 GET 端点 | 142(另有 42 个因缺路径 id 未探测) | | 实网探测 正常 / 异常 | 50 / 50(本轮复测与上轮一致,无新增异常) | | 后端已实现且有**真实数据**、但前端未接 | 25 个(见第 6.2 节,可排期接通) | 需后端配合的事项分四类:**① 契约确认(第 1 节)② 端点缺失需补齐(第 2 节)③ 异常端点需关注(第 3 节)④ 接口文档与实际不符(第 1.12 节)**;测试账号问题见第 4 节;前端已自行修掉的问题见第 5 节。 > **本轮最重要的一条**:`PUT /profile/me/review-weights` 的实际行为与接口文档**不一致**,照文档 body 写必失败(400 `code=1001`)。详见 §1.12,附可复现命令。 --- ## 1. 需要后端确认的接口契约 | # | 项目 | 前端现状(已按此实现) | 需后端确认 | |---|---|---|---| | 1 | **分页基准** | 按 **0-based** 传 `page`(Spring Data 默认),列表首页 `page=0` | 全站是否统一 0-based?若个别端点从 1 开始,请指出 | | 2 | **统一响应包** | 按 `{code,message,data,requestId,timestamp}`,`code=0` 成功(HTTP 200),失败读 `message` | 成功码是否恒为 0?`requestId` 是否用于排障回溯 | | 3 | **鉴权** | `Authorization: Bearer `;401 时用 `refreshToken` 静默换新(双 Token) | 刷新端点与失效口径;刷新失败是否也返回 401 | | 4 | **401 / 403 语义** | 401=未登录或 Token 失效;403=角色无权限 | 确认区分稳定(实网 403 返回 `code=2002`) | | 5 | **错误码清单** | 已观测:`2002`=forbidden、`6001`=profile not found | **请提供完整业务错误码表**(前端要按码做差异化提示,不能只靠文案) | | 6 | **角色模型** | `STUDENT / COMPANY / ADMIN`;`/companies/me/*`、`/company/*`、`/admin/*` 对 STUDENT 一律 403 | 企业端角色是否单一 COMPANY?有无子角色 | | 7 | **岗位状态枚举** | 前端把后端 `status` 映射为中文(如「招聘中」) | 请给 `status` 全量取值集合 | | 8 | **求职偏好字段缺口** | 后端下发多值数组 + 英文 token(`preferredJobTypes[]`、`employmentTypes:['FULL_TIME']`),前端取首项并译中文 | **后端未下发「期望薪资」「求职状态」**,前端只能显示「未设置」。是否补字段? | | 9 | **测评(assessment)** | 按「答卷(attempt)」模型桥接:一次抽 30 题 → 批量保存 → 提交出结果;保存/提交带乐观锁 `version`,且 `typeCode`+`version` 锁定同一套题 | 并发冲突返回什么码/结构?前端需据此提示「题目已更新,请重试」 | | 10 | **能力测评(ability)** | 用 `attemptQuestionId` + `selectedOptionId` 提交,**无 `version`** | 与测评(9)不一致是否有意?能力测评是否也需要乐观锁 | | 11 | **岗位详情口径** | 学生端公开详情走 `GET /jobs/public/{jobId}`;`GET /jobs/{jobId}` 是公司端看自己发布的岗位 | 确认:`/jobs/{jobId}` 对 STUDENT 返回 403 是**预期**(学生不该走这条) | | 12 | **权重保存写入模式** | 实测为**两种互斥模式**:套模板只传 `templateCode`、自定义只传 `weights`;**不能同时传** | ⚠️ 接口文档的 body 示例 `{templateCode, weights}` **是错的**,照抄必 400。详见 §1.12 | | 13 | **权重维度集合与命名** | 后端 8 个维度(`BENEFITS`/`WORK_LIFE_BALANCE`/`GROWTH`/`TEAM_ATMOSPHERE`/`PROMOTION`/`AUTHENTICITY`/`JD_CLARITY`/`SALARY_TRANSPARENCY`),**只下发英文 token,不下发中文名** | 前端已自建 token→中文名映射表兜底。**建议后端在权重/模板响应里补 `displayName`**,否则后端每加一个维度,前端都要发版才能显示中文 | | 14 | **权重来源枚举** | 实测三种:`SYSTEM_DEFAULT`(从未保存)/ `TEMPLATE`(套过模板)/ `CUSTOM`(自定义过) | 确认这三态是否稳定;`updatedAt` 是否只在保存过之后下发 | | 15 | **错误码补齐** | 本轮新观测:`2005` = 手机号或密码错误 | 并入 §1.5 的「完整错误码表」诉求 | --- ### 1.12 接口文档与实际不符:`PUT /profile/me/review-weights`(需后端修正文档或实现) **问题**:《职来后端接口.md》给该端点的 body 示例是 ```json { "templateCode": "string", "weights": { "property1": 0, "property2": 0 } } ``` 但实网(学生账号 `19900009021`,2026-09-30 实测)**照此提交必失败**。逐变体实测结果: | 提交的 body | 实网结果 | |---|---| | `{ "templateCode": "BALANCED" }` | ✅ 200,`source` 变 `TEMPLATE` | | `{ "weights": { …八个维度全给,总和 100… } }` | ✅ 200,`source` 变 `CUSTOM`,`templateCode` 消失 | | `{ "templateCode": "BALANCED", "weights": {…} }` ← **文档示例** | ❌ 400 `code=1001 bad request` | | `{ "configuredWeights": {…} }` | ❌ 400 `code=1001` | | `{ "weights": {…}, "source": "USER_CUSTOM", "templateCode": "CUSTOM" }` | ❌ 400 `code=1001` | | `{ "templateCode": "CUSTOM" }`(自定义模板码) | ❌ 400 `code=1001` | | `{ "templateCode": "NONEXIST" }` | ❌ 400 `code=1001` | | `{ "weights": {…总和 90…} }` | ❌ 400 `code=1001` | | `{ "weights": {…少一个维度…} }` | ❌ 400 `code=1001` | | `{ "weights": {…含未知 token UNKNOWN…} }` | ❌ 400 `code=1001` | | `{}` / `{ "weights": {} }` | ❌ 400 `code=1001` | **可复现命令**(替换 ``): ```bash # 1) 登录取 token curl -s -X POST https://job.youthome.com.cn/api/v1/auth/password/login \ -H 'Content-Type: application/json' \ -d '{"phone":"19900009021","password":"ZhilaiTest!2026#A"}' # 2) 文档示例(同时传两者)→ 400 code=1001 curl -s -X PUT https://job.youthome.com.cn/api/v1/profile/me/review-weights \ -H "Authorization: Bearer " -H 'Content-Type: application/json' \ -d '{"templateCode":"BALANCED","weights":{"BENEFITS":12.5,"WORK_LIFE_BALANCE":12.5,"GROWTH":12.5,"TEAM_ATMOSPHERE":12.5,"PROMOTION":12.5,"AUTHENTICITY":12.5,"JD_CLARITY":12.5,"SALARY_TRANSPARENCY":12.5}}' # 3) 只传 weights → 200 curl -s -X PUT https://job.youthome.com.cn/api/v1/profile/me/review-weights \ -H "Authorization: Bearer " -H 'Content-Type: application/json' \ -d '{"weights":{"BENEFITS":12.5,"WORK_LIFE_BALANCE":12.5,"GROWTH":12.5,"TEAM_ATMOSPHERE":12.5,"PROMOTION":12.5,"AUTHENTICITY":12.5,"JD_CLARITY":12.5,"SALARY_TRANSPARENCY":12.5}}' ``` **需要后端**(二选一): - **(推荐)修正文档**:把 body 示例改为「二选一」的说明,并注明互斥规则与 400 `code=1001`; - 或修正实现:允许同时传(此时以 `weights` 为准、`templateCode` 仅作来源标注)。 **另请补两点**:① 400 的 message 只有 `bad request`,无法定位是「总和不为 100」「缺维度」还是「两个字段同时传」——**建议按下述细分错误码**,前端要据此给不同的提示文案;② 权重总和是否必须**严格等于 100**(实测不等即 400),是否允许 99.9/100.1 这类浮点误差。 --- ## 2. 后端未实现 / 缺失的端点(实网 404,需补齐) 以下端点前端**已按接口文档实现调用**,但实网返回 `404 not found`,对应页面因此无真实数据源: | 端点 | 实网结果 | 受影响功能 | 建议 | |---|---|---|---| | `GET /salary-calculator/calculations`、`/cities`、`/cities/{cityCode}`、`/calculations/{id}`、`DELETE /calculations/{id}` | 404 | 薪资计算器工具页 | 优先补齐,属求职者高频工具 | | `GET /skills/word-cloud`、`/skills/match`、`/skills/learning-path` | 404 | 成长域「技能词云 / 岗位匹配 / 学习路径」 | 补齐或明确砍掉(前端好同步摘页面) | | `GET /onboarding/company/checklist-items`、`/onboarding/company/knowledge` | 404 | 企业端入职准备 | 补齐 | | `GET /account/deletion-request` | 404 | 账号注销申请查询(POST 同路径待验) | 确认注销链路:查询端点是否存在 | | `GET /admin/reviews/replies/pending`、`GET /company/review-reply/stats` | 404 | 评价回复审核 / 统计 | 确认是否已实现 | | `GET /profile/me/resume` | 404(`code=6001 profile not found`) | 简历页 | 学生**尚未创建简历**时建议返回 **200 + 空结构**,否则前端把「未填写」当「请求失败」 | | `GET /applications/me` | 200,但 items 只有 `{id, jobId, studentUserId, status, createdAt, updatedAt}` | 投递列表 / 求职进度页 | **缺岗位名、公司名、城市、薪资等展示字段**,前端只拿到 `jobId`。请 join 出岗位摘要;否则前端只能逐条反查岗位(N+1,列表 20 条即 20 个请求) | > 本轮复测(2026-09-30 第二轮):上表 404 组的**结论无变化**,均仍为 404。另 `GET /jobs/{jobId}/salary` 在接口文档中不存在(salary-calculator 域也 404)。早期前端误调该端点,已在本次修正中**移除**。若产品需要「按经验段拆分的真实薪资」,请后端设计并给出端点。 > 附:`/jobs/filters` 可正常返回(城市 10 个、薪资区间 2207–74568、雇佣类型 4 种),但**前端岗位筛选仍是本地硬编码**;若后端确认该端点稳定,前端下一轮改为下发制(避免两边枚举分叉)。 --- ## 3. 实网探测异常汇总(142 个 GET,50 个异常) ### 3.1 403 forbidden(角色权限,**预期**,但请复核清单) `/admin/**` 全部(scoring / reviews / reports / companies / candidate-scoring)、 `/companies/me/**` 全部(jobs / talent-invitations / match-rule / onboarding / verification / job-reviews)、 `/company/**` 全部(applications / candidates / scoring)、 `/jobs/{jobId}`、`/jobs/{jobId}/competition-ranking`、`/offers/statistics`。 > 学生账号访问自然 403,属预期。**唯一需要确认的是 `/jobs/{jobId}`**:学生端应走 `/jobs/public/{jobId}`,请确认这条 403 是设计如此。 ### 3.2 400 missing parameter(**前端需带参**,请确认参数名与必填性) | 端点 | 缺失参数 | |---|---| | `GET /jobs/compare` | `ids` | | `GET /offers/compare` | `ids` | | `GET /company/applications` | `jobId` | | `GET /admin/scoring/fairness/overview` | `period` | | `GET /admin/scoring/fairness/dimensions` | `period` | | `GET /admin/scoring/risk/trend` | `bucket` | | `GET /admin/companies/verifications/history` | `verificationId` | ### 3.3 404 not found(未实现) 见第 2 节。 --- ## 4. 测试账号 | 账号 | 密码 | 实网结果 | |---|---|---| | `19900009021`(学生) | `ZhilaiTest!2026#A` | ✅ 可用,登录返回 `role=STUDENT, userId=4` | | `19900009801`(企业) | 文档给的密码 | ❌ 401 `code=2005 手机号或密码错误` | | `19900009802`(企业) | 文档给的密码 | ❌ 401 `code=2005 手机号或密码错误` | > 本轮(2026-09-30 第二轮)对两个企业号复测了 4 组常见口令变体(`ZhilaiTest!2026#A` / `…#B` / `Test!2026#A` / `Zhilai2026!`),**全部返回 401 `code=2005`**。两点请求:① 确认这两个手机号是否**真的已注册为企业账号**(也可能只是没注册,那 2005 属于正确返回,但请给一组真实可用的);② `code=2005` 同时用于「手机号不存在」与「密码错误」,前端无法区分,建议拆分或至少让文案可辨别。 **需后端提供一组可用的企业端测试账号(含正确口令)**,否则企业端 `/companies/me/**`、`/company/**` 无法联调(学生 token 一律 403)。同时请提供一组 ADMIN 测试账号用于后台域联调。 --- ## 5. 前端已自行修复(无需后端改动) | 问题 | 处理 | |---|---| | 分页基准错误:此前传 `page=1`,会**静默丢掉第 1 页**数据 | 改为 0-based `page=0`,并封装 `toBackendPage/toFrontendPage` | | 岗位 DTO 字段名不一致(`title`/`company.name`/薪资单位「元」) | 新增映射:`title→positionName`、`company.name→companyName`、薪资「元→K」、`status→中文` | | 求职偏好结构不一致(后端多值数组+英文 token ↔ 前端单值中文) | API 层双向映射(取首项 / token↔label),并新增 `EMPLOYMENT_TYPE_LABEL`/`EMPLOYMENT_TYPE_TOKEN` | | 岗位评分调了不存在的 `GET /jobs/{id}/score` | 改打真实端点 `GET /jobs/{id}/personalized-review-score`,新增 `mapPersonalizedScore`(后端 `dimensionScores` 对象 → 雷达图维度数组) | | 岗位详情调了不存在的 `GET /jobs/{id}/salary`,且它在 `Promise.all` 中 → 404 会让**整个详情页加载失败** | 移除该调用与「真实薪资区间」表;薪资仍由岗位对象的 `salaryMin/salaryMax` 展示 | | 测评 / 能力测评的单题推进 UI 与后端「答卷模型」不同构 | Hook/API 层桥接(抽题→答题→提交→取果),沉淀 `mapAttemptToAssessment` 等 | | 后端错误响应体无 `data` 字段,导致真实 message(如 `sms rate limited`、`unsupported sms purpose`)被覆盖成笼统的「请求参数有误」 | 请求层新增 `extractError()`:错误分支不再要求 `data` 存在。**影响所有非 2xx**,不止验证码 | | 覆盖率审计脚本用正则 `<[^>]*>` 解析 `request()` 泛型,遇到嵌套泛型(如 `request<{items: Omit[]}>` 中第一个 `>` 就收尾)会**静默漏检**,覆盖率被系统性低估 | 改为平衡尖括号解析(忽略 `=>`)。修正后已接入 83 → **85**,未接入 165 → **163** | | 评分权重设置页(`app/jobs/weight-settings.tsx` + `useWeightSettings`)此前全走 mock,维度是前端自造的「九大中文维度」 | 本轮接通真实后端:并行拉 `GET /profile/me/review-weights` + `GET /review-weight-templates`,维度集合改为**以后端下发为准**(8 个英文 token → 自建中文名映射,未登记 token 原样兜底显示);保存按 §1.12 的互斥双模式择一提交 | > **⚠️ 关于 mock 开关的事实更正(前一版文档此处描述有误)**: > `app.config.js` 的 `extra.apiUseMock = true` 时,`isMockMode()` 为真,**api.ts 中 71 处 `if (isMockMode())` 的业务函数一律返回 mock**——包括已与后端对齐的投递、岗位、测评等域。也就是说「已接通的域走真实后端」**并不自动成立**,必须把 `apiUseMock` 置为 `false` 才会真实触网。 > 唯一的例外是 `src/api/auth.ts`(鉴权域已于 2026-09-30 整体硬切真实后端,不再读该开关)。 > **因此 App 内联调的正确姿势是:`apiUseMock: false`**。当前为 `true`(保 App 全程可跑、不回归),此时 App 里看到的岗位/投递/权重数据**都是 mock**,不代表后端返回了假数据;真实链路的验证目前走 `scripts/live_probe.py` 与 curl 直连。 > 未接通的「幽灵域」函数已按约定写成「真实模式下也不触网、直接返回 mock」,故翻为 `false` 不会引发大面积报错;但仍建议翻开关后跑一轮全页面冒烟。 --- ## 6. 覆盖率明细与后续排期 **未接入 163 个端点,按域分布(2026-09-30 第二轮,审计脚本修正后):** | 域 | 未接入数 | 性质 | |---|---|---| | admin | 34 | 前端未建后台页(后端应已实现) | | profile | 28 | 候选人评分/数据导出/推荐/隐私/匹配阈值等 | | companies | 20 | 企业端未接 | | offers | 12 | 前端未接(Offer 域) | | company | 10 | 企业端候选人/评分 | | onboarding | 9 | 含 2 个后端 404 | | interviews | 9 | 面试评分域,前端未接 | | jobs | 7 | 含比较/竞争排名等 | | salary-calculator | 6 | 后端 404 | | reviews | 4 | 评价回复域 | | blocks / reports / applications / skills | 3 × 4 | 屏蔽 / 举报 / 投递剩余 / 后端 404 | | account / health / info / 等 | 6 | 工具类端点 | ## 6.1 前端后续接入优先级 - **P0**:求职者核心闭环 —— `applications`(投递剩余)、`interviews`(面试)、`offers`(Offer) - **P1**:企业端 `companies/**`、`company/**`、`reviews` 回复、`blocks`、`reports` - **P2**:`admin/**`(后台)、`profile` 剩余(候选人评分、数据导出、推荐、匹配阈值) ## 6.2 后端已实现且有真实数据、前端尚未接通的端点(本轮盘点,可直接排期) > 这 25 个端点实网返回 **200 且带真实数据**,是「接通即见效」的部分,不需要等后端做任何事。 > 数据来源:`scripts/live_probe.py` 实网结果 × 覆盖率审计未接入清单 求交集。 | 端点 | 实网返回要点 | 建议 | |---|---|---| | `GET /profile/me/job-recommendations` | 推荐岗位列表,含 `title/city/employmentType/salaryMin/Max/mode/reasons` | **P1** 求职者侧可直接出「为你推荐」 | | `GET /profile/me/review-weights` ✅本轮已接 | 8 维度权重 + `source/templateCode` | 已完成 | | `GET /review-weight-templates` ✅本轮已接 | 4 个模板(BALANCED/SALARY_FIRST/GROWTH_FIRST/STABILITY_FIRST) | 已完成 | | `PUT /profile/me/review-weights` ✅本轮已接 | 见 §1.12 互斥双模式 | 已完成 | | `GET /jobs/{jobId}/match` | `matchScore / eligible / matchedConditions / explanation / gaps`(**含「未设置城市偏好」这类可执行诊断**) | **P1** 「双向透明」核心:岗位详情页展示「为什么匹配 / 差在哪」 | | `GET /jobs/filters` | 城市 10 个、薪资区间 2207–74568、雇佣类型 4 种 | **P1** 替换前端硬编码筛选枚举 | | `GET /profile/me/privacy` + `PUT` | `resumeVisibility / contactVisible / version` | **P1** 简历可见性与联系方式开关(合规相关) | | `GET /profile/me/match-rule` + `PUT` | `requiredHrScore`(默认 60) | **P1** 学生侧双向匹配阈值 | | `GET /profile/me/talent-discovery` + `PUT` | `enabled / version`(默认 false) | **P2** 「让企业发现我」开关 | | `GET /profile/me/candidate-scoring` | `overallScore` + 8 维 `weights` | **P2** 候选人评分卡 | | `GET /profile/me/candidate-scoring/trend` | 月度趋势(含 `rulesVersion`) | **P2** | | `GET /profile/me/candidate-percentile` | 百分位(当前 `score/percentile` 为 null,`cohortSize` 有值) | 需后端确认「样本不足」时的口径 | | `GET /profile/me/candidate-score-card` | 评分卡(`overallScore` 为 null) | 同上 | | `GET /job-recommendation-taxonomies` | 行业 / 职能分类树(`code/type/displayName/parentCode`) | **P2** 推荐偏好选择器 | | `GET /profile/me/deletion` + `POST` | `{userId, status, version}` | **P2** 注销状态查询(注意:`/account/deletion-request` 是 404 的另一套路径,见 §2) | **实网 200 但返回空、需先造数据才能联调的端点**(不阻塞接口本身): `GET /offers`、`GET /blocks/me`、`GET /reports/me`、`GET /profile/me/talent-invitations`、`GET /profile/me/data-export-tasks`、`GET /profile/me/candidate-score-trend`、`GET /profile/me/candidate-scoring/appeals`。 → 其中 **`/interviews/me`、`/offers`、`/conversations` 返回空数组**,加上企业账号 401,是面试/Offer 域目前**无法实网验证字段**的根因(见 §4)。请后端为学生测试号造 1–2 条面试与 Offer 数据,或提供有数据的学生账号。 --- ## 7. 待后端回复的清单(速览) **本轮新增(P0,优先)** 1. **§1.12 `PUT /profile/me/review-weights` 的 body 契约**:接口文档示例是错的,同时传 `templateCode` + `weights` 必 400 `code=1001`。请确认「二选一」为准,并修正文档;同时请把 `bad request` 细分为可辨识的错误码(总和≠100 / 缺维度 / 未知 token / 两字段同时传)。 2. **§1.13 权重维度补 `displayName`**:后端只下发英文 token(8 个维度),前端不得不自建映射表——后端每新增维度都要前端发版。建议在 `review-weights` 与 `review-weight-templates` 响应里带上中文名。 3. **§2 `GET /applications/me` 缺展示字段**:items 只有 jobId,没有岗位名/公司名/城市/薪资,投递列表无法直接渲染,请 join 出岗位摘要。 4. **§4 企业账号仍全部 401(`code=2005`)**:试了 4 组口令变体均失败。请确认这两个号是否真已注册,并提供一组可用的企业 / ADMIN 账号;另请为学生测试号造 1–2 条**面试**与 **Offer** 数据(`/interviews/me`、`/offers` 现为空数组,字段无法验证)。 **沿用上轮** 5. 第 1 节 11 项契约逐条确认(尤其:分页基准、**完整错误码表**、测评 version 冲突码、岗位 status 枚举、偏好缺「期望薪资/求职状态」)。 6. 第 2 节 404 端点:是「未实现」还是「路径变更」?给出排期或确认砍掉(尤其 `salary-calculator/**`、`skills/**`、`onboarding/company/**`)。 7. 第 3.2 节 400 端点:确认必填参数名(`ids` / `jobId` / `period` / `bucket` / `verificationId`)。 8. `GET /jobs/{jobId}` 对学生的 403 是否为预期(学生端是否只应走 `/jobs/public/{jobId}`)。 9. `/profile/me/privacy` 的 `version` 字段语义(乐观锁?前端 PUT 是否需要回传?当前前端未回传也能成功,请确认)。 10. `candidate-percentile` / `candidate-score-card` 的 `score`/`percentile`/`overallScore` 当前为 `null`:是「样本不足」的正常态,还是数据未产出?(前端需据此决定展示「暂无数据」还是继续 loading) 11. 是否需要后端补「岗位分经验段薪资」端点(前端已移除误调的 `/jobs/{jobId}/salary`)。