# 职来后端前端联调说明(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. Mock 验证码日志位置 开发环境短信为 Mock,验证码打印在**服务控制台/stdout 日志**中: ``` MOCK SMS CODE: phone=13800000002, purpose=login, code=483920 ``` - 运行 `mvn spring-boot:run` → 看终端输出 - 后台运行 → 看重定向的日志文件(如 `logs/mydreamjob.log`) - 每次发码后取**最新一条** `code=<6位数字>` ## 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 使用方式 登录成功返回 `accessToken`(有效期 24h)与 `refreshToken`。 ```json { "code": 200, "data": { "userId": 2, "accessToken": "eyJhbGciOi...", "refreshToken": "eyJhbGciOi...", "expiresIn": 86400, "role": "STUDENT" } } ``` **后续请求**:所有需要认证的接口在请求头携带 ``` Authorization: Bearer ``` ## 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. 验证码打印在服务日志:`MOCK SMS CODE: ... code=xxxxxx` 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,参与计算维度重新归一化。 ### 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` 做请求地址。 ### 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)。