# 职来后端前端联调说明(RC) ## 1. API Base URL | 环境 | Base URL | |------|----------| | 本地开发 | `http://127.0.0.1:8080/api/v1` | | 生产(示例) | `https://api.mydreamjob.example.com/api/v1` | 所有接口统一前缀 `/api/v1`,OpenAPI 文档:`GET /api/v1/v3/api-docs`(Swagger UI:`/api/v1/swagger-ui/index.html`)。 ## 2. 后端启动(Windows) 先启动基础设施容器: ```powershell cd backend-infra docker compose up -d ``` 然后设置环境变量并启动: ```powershell cd mydreamjob-server $env:DB_URL = "jdbc:mysql://127.0.0.1:3307/mydreamjob?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai" $env:DB_USERNAME = "mydreamjob" $env:DB_PASSWORD = "<你的数据库密码>" $env:RABBITMQ_USERNAME = "mydreamjob_dev" $env:RABBITMQ_PASSWORD = "<你的 RabbitMQ 密码>" # 注意:本机 Windows 环境变量 RABBITMQ_VHOST 可能被设置为无关路径,必须显式覆盖为默认 vhost $env:RABBITMQ_VHOST = "/" $env:JWT_SECRET = "<至少256位的随机密钥>" $env:SMS_MOCK_ENABLED = "true" mvn spring-boot:run -Dspring-boot.run.profiles=dev ``` 健康检查:`http://127.0.0.1:8080/api/v1/health` 返回 `status: UP`。 ## 3. 本地获取测试验证码(不通过日志) **应用日志不会输出验证码**,也不得从控制台 / stdout / 日志文件中读取验证码。 本地 dev/test 联调请使用受控方式取得验证码: 1. 启动应用时显式开启 Mock 短信:`SMS_MOCK_ENABLED=true`(即 `app.sms.mock.enabled=true`) 2. 显式授权本次执行读取验证码表:`ALLOW_TEST_OTP_DB_READ=true` 3. 通过受控测试脚本或数据库内部只读查询取得验证码;脚本内部取码,不向终端展示 `docs/smoke-test.sh` 已按上述双重保护实现;两项条件任一不满足即输出 `test OTP retrieval is disabled` 并安全失败。 **约束:** - 生产环境**禁止**查询验证码表辅助登录 - 当前尚未接入真实短信供应商;`app.sms.mock.enabled` 为 `false`、缺失、空值或非法值时,发码返回“服务不可用”,绝不返回虚假成功 - 验证码当前仍以**明文**存储于 `sms_verification_codes`,属已知遗留风险(未加密、未哈希) ## 4. Apifox 导入 OpenAPI 1. 打开 Apifox → 新建项目 2. 左侧「导入数据」→ 选择「OpenAPI/Swagger」 3. 上传 [docs/openapi-rc.json](../docs/openapi-rc.json)(或填 URL `http://127.0.0.1:8080/api/v1/v3/api-docs`) 4. 点击「确认导入」,Apifox 自动生成全部接口、参数、响应模型 5. 在 Apifox 中可直接调试:登录接口获取 token 后,设置「环境变量 `token`」,在接口「Authorization」填 `Bearer {{token}}` ## 5. Token 使用方式(S02) 登录成功返回 `accessToken`(有效期 24h)与 `refreshToken`(有效期 7 天)。 ```json { "code": 200, "data": { "userId": 2, "accessToken": "eyJhbGciOi...", "refreshToken": "eyJhbGciOi...", "expiresIn": 86400, "role": "STUDENT" } } ``` **后续请求**:所有需要认证的接口在请求头携带 ``` Authorization: Bearer ``` ### 5.1 两个 Token 的用途必须分开 | Token | 用在哪 | |---|---| | `accessToken` | 所有普通业务接口的 `Authorization` 头 | | `refreshToken` | **只能**用于 `POST /api/v1/auth/refresh` | 把 `refreshToken` 放进普通接口的 `Authorization` 头会被拒绝(401); 把 `accessToken` 传给刷新接口同样会被拒绝(401)。两者由不同密钥签发,不可互换。 ### 5.2 刷新与登出 ``` POST /api/v1/auth/refresh body: { "refreshToken": "" } POST /api/v1/auth/logout header: Authorization: Bearer (无 body) ``` ### 5.3 前端必须遵守的规则 1. **上线即强制重新登录**:S02 之后所有旧 Token(不含类型与会话信息)全部失效, 前端需要接受一次强制登录,不要尝试兼容旧 Token。 2. 普通请求**只携带 Access Token**。 3. 收到普通接口 401 时,**最多刷新一次**;刷新成功后原子替换两个 Token 并重放原请求。 4. **刷新请求必须串行**:多个并发 401 不能同时用同一个 Refresh Token 去刷新 (并发刷新会被服务端判定为重放并撤销整个会话)。 5. **刷新失败一律清除本地两个 Token 并跳转登录页**,不要循环刷新。 6. **登出时先调用 `/auth/logout`,随后无条件清除本地 Token**; 不要依赖第二次登出请求(Access Token 失效后它本来就会 401)。 7. **不要记录或上报 Token**:不写入 URL、本地日志、埋点或错误上报正文。 ## 3. 请求头 | Header | 必需 | 说明 | |--------|------|------| | `Authorization` | 认证接口 | `Bearer ` | | `Content-Type` | POST/PUT/PATCH | `application/json` | | `X-Request-Id` | 可选 | 全链路追踪 ID,审计日志会记录 | | `User-Agent` | 可选 | 客户端标识,审计日志会记录 | **注意**:`X-Request-Id` 和 `User-Agent` 会被写入 audit_logs,联调时建议带自定义值便于排查。 ## 4. 分页格式 列表接口统一使用 `page`(从 0 起)和 `size` 参数,返回 `PageResponse` 结构: ```json { "code": 200, "data": { "items": [ ... ], "total": 42, "page": 0, "size": 20, "totalPages": 3 } } ``` | 接口 | 默认 size | 最大 size | |------|-----------|-----------| | GET /jobs | 20 | 50 | | GET /applications/me | 20 | 100 | | GET /company/applications | 20 | 100 | | GET /jobs/{id}/reviews | 20 | 50 | | GET /conversations/{id}/messages | 20 | 50 | | GET /notifications | 20 | 50 | ## 5. 统一响应与错误码 所有接口返回统一 `ApiResponse`: ```json { "code": 200, "message": "ok", "data": {...}, "requestId": "...", "timestamp": "..." } ``` | HTTP | 业务 code | 含义 | |------|-----------|------| | 401 | 401(安全过滤器) | 未登录 / 无 token | | 403 | 2002, 8004, 8303, 8402 | 无角色权限 / 越权(含企业归属不匹配) | | 400 | 1001, 3002-3006, 8404-8408, 8505, 9105 | 参数错误 / 非法请求体 / 资源未命中默认映射 | | 404 | 4001, 5001, 6001, 7001, 8002, 9102, 9104, 8305, 8501 | 资源不存在 | | 409 | 5003, 7003, 8001, 8003, 8301-8302, 8304, 8502-8504, 9101, 9103 | 重复 / 非法状态流转 | | 500 | 1002, 9001 | 服务器内部错误 | **前端策略**:优先判断 HTTP 状态码,再读取 `body.code` 显示业务信息。 ## 6. 测试账号 | 账号 | 角色 | 说明 | |------|------|------| | `13800000002` | STUDENT | 学生,可投递/匹配/查看通知 | | `13800000003` | COMPANY | 企业,可创建岗位/查看投递/更新状态 | | `13800000001` | ADMIN | 管理员(预留) | **登录方式**(开发环境 Mock): 1. `POST /auth/sms/send?phone=<手机号>` 发送验证码 2. 验证码**不再输出到日志**;本地按 §3 的受控方式取得(生产禁止查表) 3. `POST /auth/login` 携带 `{phone, code, purpose:"login"}` 获取 token ## 7. 典型联调流程 ``` 学生端: 发码 → 登录 → GET /jobs → GET /jobs/{id}/match → POST /applications → GET /notifications → POST /notifications/{id}/read 企业端: 发码 → 登录 → POST /companies/me → POST /jobs → PATCH /jobs/{id}/status → GET /company/applications?jobId= → PATCH /applications/{id}/status ``` ## 8. 投递接口与评价/对比补充 ### 8.1 投递接口(正确写法) 创建投递为 **`POST /applications`**,请求体 `{"jobId": 123}`(不要写成 `POST /jobs/{jobId}/applications`): ```json { "jobId": 123 } ``` ### 8.2 岗位八维评价 `POST /jobs/{jobId}/reviews` 可附加可选 `dimensionScores`(八个固定维度,值 1-5,可稀疏提交): ```json { "rating": 4, "content": "岗位信息真实", "tags": ["团队氛围"], "dimensionScores": { "AUTHENTICITY": 5, "TEAM_ATMOSPHERE": 4 } } ``` - 不传 `dimensionScores` 时旧接口完全兼容; - 评价列表/详情返回 `dimensionScores`; - 汇总 `GET /jobs/{jobId}/reviews/summary` 返回 `dimensionSummary`(`INSUFFICIENT` sampleCount<3 / `SUFFICIENT` >=3); - 详情见 `docs/job-review-api.md`。 ### 8.3 岗位对比(公开) `GET /jobs/compare?ids=1,2,3`(无需登录,2-3 个公开岗位),返回公开字段与公开评分。 ## 9. Candidate Scoring(求职者评分体系)联调 默认关闭时规则版本固定 `rulesVersion = "candidate-scoring-v0.1"`;八维 `EDUCATION / INTERNSHIP / ABILITY / COMPLIANCE / STABILITY / GROWTH / INTEGRITY / ACTIVITY`,默认权重各 12.50,参与计算维度重新归一化。 > V38 联调(功能开关 `scoring.ability-assessment.enabled`,默认 `false`):开关开启后规则版本为 `"candidate-scoring-v0.2"`,`ABILITY` 可由最新有效四维能力测评(WORKPLACE_ABILITY、COMPLETED、答题 24/24、`valid_until > capturedNow`)映射为 `OK` 并参与综合分。学生 `ABILITY` 单维明细的 `subScores[]` 为四条白名单聚合分(逻辑能力/35、执行能力/30、学习能力/20、沟通能力/15),不含任何答案/答卷/来源结果 ID;企业评分卡对 `ABILITY` 仍仅暴露聚合 `score/status`。后端本地验证已通过,真实 MySQL/Docker 黑盒待负责人验收。 ### 9.1 接口 | 接口 | 方法 | 角色 | 说明 | |------|------|------|------| | `/profile/me/candidate-scoring` | GET | STUDENT 本人 | 综合评分卡(8 维 + overallScore + weights + status) | | `/profile/me/candidate-scoring/dimensions/{dimension}` | GET | STUDENT 本人 | 单维度明细(subScores/reasons/dataSources);非法 dimension → 400 | | `/profile/me/candidate-scoring/trend?months=6` | GET | STUDENT 本人 | 月度趋势(months 1–24,越界 400) | | `/company/candidates/{applicationId}/scoring` | GET | COMPANY(岗位归属) | 企业只读评分卡 | ### 9.2 权限矩阵 | 接口 | 未登录 | STUDENT | COMPANY | ADMIN | |------|--------|---------|---------|-------| | 学生 3 接口 | 401 | ✅ 本人 | 403 | 403 | | 企业评分 | 401 | 403 | ✅(岗位归属) | 403 | - 企业归属:`companyRepository.findByOwnerUserId(jwtUserId)` 无企业→403/2002;`company.id != job.companyId`→403/8004;application 不存在→404/8002。 ### 9.3 企业响应脱敏字段(禁止展示) 企业评分响应**仅含** `applicationId / overallScore / status / dimensions[]{dimension,score,status} / generatedAt / rulesVersion`;**不得出现** `studentUserId / candidateId / phone / email / resumeUrl / subScores / 内部日志`。 ### 9.4 状态与错误码 - 维度状态:`OK / ESTIMATED / INSUFFICIENT_DATA / PENDING`;PENDING/INSUFFICIENT_DATA 的 `score` 为 `null`。 - 综合卡状态:`OK / PARTIAL / INSUFFICIENT_DATA / PENDING`;无可计算维度时 `overallScore=null`。 - 错误码复用:`BAD_REQUEST(1001)`、`UNAUTHORIZED(2001)`(未登录实际 401)、`FORBIDDEN(2002)`、`APPLICATION_NOT_FOUND(8002)`、`APPLICATION_OWNER_MISMATCH(8004)`。 ### 9.5 前端注意 - 学生端先调综合卡,按 `status` 决定是否渲染 `overallScore`;PENDING/INSUFFICIENT 维度显示"数据不足/待补充"。 - 企业端评分卡仅用于候选人详情展示,勿透传任何候选人联系方式或简历地址。 ## 10. 企业评分运营看板联调 全部只读聚合;候选人按学生去重,综合分复用评分引擎公式;岗位评价口径为「审核通过 + PUBLISHED」。详见 [docs/company-scoring-dashboard-api.md](company-scoring-dashboard-api.md)。 ### 10.1 接口 | 接口 | 方法 | 角色 | 说明 | |------|------|------|------| | `/company/scoring/dashboard/overview` | GET | COMPANY | 企业评分运营概览(岗位/评价/候选人统计) | | `/company/scoring/dashboard/job-rankings?page&size` | GET | COMPANY | 岗位评分排名(avgRating DESC, jobId DESC) | | `/company/scoring/dashboard/job-rankings/{jobId}` | GET | COMPANY | 单岗位评分排名明细(跨企业 400/7002) | | `/company/scoring/dashboard/candidate-rankings?page&size` | GET | COMPANY | 候选人评分排名(按学生去重,overallScore DESC) | | `/company/scoring/dashboard/candidate-rankings/{applicationId}` | GET | COMPANY | 单候选人评分排名明细(跨企业 403/8004) | | `/company/scoring/dashboard/trend?months=6` | GET | COMPANY | 企业评分趋势(岗位评价 + 候选人综合分,按月) | | `/admin/scoring/dashboard/overview` | GET | ADMIN | 全局运营概览(全平台统计 + 企业 Top10) | ### 10.2 权限矩阵 | 接口 | 未登录 | STUDENT | COMPANY | ADMIN | |------|--------|---------|---------|-------| | 企业看板 6 接口 | 401 | 403 | ✅(本企业) | 403 | | 全局概览 | 401 | 403 | 403 | ✅ | ### 10.3 关键口径 - 候选人去重:同一学生多次投递视为 1 名候选人,代表投递取企业下 `applicationId` 最大一条;`WITHDRAWN`/`REJECTED` 排除。 - 综合分:复用 `CandidateScoringCalculator`,读取候选人 `<= 当前月` 最新期快照计算;无可计算维度 → `overallScore=null`。 - 岗位评价:`status=PUBLISHED` 且 `moderation_status ∈ {AUTO_APPROVED, APPROVED}`(与评价汇总口径一致)。 - `percentile`:在有分候选人中计算(升序),无分 → `null`;`sampleCount` 为有分候选人数。 - 分页参数:`page`(0 起)、`size`(1–100),`months`(1–24,默认 6)。 ### 10.4 前端注意 - 看板为只读统计,勿依赖实时刷新频次;趋势冷启动期通常只有当前月数据,随评分调用/评价累积逐渐补齐。 - 候选人排名项不返回联系方式与简历地址,仅返回姓名/学校/综合分/百分位/维度分(维度分仅在详情接口)。 - `avgJobScore`/`avgCandidateScore`/`overallScore` 无数据时返回 `null`,前端按「暂无数据」处理。 ## 11. Offer 进阶(还价 / 对比 / 统计)联调 在基础 Offer 之上新增薪资洽谈、Offer 对比、企业统计,并扩展 `WITHDRAWN` 撤销状态。 完整字段/错误码见 [docs/offer-advance-api.md](offer-advance-api.md)。OpenAPI 总路径数 102 → **107**。 ### 11.1 接口 | 接口 | 方法 | 角色 | 说明 | |------|------|------|------| | `/offers/{id}/counter-offers` | POST | STUDENT 本人 | 发起还价(SENT 时自动进入 NEGOTIATING) | | `/offers/{id}/counter-offers` | GET | STUDENT 本人 / COMPANY(本企业) | 还价列表 | | `/offers/{id}/counter-offers/{coId}/accept` | PATCH | COMPANY(本企业) | 接受还价 → Offer 成交(ACCEPTED) | | `/offers/{id}/counter-offers/{coId}/reject` | PATCH | COMPANY(本企业) | 拒绝还价 → Offer 保持 NEGOTIATING | | `/offers/compare?ids=1,2,3` | GET | STUDENT 本人 | 对比本人 2-5 个非 DRAFT Offer | | `/offers/statistics?months=6` | GET | COMPANY | 企业 Offer 统计(状态分布/接受率/响应时长) | ### 11.2 还价要点 - 仅 `SENT/NEGOTIATING` 可发起;候选人发起后 Offer 自动进入 `NEGOTIATING`,前端应刷新 Offer 状态。 - 同一 Offer 同时最多一个 PENDING 还价;二次发起返回 409 `OFFER_COUNTER_OFFER_EXISTS`。 - 企业 `accept` 后会更新 Offer 薪资/入职日期并置为 `ACCEPTED`;`reject` 后 Offer 仍为 `NEGOTIATING`,候选人可再次还价。 ### 11.3 对比要点 - `ids` 为逗号分隔且 2-5 个、全为正数;**非法参数一律 400**,前端需先本地校验避免请求失败。 - 返回项附带 `companyName`;被比较 Offer 含 `DRAFT` 视为 403 `OFFER_COMPARE_NOT_OWNER`,故只选已发送/洽谈中 Offer。 ### 11.4 统计要点 - 仅企业可访问;`months` 1-24,越界按默认 6 处理。 - `acceptanceRate` / `avgResponseHours` 无数据时为 `null`,前端按「暂无数据」处理;`byStatus` 恒含全部 7 个状态键。 ### 11.5 撤销(WITHDRAWN) - 企业 `PATCH /offers/{id}/status` `{"status":"WITHDRAWN"}`,仅 `SENT/NEGOTIATING` 有效;其他状态 409。 - 撤销/过期会使该 Offer 的 PENDING 还价变为 `EXPIRED`,Offer 自身释放"唯一有效 Offer"槽位;企业端应刷新 Offer + 还价列表。 ### 11.6 新增错误码(已并入统一映射) | 错误码 | HTTP | 含义 | |---|---|---| | 8109 | 409 | 已存在 PENDING 还价 | | 8110 | 404 | 还价不存在/不属于该 Offer | | 8111 | 409 | 还价非 PENDING | | 8113 | 409 | Offer 已撤销/过期,还价失效 | | 8114 | 400 | Offer 状态不允许还价 | | 8115 | 400 | 对比数量需 2-5 个 | | 8116 | 403 | 对比含非本人/非有效 Offer | ## 12. 学生个人资料 GET /profile/me 联调 ### 12.1 基础信息 | 项 | 值 | |----|-----| | Base URL | `http://127.0.0.1:8080/api/v1` | | 状态 | 已实现、已验收(运行时黑盒 18/18 PASS) | | 接口 | `GET /api/v1/profile/me` | | 认证 | 必须携带 `Authorization: Bearer ` | | Token 获取 | 通过登录接口(`POST /auth/login`,开发环境为 Mock 验证码)获取 `accessToken`;**文档不提供任何真实凭据** | | 限流 | 当前未设置接口级限流 | > `accessToken` 有效期 24h。Token 只通过登录流程获得,切勿在文档、代码或日志中硬编码或打印真实 Token。 ### 12.2 请求参数 - **无 Path 参数**:路径不含任何 `{id}` 等占位符。 - **无 Query 参数**:不读取任何查询串参数。 - **无 Body**:不解析请求体。 - **不接受身份参数**:身份仅从 JWT / SecurityContext 获取,`userId`、`studentUserId` 等入参一律忽略。 - 例如请求 `GET /api/v1/profile/me?userId=999`,后端仍返回 **当前 JWT 身份** 对应的资料,`userId=999` **不会改变** JWT 身份。 ### 12.3 角色权限说明 | 调用方 | HTTP | body.code | body.message | |--------|------|-----------|--------------| | STUDENT | 200 | `200` | `ok` | | COMPANY | 403 | `2002` | `forbidden` | | ADMIN | 403 | `2002` | `forbidden` | | 未登录 / Token 缺失 / 非法 / 过期 | 401 | `401` | `not authenticated` | ### 12.4 成功响应示例(有资料) ```json { "code": 200, "message": "ok", "data": { "id": 1, "userId": 2, "realName": "张三", "education": "本科", "school": "某某大学", "graduationYear": 2026, "resumeUrl": "/uploads/resumes/2/resume.pdf", "skills": "[Java, Spring Boot]", "createdAt": "2026-08-24T10:00:00" }, "timestamp": "2026-08-24T10:00:00Z" } ``` > 成功 `code=200`(**不是 0**)。`null` 字段不序列化。 ### 12.5 空资料响应示例 ```json { "code": 200, "message": "ok", "data": {}, "timestamp": "2026-08-24T10:00:00Z" } ``` - `data={}` 是**空对象,不是 `null`**。 - 有资料查询也**不会**返回 HTTP 404。 - 不会自动创建 `student_profiles` 记录,不修改数据库。 - 前端必须对 `data` 进行**空对象判定**(`Object.keys(data).length === 0`),而非仅判非 null。 ### 12.6 401 响应示例 ```json { "code": 401, "message": "not authenticated", "timestamp": "2026-08-24T10:00:00Z" } ``` ### 12.7 403 响应示例 ```json { "code": 2002, "message": "forbidden", "data": null, "timestamp": "2026-08-24T10:00:00Z" } ``` ### 12.8 业务码说明表 | 场景 | HTTP | code | message | |------|------|------|---------| | 成功 | 200 | `200` | `ok` | | 未登录 / Token 无效 | 401 | `401` | `not authenticated` | | 非 STUDENT 角色 | 403 | `2002` | `forbidden` | > - 成功码是 `200`,**不要写成 `0`**。 > - 本接口未登录错误为 `401`,**不要写成 `2001`**。运行时认证入口实际返回 `code=401`,以运行时契约为准。 ### 12.9 响应字段表(StudentProfileResponse,null 不输出) | 字段 | 类型 | 是否返回 | 说明 | 示例 | |------|------|----------|------|------| | id | Long | 有资料时 | 学生资料 ID | `1` | | userId | Long | 有资料时 | 当前登录用户 ID | `2` | | realName | String | 可空 | 真实姓名 | `张三` | | bio | String | 可空 | 个人简介 | — | | location | String | 可空 | 所在地 | — | | education | String | 可空 | 学历 | `本科` | | school | String | 可空 | 学校 | `某某大学` | | graduationYear | Short/Integer | 可空 | 毕业年份(四位) | `2026` | | resumeUrl | String | 可空 | 简历地址 | `/uploads/resumes/2/resume.pdf` | | skills | String | 可空 | 技能(字符串) | `[Java, Spring Boot]` | | createdAt | date-time | 有资料时 | 创建时间 | `2026-08-24T10:00:00` | > 不返回 `passwordHash`、手机号、验证码、JWT、内部认证字段。 ### 12.10 skills 特别说明 - 当前类型为 **`String`**,**不保证是合法的 JSON**。 - 前端**不要无条件 `JSON.parse(skills)`**,解析失败会抛异常。 - 示例 `"[Java, Spring Boot]"` 只是普通字符串内容,不代表数组表示法。 - 后续若改为数组,必须作为**独立版本或兼容字段变更**处理,不能静默破坏现有消费方。 - **本轮不修改 `skills` 类型,也不修改 PUT 语义。** ### 12.11 resumeUrl 特别说明 - 值可能是**相对存储路径**或文件存储地址,**不保证是完整的 `http/https` URL**。 - 前端**下载简历应调用 `GET /api/v1/profile/me/resume`**,不要直接拼接 `resumeUrl` 做请求地址。 - 上传仍仅接受 PDF/DOC/DOCX 且不超过 10MB;服务端会同时校验扩展名、Content-Type 和实际文件结构。伪装扩展名、损坏文件、危险 DOCX 压缩包或含路径/控制字符的文件名返回 400,前端不要仅依据浏览器 MIME 判断文件有效。 - 上传替换失败时原简历保持可用;成功提交后旧文件才会清理。该流程不改变现有请求字段或响应结构。 ### 12.12 graduationYear 特别说明 - 当前后端**没有实现**年份范围校验。 - 文档**不得虚构**未实现的范围限制(如 1900–2100 之类)。 - 前端若展示年份,可自行决定是否校验,但接口本身不做约束。 ## 13. 候选人反馈响应脱敏 候选人反馈接口保持原有路径、请求、权限和错误码不变: ```text POST /applications/{applicationId}/feedback GET /applications/{applicationId}/feedback PUT /applications/{applicationId}/feedback DELETE /applications/{applicationId}/feedback ``` 成功响应中的 `data` 仅包含以下业务字段:`id`、`applicationId`、`rating`、`content`、`tags`、`createdAt`、`updatedAt`。 学生只能查看自己的投递反馈;岗位所属企业可以创建、查看、修改和删除反馈;其他学生、其他企业和管理员不因用户 ID 巧合而获得访问权限。 响应示例: ```json { "code": 200, "message": "ok", "data": { "id": 1, "applicationId": 31, "rating": 5, "content": "沟通良好", "tags": ["沟通良好"], "createdAt": "2026-08-25T10:00:00", "updatedAt": "2026-08-25T10:00:00" } } ``` 前端不得依赖或透传以下字段;这些字段不会出现在反馈响应中: ```text companyUserId、studentUserId、phone、email、passwordHash、resumeUrl ``` `candidate_feedback` 数据表仍保留企业用户和学生用户关联字段供权限校验和评分聚合使用,但这些内部字段不属于 API 响应。CandidateFeedback 也不等同于结构化面试评分;面试评分将在独立模块中提供。 ## 14. 隐私设置(Privacy Settings)联调 学生隐私设置用于控制简历可见性与联系方式可见性,仅 STUDENT 角色可用: ```text GET /profile/me/privacy PUT /profile/me/privacy ``` - 身份仅从 JWT 获取,不接受 `userId` / `studentUserId` / `candidateId` 等参数。 - GET 无记录时返回默认对象(`resumeVisibility=PRIVATE`、`contactVisible=false`、`version=0`),不写库。 - PUT 执行 upsert,同一学生始终一行;`version` 冲突返回 `409 / 9201`。 - COMPANY / ADMIN → `403 / 2002`;未登录 → `401`。 - 企业候选人详情/列表接口保持原脱敏白名单,不返回 `resumeUrl` 与联系方式,本轮不新增联系方式字段。 详见 [docs/privacy-settings-api.md](privacy-settings-api.md)。 ## 15. 账号注销(Account Deletion)联调 当前登录用户可对自己的账号发起注销申请、查询状态、在冷静期内取消,适用于 STUDENT / COMPANY / ADMIN: ```text POST /profile/me/deletion # 申请注销(NONE → PENDING;PENDING 幂等 200;COMPLETED → 409/9301) GET /profile/me/deletion # 查询状态(无记录返回默认 NONE,不写库) POST /profile/me/deletion/cancel # 取消注销(仅 PENDING 且未过期;PENDING → NONE) ``` - 身份仅从 JWT 获取,不接受 `userId` / `candidateId` 等参数;未登录 → `401`。 - **状态机**:NONE → PENDING → COMPLETED(终态),可在冷静期内取消回 NONE。 - **冷静期**:默认 30 天(`app.deletion.cooling-days` / `DELETION_COOLING_DAYS` 可配置)。 - **PENDING 写限制**:冷静期内 POST/PUT/PATCH/DELETE 业务写操作返回 `409 / 9302`;GET、OPTIONS 及三个注销接口放行。 - **COMPLETED 旧 JWT**:即使 JWT 未过期也拒绝普通业务请求(`409 / 9303`);登录时亦被阻止。 - **脱敏**:仅脱敏 `users` 表 PII(phone→`deleted_{id}`、email/passwordHash/avatarUrl 置空、nickname→`deleted`、is_active=false),**不删除任何业务数据**。 - **原手机号禁止重新注册**:注销完成后仅保存原手机号的 HMAC-SHA256 摘要(`deleted_phone_hash`,密钥 `app.deletion.phone-hash-secret`),不存明文;原手机号再次登录/注册返回 `409 / 9303`,且不会自动创建新账号。 - **懒转换**:到期 PENDING 首次 `GET /profile/me/deletion` 即返回 `COMPLETED`(独立事务重读最新记录),不会出现先 PENDING 后 COMPLETED 的不一致。 - **ADMIN**:最后一个有效 ADMIN 不允许注销 → `409 / 9306`。 - 错误码 `9301-9306`(详见 [docs/account-deletion-api.md](account-deletion-api.md))。 前端建议:发起注销与取消前先 `GET /profile/me/deletion` 确认当前状态;处于 PENDING 时应禁用业务提交类按钮并提示冷静期到期时间。 ## 16. 个人数据导出(Personal Data Export)联调 **这不是简历导出**(简历导出走 `/resume-export-tasks`,两者独立)。个人数据导出把当前登录用户的全量业务数据异步打包成 ZIP 供本人下载,适用于 STUDENT / COMPANY / ADMIN: ```text POST /profile/me/data-export-tasks # 创建导出任务(异步,返回 PENDING) GET /profile/me/data-export-tasks?page=0&size=20 # 本人任务列表(倒序分页) GET /profile/me/data-export-tasks/{taskId} # 本人任务详情(状态/大小等) GET /profile/me/data-export-tasks/{taskId}/download # 下载已完成 ZIP(需 JWT) ``` - **异步**:创建后立即返回 `PENDING`;后台 Worker 打包完成后变为 `COMPLETED` 才可下载。 - **下载需 JWT**:下载地址不能脱离 JWT 单独访问;不提供公开 URL。 - **文件有效期**:默认 24 小时(`app.data-export.file-ttl-hours`),过期状态为 `EXPIRED` 且不可再下载。 - **导出范围/脱敏**:仅包含本人数据;不含他人手机号/邮箱/简历地址、不含 `passwordHash`/JWT/短信验证码/`deletedPhoneHash`、管理员内部审核字段、系统异常堆栈、服务器绝对路径与连接串。会话对方仅保留 `participantId`。详见 [docs/personal-data-export-api.md](personal-data-export-api.md)。 - **错误**:未登录 `401`;访问他人任务 `403/2002`;任务不存在 `404/9401`;存在未完成任务 `409/9402`;文件未就绪 `409/9403`;已过期 `409/9404`;失败 `409/9405`;文件缺失 `409/9406`。 - **注销限制**:注销 PENDING 禁止创建(`409/9302`);注销 COMPLETED 所有导出接口返回 `409/9303`。 前端建议:创建后轮询任务详情直到 `status=COMPLETED` 且 `downloadAvailable=true` 再展示下载按钮;处于注销 PENDING/COMPLETED 时应隐藏「导出我的数据」入口。 ## 17. 结构化面试评分(Structured Interview Score)联调 **这不是 candidate_feedback**(候选人反馈是另一模块,两者独立;本模块是结构化维度评分)。企业为已完成(`COMPLETED`)的面试填写并提交评分,学生仅能在提交后查看自己的评分与历史: ```text PUT /interviews/{interviewId}/score # 企业 owner 保存草稿(幂等更新,DRAFT) GET /interviews/{interviewId}/score # 查看评分(企业 see DRAFT/SUBMITTED;学生仅 SUBMITTED) POST /interviews/{interviewId}/score/submit # 提交草稿 → SUBMITTED(不可逆) GET /interviews/{interviewId}/score/history # 评分快照历史(无记录返回 200 空数组) ``` - **五个维度**:`technicalScore`、`communicationScore`、`problemSolvingScore`、`roleFitScore`、`professionalismScore`,每项 1-5。 - **草稿**:PUT 时五维可留空;重复 PUT 更新同一行(`version` 递增)。 - **提交**:五维全部非空,否则 `400/1001`;`overallScore` 由服务端按 `(t+c+ps+rf+p)/5` 计算(两位小数 HALF_UP),**客户端不要传**,传了也会被忽略。 - **recommendation**(可选):`STRONG_HIRE` / `HIRE` / `NEUTRAL` / `NO_HIRE`,企业手动选择,不由 overallScore 推导。 - **权限**:写入仅岗位所属企业 owner(COMPANY);ADMIN 不可访问;STUDENT 不可写。学生只能看自己的 `SUBMITTED`,查看他人或 DRAFT 返回 `403/8402`。 - **状态机**:无评分 → `DRAFT` → `SUBMITTED`。`SUBMITTED` 后再次 PUT `409/9503`;重复 submit 幂等返回原评分(不新增历史、不重复通知)。 - **通知**:提交成功仅通知目标学生,`businessType=INTERVIEW`,`businessId=interviewId`;草稿与重复提交不发通知;企业自身不通知;无 Push。 - **错误码**:`9501` 并发冲突(409)、`9502` 面试未完成(409)、`9503` 已提交(409)、`9504` 无评分(404);归属非法 `8402`、未被授权 `2002`、面试不存在 `8401`。 - **脱敏**:响应不返回 `companyUserId`/`studentUserId`/`evaluatorUserId`/手机号/邮箱。 - **历史脱敏**:`GET /score/history` 不返回 `actorUserId`,仅 `actorRole`。 - 面试本身必须为 `COMPLETED` 才能评分(否则 `409/9502`)。 前端建议:企业端保存草稿用 PUT;「提交」用 POST submit 并在按钮上二次确认(不可逆);学生端仅在评分状态为 `SUBMITTED` 时展示。API 详见 [docs/interview-score-api.md](interview-score-api.md)。 ## 18. 同岗位竞争力排名(Competition Ranking)联调 学生查看自己在某个 **已投递且为 PUBLISHED** 岗位候选人池中的竞争力排名(匿名): ```text GET /jobs/{jobId}/competition-ranking # 仅 STUDENT 且已投递该岗位 ``` - **权限矩阵**: - 未登录 → `401`(安全过滤器) - `COMPANY` / `ADMIN` → `403/2002` - STUDENT 但未投递该岗位 → `403/2002` - **STUDENT 唯一投递为 `WITHDRAWN` 或 `REJECTED` → `403/2002`**:投递已退出候选人池,与 cohort 排除口径一致,不能再查看排名。 - `jobId <= 0` → `400/1001` - 岗位不存在或状态非 `PUBLISHED`(如 `DRAFT`/`CLOSED`)→ `404/7001` - **cohort**:该岗位非 `WITHDRAWN`、非 `REJECTED` 的所有投递;`SUBMITTED/VIEWED/INTERVIEW/HIRED` 计入;同一学生同一岗位由数据库唯一约束保证仅一条。无有效评分的候选人**仍计入 `cohortSize`**,但不参与百分位,排在末尾。 - **综合分**:复用 `CandidateScoringCalculator` 真实公式(仅统计 `OK/ESTIMATED` 维度,`PENDING/INSUFFICIENT_DATA` 不折算为 0),取每人 `period <= 当前月` 的**最新**快照。 - **百分位**:严格复用企业评分看板公式 `lower / (scored - 1) * 100`;有分候选人数 `< 2` 时 `percentile = null`;未取整为 2 位小数 HALF_UP。 - **本人无有效评分**:`rank`、`overallScore`、`percentile` 均为 `null`(仍返回 `cohortSize`)。 - **响应结构(示例)**: ```json { "jobId": 9, "rank": 2, "cohortSize": 8, "overallScore": 77.50, "percentile": 66.67, "status": "PARTIAL", "asOfPeriod": "2026-08", "mySnapshotPeriod": "2026-08", "dimensions": [ { "dimension": "EDUCATION", "score": 80, "status": "OK", "weight": 12.50 } ], "gaps": [], "topCandidates": [ { "rank": 1, "overallScore": 88.50 } ], "rulesVersion": "candidate-scoring-v0.1", "generatedAt": "2026-08-26T10:00:00Z" } ``` - **脱敏**:`topCandidates` 固定返回 Top 5、每项**仅含 `rank` 与 `overallScore`**,不含 `studentUserId`/`candidateId`/`name`/`school`/`phone`/`email`/`resumeUrl`/`applicationId` 等任何身份字段;响应整体也不含 `candidateId`。不提供 `top` 查询参数。 - **只读**:该接口为 `GET`,不写任何业务表;身份仅取自 JWT,不接收 `userId/studentUserId/candidateId` 参数。 - 规则版本固定 `rulesVersion = "candidate-scoring-v0.1"`;`asOfPeriod` 为计算截止月(当前月),`mySnapshotPeriod` 为当前学生实际使用的快照月(若与他人周期不同,二者可不同)。 - API 详见 [docs/competition-ranking-api.md](competition-ranking-api.md)。 ## 19. 举报与屏蔽(Report & Block)联调 提供举报、屏蔽与 ADMIN 举报处理能力。**身份一律取 JWT,不接受 `userId/reporterId/reportedUserId` 参数**。 **举报(用户端)** ```text POST /reports # 提交举报 GET /reports/me # 我的举报列表(status/page/size) GET /reports/{id} # 举报详情(本人或 ADMIN) ``` - `targetType=USER` 必填 `targetUserId`;`targetType=MESSAGE` 必填 `messageId`。`reportedUserId` 由服务端推导,不传。 - `category` ≤40 字符;`reason` 1–1000 字符;`evidenceUrls` 可选(≤5 个,仅 http/https,每个 ≤500 字符);`idempotencyKey` 可选(≤64 字符)。 - **幂等**:同 `idempotencyKey` + 相同请求体重放返回原举报 `200`;同 key 不同请求体 → `409/9606`;同目标同分类已有 PENDING → `409/9603`。 - **限流**:每用户每小时最多 10 次 `POST /reports`,超限 → `429/9609`。 - **MESSAGE 举报**:无权访问该消息 → `403/2002`(不泄露消息是否存在);发送者由服务端推导,禁止客户端注入。 - 举报自己 → `400/9602`;不存在目标 → 复用用户不存在错误码。 **屏蔽(用户端)** ```text PUT /blocks/{blockedUserId} # 屏蔽 DELETE /blocks/{blockedUserId} # 解除(幂等) GET /blocks/me # 我的屏蔽列表(page/size) ``` - 屏蔽自己 → `400/9608`;PUT 已存在返回 200 不新增;DELETE 不存在返回 200 不报错。 - 屏蔽仅影响**站内信**:禁止与对方新建 DIRECT 会话、禁止向对方发送新消息(`403/2002`,模糊提示“message cannot be sent”);历史消息仍可读,且不影响系统通知、投递、Offer、面试、评分、评价。 **ADMIN 举报管理** ```text GET /admin/reports # 举报列表(status/targetType/category/page/size) GET /admin/reports/{id} # 举报详情 POST /admin/reports/{id}/decision # 处理决定 ``` - 仅 ADMIN;COMPANY/STUDENT/未登录分别 `403/403/401`。 - `decision` 仅 `RESOLVED`/`REJECTED`;REJECTED 必填 `note`;重复决定 → `409/9604`;非法流转 → `409/9605`。 - 处理不自动封禁、不删除消息、不修改投递/面试/评分状态;处理后向举报人发送一条 `REPORT_DECISION` 通知。 **脱敏**:所有举报/屏蔽响应均**不返回** phone、email、passwordHash、resumeUrl、JWT、消息全文;MESSAGE 举报仅返回脱敏 `messagePreview`;举报列表不返回举报人内部 ID。 - API 详见 [docs/report-block-api.md](report-block-api.md)。 ## 20. 统一求职进度聚合(Career Progress)联调 学生端「求职进度」页的**只读**聚合接口,一次返回投递分页、岗位摘要、下一场面试、最近已提交结构化面试评分、可展示 Offer、同岗位竞争力摘要,以及待办/通知计数: ```text GET /profile/me/career-progress?page=0&size=20&status=INTERVIEW # 仅 STUDENT;page/size/status 均可选 ``` - **权限矩阵**: - 未登录 → `401`(安全过滤器) - `COMPANY` / `ADMIN` → `403/2002` - `page<0`、`size<1`、`size>50`、非法 `status` → `400/1001` - 身份只取 JWT,不接受 `userId/studentUserId/candidateId` 参数 - **查询参数**:`page`(默认 0,≥0)、`size`(默认 20,1–50)、`status`(可选,仅 `SUBMITTED/VIEWED/INTERVIEW/HIRED/REJECTED/WITHDRAWN`)。 - **响应结构(示例,字段为 camelCase)**: ```json { "todo": { "pendingInterviewResponseCount": 1, "upcomingConfirmedInterviewCount": 2, "pendingOfferResponseCount": 1, "unreadNotificationCount": 3, "unreadReportDecisionCount": 1 }, "applications": { "items": [ { "applicationId": 31, "applicationStatus": "INTERVIEW", "appliedAt": "2026-08-20T10:00:00", "updatedAt": "2026-08-25T12:00:00", "job": { "jobId": 9, "title": "Java 后端工程师", "companyName": "示例科技", "city": "杭州", "employmentType": "FULL_TIME", "jobStatus": "PUBLISHED" }, "nextInterview": { "interviewId": 23, "roundNo": 2, "title": "技术二面", "interviewType": "ONLINE", "startAt": "2026-08-30T14:00:00", "endAt": "2026-08-30T15:00:00", "status": "CONFIRMED" }, "latestSubmittedInterviewScore": { "interviewId": 22, "overallScore": 3.60, "recommendation": "HIRE", "submittedAt": "2026-08-25T16:00:00" }, "offer": { "offerId": 14, "status": "SENT", "title": "Java 后端工程师", "expireAt": "2026-09-05T23:59:59", "onboardingDate": "2026-09-15" }, "competition": { "eligible": true, "rank": 3, "cohortSize": 18, "overallScore": 77.50, "percentile": 82.35, "status": "PARTIAL", "mySnapshotPeriod": "2026-08", "rulesVersion": "candidate-scoring-v0.1" } } ], "total": 8, "page": 0, "size": 20, "totalPages": 1 }, "generatedAt": "2026-08-27T10:00:00Z" } ``` - **空数据**:无投递 → `items=[]`、`total=0`、`todo` 全 0;无下一场面试/无已提交评分/无可展示 Offer 时,相应对象为 `null`(JSON 省略)。 - **competition**: - 有效投递(`SUBMITTED/VIEWED/INTERVIEW/HIRED`)`eligible=true`;有评分快照才返回 `rank/overallScore/percentile/mySnapshotPeriod`,无快照则这些为 `null` 但仍返回 `cohortSize`。 - `WITHDRAWN/REJECTED` 投递保留在历史,但 `eligible=false` 且不返回 `rank/cohortSize/overallScore/percentile/status/mySnapshotPeriod/rulesVersion`。 - 复用 V32 同岗位排名的同一套 cohort/排序/百分位口径,但不返回 `dimensions/gaps/topCandidates`。 - **Todo 口径**:`pendingInterviewResponseCount`=状态 `SCHEDULED` 面试数;`upcomingConfirmedInterviewCount`=状态 `CONFIRMED` 且 `startAt>now`;`pendingOfferResponseCount`=状态 `SENT/NEGOTIATING` 且 `expireAt>now`;`unreadNotificationCount`=未读通知总数;`unreadReportDecisionCount`=未读 `REPORT_DECISION` 通知数。 - **脱敏**:不返回 `studentUserId/candidateId/companyUserId/evaluatorUserId/phone/email/resumeUrl/passwordHash/JWT`、通知标题/正文、评分 `comment`、Offer 薪资/条款、他人任何身份字段。**本接口只返回通知计数,通知详情仍用 `/notifications`**。 - **只读**:`GET`,不写任何业务表;`size` 上限 50,聚合查询次数与单页条数无关(无 N+1)。 - API 详见 [docs/career-progress-api.md](career-progress-api.md)。 ## 21. RIASEC 职业兴趣测评(Assessment)联调 基于 RIASEC 理论的自研题库(72 道),**仅供职业兴趣探索参考,不构成心理或职业诊断**。结果不写入求职者评分引擎。 **接口(身份一律取 JWT,不接受 `userId` 参数)** ``` GET /assessments/types # 测评类型列表(任意登录用户) GET /assessments/HOLLAND_RIASEC/questions # 题目列表,仅 ACTIVE(隐藏 dimension/reverseScored/source/版本/questionCode) POST /assessments/attempts # 创建答卷(六维各抽 5 题共 30 道)【仅 STUDENT,须 ACTIVE】 GET /assessments/attempts/{attemptId} # 答卷详情(固定题目顺序+已答) PATCH /assessments/attempts/{attemptId}/answers # 保存答案(value 1-5,同请求内同题不可重复,upsert) POST /assessments/attempts/{attemptId}/submit # 提交(30 题全答)返回结果 GET /assessments/attempts/{attemptId}/result # 结果(六维聚合分+排名+探索方向) GET /assessments/me?page=0&size=20 # 我的测评历史 ``` **要点** - **类型 ACTIVE**:题目列表与创建答卷均要求类型为 ACTIVE;INACTIVE/不存在统一 `404/9701`。 - **题目响应**:不返回任何可推导维度或内部编码的字段,`dimension/reverseScored/source/questionVersion/questionCode` 一律不下发;前端只应使用 `id/stem/scaleType`。 - **创建答卷**:返回 30 道题及 `version`;每次创建随机抽题并持久化,**刷新页面 / 再次 GET 不重新抽题**。六维各恰好 5 题、总数严格 30;题库任一维度不足 5 道时拒绝创建。 - **保存答案**:请求带 `version`(乐观锁),并发/版本过期 → `409/9707`;题目不属于本答卷 → `400/9705`;同一请求内同题重复出现 → `400/9705`;同一题跨请求重复保存为 upsert。首次保存后状态变为 `IN_PROGRESS`。 - **提交**:answeredCount<30 → `400/9706`;30 题提交生成结果并写 `ASSESSMENT_ATTEMPT_SUBMITTED` 审计;**重复提交幂等**返回原结果、不重复生成/审计(原子流转 + `attempt_id` 唯一约束,并发提交只生成一条结果/一条审计);不写入 `candidate_dimension_scores`。 - **越权**:答卷题目、已保存答案与结果均仅本人 STUDENT 可读;COMPANY/ADMIN 越权读取一律 `403/2002`。 - **状态机**:DRAFT → IN_PROGRESS → COMPLETED;COMPLETED 不可改;同一学生同一类型最多一份活跃答卷(`409/9708`,由 `active_key` 唯一约束并发原子保证);并发创建竞争失败返回 `409/9708`(不返回 500);同一 `idempotencyKey` 并发重放返回原 attempt(幂等 200);完成后允许重新测评。 - **计分**:`overallScore` 六维按 `percentage = raw/25×100`(HALF_UP scale=2)降序,同分按维度字母升序;前三组成 `primaryCode`。 - **结果脱敏**:不返回 `studentUserId/phone/email/password/原始答案/企业信息/他人信息`;探索方向使用"可能感兴趣的职业探索方向"文案,不使用"适合/精准/最适合/诊断"表述。 - **探索方向**:`career-direction-v0.1`,精确三字母优先,未配置按前三个维度的兜底方向。 - **错误码**:9701(404)/9702(404)/9703(409)/9705(400)/9706(400)/9707(409)/9708(409)/9709(409)。 - **数据表**:新增 `assessment_definitions / assessment_questions / assessment_attempts / assessment_attempt_questions / assessment_answers / assessment_results` 6 张表(V34 迁移)。 - API 详见 [docs/riasec-assessment-api.md](riasec-assessment-api.md)。 ## 22. 四维能力测评(Ability Assessment)联调 > 功能版本 V36。学生四维能力自评(逻辑 / 沟通 / 执行 / 学习),每次按每维抽 6 题共 24 题,限时 30 分钟。企业端不可查看学生测评结果;本版不写入候选评分引擎。API 详见 [docs/ability-assessment-api.md](ability-assessment-api.md)。 ### 22.1 接口(7 个) ``` GET /abilities/types 类型列表(任意登录用户,仅 ACTIVE) POST /abilities/attempts 创建答卷(仅 STUDENT) GET /abilities/attempts/{attemptId} 答卷详情/恢复(仅本人) PATCH /abilities/attempts/{attemptId}/answers 保存/修改答案 upsert(仅本人) POST /abilities/attempts/{attemptId}/submit 提交(24/24 且四维各 6,仅本人) GET /abilities/attempts/{attemptId}/result 结果与每题解析(仅本人) GET /abilities/me?page=0&size=20 我的历史分页 ``` - 不提供 `GET /abilities/{type}/questions`;题目只在人类创建答卷后随 `GET /abilities/attempts/{attemptId}` 返回。 ### 22.2 创建与恢复 - `POST /abilities/attempts` 传 `{typeCode:"WORKPLACE_ABILITY", idempotencyKey?}`;同幂等键重放返回原答卷,不重抽题。 - 创建仅进入 `DRAFT`,`startedAt/expiresAt` 为空、`remainingSeconds=null`,不开始计时。 - 同学生同类型同时至多存在一份 `DRAFT/IN_PROGRESS` 活跃答卷;再创建返回 `9808`。 - 刷新或重进页面,用 `GET /abilities/attempts/{attemptId}` 恢复:题目顺序固定、返回已选 `selectedOptionId`,不重新抽题。 - 返回字段:`attemptId/status/answeredCount/totalQuestions(24)/completionRate/startedAt/expiresAt/remainingSeconds/questions[{attemptQuestionId,position,itemType,stem,options[{optionId,text}],selectedOptionId}]`。 ### 22.3 首次保存启动 30 分钟 - 第 1 个有效答案保存时一次性设置 `startedAt` 与 `expiresAt=startedAt+30分钟`,`status` 转 `IN_PROGRESS` 并返回 `remainingSeconds`。 - 后续保存不延长倒计时。同一 PATCH 内重复 `attemptQuestionId` 或非法 `optionId` → 整体拒绝 `9805`,不启动计时。 - 超时后保存/提交一律按 `9803` 拒绝;读取时自动把超时答卷标为 `EXPIRED` 并释放活跃占位(`active_key` 置空)。 ### 22.4 提交条件 - 必须 24/24 且四维每维各 6 答满才能提交,否则 `9806`(产品「完成率 ≥70%」仅示意,不做提交门槛)。 - 提交原子推进 `DRAFT/IN_PROGRESS→COMPLETED`;重复/并发提交幂等返回原结果,只生成 1 条结果与 1 条提交审计。 ### 22.5 7 天有效期 / 9810 冷却 / 9811 90 天限制 - 提交生成结果:`validUntil=completedAt+7天`;`validityStatus` 取 `VALID/EXPIRED`(动态判断)。 - 有效结果存续期内禁止重测 → `POST /abilities/attempts` 返回 `9810`(409)。 - 滚动 90 天内最多完成 3 次;达上限 → `9811`(429)。`EXPIRED` 超时答卷不计入完成次数。 - 历史答卷与结果永久保留,可随时分页查询 `GET /abilities/me`。 ### 22.6 结果字段(白名单,前端可直接展示) - `dimensionScores[{code,score,answeredCount}]`(LOGIC/COMMUNICATION/EXECUTION/LEARNING 百分制)、`overallScore`、`weaknesses[{code,score}]`(并列最低维度全返回)、`radar[{code,value}]`、`improvementAreas[{dimension,currentScore,summary,actions}]`、`questionFeedback[{attemptQuestionId,position,stem,selectedOptionId,awardedScore,maxScore,explanation}]`、`validityStatus/validUntil/answeredCount/completionRate/questionSetVersion/rulesVersion/generatedAt/disclaimer`。 - `improvementAreas` 只含 `dimension/currentScore/summary/actions`,不返回 `competencyTag`。 ### 22.7 脱敏字段(禁止向学生/前端返回) - 题目与结果均不得返回:`questionCode`、`competencyTag`、`answerKey`、`scoringRule`、`source`、`difficulty`、`discrimination`,以及 `studentUserId/phone/email/password` 等身份字段。 - `questionFeedback` 允许题干、用户所选项、得分与解析文本,但绝不返回正确选项字母或完整评分规则。 ### 22.8 错误码(V36,98xx) | 码 | 含义 | HTTP | |---|---|---| | 9801 | 类型不存在/未启用 | 404 | | 9802 | 答卷不存在 | 404 | | 9803 | 答卷状态非法(含超时/已提交) | 409 | | 9805 | 答案非法(跨答卷/同题重复/非法选项) | 400 | | 9806 | 未答满 24/24 或某维不足 | 400 | | 9807 | 并发保存冲突 | 409 | | 9808 | 已有活跃答卷 | 409 | | 9809 | 幂等冲突 | 409 | | 9810 | 有效结果冷却期内不可重测 | 409 | | 9811 | 90 天完成次数达上限 | 429 | | 复用 | 1001 参数/业务非法、2001 未登录、2002 无权限 | 400/401/403 | ### 22.9 前端注意 - 学生每次答卷题目固定顺序且数量恒为 24;恢复页只渲染 `questions` 即可,勿依赖外部题库接口。 - 计时用后端 `remainingSeconds` 展示(创建阶段为 null);到期后结果不生成,提示重测需先创建新答卷。 - 结果页必须展示 `disclaimer` 文案(自我了解与提升参考,不构成职业资格/招聘结论/永久标签)。 ## 23. 防刷风险概览与运营规则配置(Risk Governance)联调 > 功能版本 V37。仅 ADMIN 使用,把既有拒绝型安全审计(`REPORT_RATE_LIMITED`/`BLOCKED_SEND_PROHIBITED`)转为可聚合的风险事件并支持规则配置与人工标记。不自动封禁/限流,不改动用户、投递、消息、评分状态。API 详见 [docs/risk-governance-api.md](risk-governance-api.md)。 ### 23.1 接口(7 个,仅 ADMIN) ``` GET /admin/scoring/risk/overview 风险概览(OPEN 总数/等级分布/规则分布/最近检测) GET /admin/scoring/risk/trend?bucket=HOUR|DAY 风险趋势(SQL 聚合) GET /admin/scoring/risk/distribution 按规则×等级分布 GET /admin/scoring/risk/events 事件分页(status/severity/ruleCode/from/to) GET /admin/scoring/risk/rules 规则列表 PUT /admin/scoring/risk/rules/{ruleCode} 更新规则(expectedVersion 乐观锁) POST /admin/scoring/risk/events/{eventId}/actions 人工标记(ACKNOWLEDGE/DISMISS) ``` - 身份一律 JWT/SecurityContext;不接受 `adminUserId/operatorUserId/userId` 参数。 - 未登录 `401`;STUDENT/COMPANY `403/2002`;仅 ADMIN 可访问。 ### 23.2 时间与分页 - 默认时间范围近 7 天,最大 90 天;`from>to` 或超范围 → `9906`。 - `page` 从 0 起、`size` 1–50;`trend.bucket` 仅 `HOUR`/`DAY`。 ### 23.3 规则配置 - 更新请求:`{"enabled":true,"thresholdCount":3,"windowMinutes":15,"severity":"MEDIUM","expectedVersion":2}`。 - 不允许修改 `ruleCode`/`sourceAction`;`expectedVersion` 不匹配 → `9903`(409)。 - 规则只影响后续扫描;禁用规则对审计照常越过 checkpoint,重新启用不回放历史。 ### 23.4 人工标记与状态机 - 事件状态机:`OPEN→ACKNOWLEDGED`、`OPEN→DISMISSED`、`ACKNOWLEDGED→DISMISSED`;`DISMISSED` 终态。 - 操作请求:`{"action":"ACKNOWLEDGE","reasonCode":"MONITORING","expectedVersion":0}`。 - `reasonCode` 仅 `MONITORING`/`CONFIRMED_FALSE_POSITIVE`/`OUT_OF_BAND_ESCALATED`,无自由文本备注。 - 冲突/非法流转 → `9904`/`9905`(409)。 ### 23.5 响应脱敏(ADMIN 可见字段白名单) - 事件响应仅:`eventId/ruleCode/sourceAction/severity/status/occurrenceCount/windowStartedAt/windowEndedAt/detectedAt/version`。 - 严禁返回:`subjectUserId`、手机号、邮箱、`ipAddress`、`userAgent`、`details`、`requestId`、token/password/secret 等。运营仅用 `eventId` 定位,不做自动处置。 ### 23.6 错误码(V37,99xx) | 码 | 含义 | HTTP | |---|---|---| | 9901 | 规则不存在 | 404 | | 9902 | 事件不存在 | 404 | | 9903 | 规则并发更新 | 409 | | 9904 | 事件并发操作 | 409 | | 9905 | 非法 action/reasonCode/流转 | 409 | | 9906 | 非法筛选(时间/bucket/severity/status) | 400 | | 复用 | 2001 未登录、2002 无权限、1001 分页/参数非法 | 401/403/400 | ### 23.7 前端注意 - 风险入口仅在管理后台;学生/企业端无任何风险接口。 - 规则/阈值的命中可能不会立刻出现:扫描默认每 60 秒跑一次(`app.risk.scan.fixed-delay-ms`)。 - 概览/趋势/分布由后端 SQL 聚合返回,前端无需额外拉取明细。 ## 24. V40 企业评价管理与官方回复 > 验收状态:真实 MySQL/Docker 黑盒已通过(`PASS=62 FAIL=0 SKIP=0`);OpenAPI 159/196,待 Git 检查点。 ## V41 企业人才发现 新增接口详见 [enterprise-talent-discovery-api.md](enterprise-talent-discovery-api.md)。学生先通过 `/profile/me/talent-discovery` 显式开启授权;企业仅能查询已认证企业自己的已发布岗位,岗位需具备 V39 审核通过且启用的画像。候选卡只返回不透明 `candidateKey`、匿名标签、城市、学历、毕业年份、白名单技能、匹配档位、固定概述和可用信号类别,不返回学生身份、联系方式、简历或任何测评分数。 V41 服务端先对全部合格候选计算并全局排序,再切分页;`fitScore` 不下发。无可用岗位画像返回 `UNAVAILABLE`,无候选返回 `NO_ELIGIBLE_CANDIDATE`。OpenAPI 实际为 161 paths / 199 operations(学生 GET/PUT 共用一个 path)。 ### V42 企业人才邀约与授权沟通 V42 新增 6 个 path / 6 个 operation,静态 OpenAPI 计数为 167 paths / 205 operations。已认证企业只能对本公司 `PUBLISHED` 且画像 `enabled=true && APPROVED` 的岗位使用 V41 `candidateKey` 创建邀约;请求只提交 `candidateKey`、站内 `message`(1–300 字)和 UUID `idempotencyKey`,不得提交学生 ID。服务端会重新校验授权、账号状态、当前企业全部历史投递、有效推荐信号和每日限额。 > 验收状态:自动化测试 1475/1475 通过;真实 MySQL/Docker 黑盒通过(`PASS=76 / FAIL=0`),运行时与静态 OpenAPI 均为 167 paths / 205 operations。 学生通过 `/profile/me/talent-invitations` 查看邀约,详情中的 `version` 必须原样带入决策请求的 `expectedVersion`。只有 `PENDING` 可 `ACCEPT` 或 `REJECT`;接受成功才返回 `conversationId` 并建立受限 `TALENT_INVITATION` 会话,拒绝或过期不建会话。邀约有效期 7 天,终态后同企业同学生冷却 30 天;重复幂等请求返回原邀约,不重复占用额度。 企业响应不包含 `candidateKey`、`studentUserId`、测评/评分详情、联系方式或内部原因;邀约会话的会话、消息列表和发送响应只返回 `CURRENT_USER`/`OTHER_PARTICIPANT` 相对身份,不暴露 `createdBy`/`senderUserId`。V42 不写 `candidate_dimension_scores`,不发送通知。 > 验收状态:自动化测试 1461/1461 通过;真实 MySQL/Docker 黑盒已通过(`PASS=37 / FAIL=0`)。黑盒确认最新低分测评优先于历史高分、同企业任一历史投递均排除、其他企业投递不误排,并验证全局排序在分页之前完成。 ### 24.1 接口 ```text GET /companies/me/job-reviews GET /companies/me/job-reviews/{reviewId} POST /companies/me/job-reviews/{reviewId}/reply PUT /companies/me/job-reviews/{reviewId}/reply GET /admin/review-replies/pending POST /admin/review-replies/{replyId}/decision ``` 企业接口只返回当前登录企业自有岗位评价;身份从 JWT 获取,不传 `companyId`、`studentUserId`。STUDENT/ADMIN 为 `403/2002`,未登录为 `401`,跨企业访问不泄露评价是否存在。 ### 24.2 回复状态与操作 - 创建回复正文 1–500 字符,`idempotencyKey` 必须为 UUID(最长 64);安全文本为 `AUTO_APPROVED`,风险文本为 `PENDING`。 - 同评价最多一个当前回复;同幂等键同评价同正文重放返回原回复,内容不一致为 `409/10106`,已有回复为 `409/10102`。 - 只有 `REJECTED` 回复可以 PUT 修订,必须携带 `expectedVersion`;`AUTO_APPROVED`、`APPROVED`、`PENDING` 不可覆盖或删除。版本冲突为 `409/10104`。 - ADMIN 仅可对 `PENDING` 执行 `APPROVE` 或 `REJECT`;驳回时 `reason` 必填(1–500 字符)。重复决策为 `409/10103`,非法决策为 `400/10107`。 企业列表/详情的 `reply` 只包含 `id/content/status/version/updatedAt/rejectionReason`。禁止依赖 `studentUserId`、学生姓名、联系方式、IP/UA、`moderationScore`、内部 `reasonCodes` 或管理员 ID。 ### 24.3 公开评价与官方回复 既有 `/jobs/{jobId}/reviews` 与 `/jobs/{jobId}/reviews/{reviewId}` 的评价载荷不再包含 `studentUserId`,可显示固定 `reviewerLabel="匿名求职者"`。仅公开评价且回复状态为 `AUTO_APPROVED`/`APPROVED` 时显示: ```json {"officialReply":{"content":"感谢反馈。","repliedAt":"2026-09-04T10:05:00"}} ``` `officialReply` 不含回复 ID、公司 ID、审核状态、版本、原因码或审核人信息;`PENDING`/`REJECTED` 不渲染该区域。前端应兼容字段缺省或 `null`。 ### 24.4 管理员页面 待审列表可按公司、岗位、风险等级和时间筛选;管理员响应中的风险等级/原因码只用于后台审核,不得透传到公开评价。V40 不提供回复通知、统计、申诉或公开历史接口。 ### 24.5 错误码 `10101` 回复不存在(404)、`10102` 已有回复(409)、`10103` 状态非法(409)、`10104` 并发冲突(409)、`10105` 正文/幂等键非法(400)、`10106` 幂等冲突(409)、`10107` 决策非法(400)。 ## 24. V39 测评驱动岗位推荐(编码中) - 开关 `RECOMMENDATION_ASSESSMENT_ENABLED` 默认 `false`:关闭时 `GET /profile/me/job-recommendations` 仅返回 `mode=FALLBACK` 的最新公开岗位,不展示伪造的测评匹配分。 - 开启后按学生偏好 40%、RIASEC 聚合分 35%、四维能力聚合分 25% 做实时规则计算;缺少任一数据源时对可用项重归一化。推荐不读取题目、答案或候选评分快照。 - 新增公开 `GET /job-recommendation-taxonomies`;企业通过 `GET/PUT /companies/me/jobs/{jobId}/recommendation-profile` 选择平台受控行业/岗位族并填写画像。更新启用画像后状态为 `PENDING_REVIEW`,仅 ADMIN 审核通过的启用画像参与个性化推荐。 - 学生接口:`GET /profile/me/job-recommendations?page&size`;`POST /profile/me/job-recommendations/{jobId}/feedback`;`DELETE .../feedback?expectedVersion=`。`NOT_INTERESTED` 会持久化排除该岗位;解释反馈不影响首版分数。 - ADMIN:`GET /admin/job-recommendation-profiles`、`POST /admin/job-recommendation-profiles/{jobId}/decision`。企业端不得取得任何学生分数或推荐理由;学生端不得取得画像权重、测评结果 ID 或题库字段。 ## 25. V43 候选评分公平性与稳定性监测 V43 仅供管理员后台使用,前端不得向学生或企业展示。当前状态为版本内补集定向修复完成、待独立复验;总览和申诉使用最终 SQL 聚合,并统一执行独立候选人数与补集抑制。`drift` 和 `ranking-stability` 在当前没有不可变评分历史与排名快照时返回 `availability=UNAVAILABLE`,页面应显示“暂无可比历史数据”。 ## 27. N07 消息撤回与会话一致性 消息发送者可在发送后两分钟内调用 `POST /conversations/{conversationId}/messages/{messageId}/recall` 撤回本人消息。请求不接收用户 ID;服务端从登录身份、会话成员关系和消息发送者校验归属。窗口边界采用 `capturedNow <= createdAt + 2 minutes`,恰好两分钟仍允许,超过窗口返回 409。非成员、非原发送者、管理员和跨会话访问均按既有 403/2002 语义处理。 成功撤回后,消息状态为 `RECALLED`,正文统一显示“消息已撤回”,并保留原时间线位置;响应、消息列表、发送响应和审计均不再返回原正文。重复撤回幂等返回当前撤回状态,不重复写审计。撤回消息不计入未读数,`markRead` 不为撤回消息新增已读记录。`DIRECT` 会话保持既有 `senderUserId` 语义,`TALENT_INVITATION` 会话继续只返回 `CURRENT_USER`/`OTHER_PARTICIPANT` 相对身份。完整接口契约见 [messaging-api.md](messaging-api.md)。 ## 26. N02 候选评分规则解释与合理性 学生评分卡和单维详情在保持原有路径不变的前提下增加固定解释字段。综合卡新增 `explanationVersion=candidate-scoring-explanation-v1`、`coverage`(实际参与 overallScore 的维度及排除原因)和 `comparabilityNotice`;单维详情新增中性 `displayName`、`whatItMeasures`、`interpretation`、`limitations`、`evidenceLevel`、`ruleContributions`、`improvementActions`、`explanationVersion`、`validity`(仅 ABILITY)和 `calculatedAt`。 `ruleContributions` 与真实评分分支同源,贡献之和可对账最终维度分数;固定模板不使用 AI/LLM/SHAP。`dataSources` 只显示“个人教育资料”“求职偏好”“平台求职活动”“简历与技能资料”“Offer 履约与候选反馈”“四维能力测评”等产品语义名称,不展示表名、字段路径、题目、答案、答案键或评分规则。`INTEGRITY`、`GROWTH`、`STABILITY`、`COMPLIANCE`、`ACTIVITY` 等信号均明确为平台资料/行为代理,不代表人格、信用、成长潜力、服从性、工作积极性或录用结论。 企业评分接口 `/company/candidates/{applicationId}/scoring` 的字段和 JSON 结构保持不变,企业不返回任何解释字段、行为细分、规则贡献、改进建议或 ABILITY 四维详情。N02 不新增 Flyway、表、ErrorCode、公网路径、消息或审计动作,也不改变分数、权重、排名、推荐、申诉和快照写入语义。 可用接口、参数和小样本规则见 [candidate-scoring-fairness-api.md](candidate-scoring-fairness-api.md)。前端不得根据城市、学校、姓名等字段创建敏感人群筛选,也不得尝试通过多次小样本查询反推出候选人。 总览、维度和申诉响应都显式返回 `availability/reason`。状态/版本/回滚原因及分布桶均按独立候选人数计算;`appealCount` 仅表示事件数。`SUPPRESSED` 时不得用总体计数、补集或其它接口字段反推出被隐藏比例,`NO_DATA`、`INSUFFICIENT_SAMPLE`、`SUPPRESSED` 均不得渲染为 `NORMAL`。 ## 13. 企业下载候选人简历(S03) ``` GET /api/v1/company/candidates/{applicationId}/resume Authorization: Bearer ``` 响应是**文件流**(不是 JSON 包装),前端应按二进制处理: - 成功:`200` + `Content-Type`(PDF/DOC/DOCX)+ `Content-Disposition: attachment; filename*=UTF-8''…` + `Cache-Control: no-store` - `401` 未登录或会话失效 - `403` 非 COMPANY 角色、无企业资料、跨企业投递,或候选人隐私设置不允许 - `404` 投递不存在(`APPLICATION_NOT_FOUND`)或学生没有简历(`PROFILE_NOT_FOUND`) 前端注意事项: 1. **不要**从候选人列表/详情里找 `resumeUrl` —— 这些 DTO 有意不含该字段; 下载必须走上面这个接口。 2. 403 与 404 都按「不可下载」处理,**不要**在 UI 上区分「隐私拒绝」与「跨企业」, 避免泄露归属信息。 3. 下载链接不要放进 URL 或日志;如需预览,请用同一 Authorization 头请求。 4. 响应已声明 `no-store`,前端不要再把简历文件写入本地持久缓存。