Browse Source

docs: publish onboarding APIs and OpenAPI

backup/master-local-20261012
pwklppg 1 month ago
parent
commit
aad0ba6a29
  1. 12
      README.md
  2. 4
      changes/n04-onboard/.spec-superflow.yaml
  3. 4
      changes/n04-onboard/.superpowers/sdd/execution-plan.yaml
  4. 3
      changes/n04-onboard/.superpowers/sdd/progress.md
  5. 81
      changes/n04-onboard/.superpowers/sdd/reviews/W3-T7.md
  6. 30
      docs/backend-development-roadmap.md
  7. 127
      docs/frontend-integration.md
  8. 433
      docs/onboarding-api.md
  9. 5575
      docs/openapi-rc.json
  10. 107
      docs/verify_openapi.py

12
README.md

@ -6,15 +6,16 @@
| 项目 | 当前状态 |
| --- | --- |
| Git 检查点 | V34 已冻结,最新检查点为 `bf75fc` |
| 数据库迁移 | Flyway V1–V33;V34 为纯接口聚合模块,无新增迁移 |
| API 契约 | 126 paths / 160 operations |
| 自动化测试 | 1275 个通过,0 失败,0 错误 |
| Git 检查点 | N04 已建立 T7 暂停检查点 `73afd87`;T8 完成后形成最终检查点 |
| 数据库迁移 | Flyway V1–V39;N04 新增 V38–V39 |
| API 契约 | 135 paths / 172 operations |
| 自动化测试 | 1492 个执行:1481 通过、2 失败、9 错误;失败均在既有 CareerProgress/JobReviewDimension/LocalResumeStorage 测试,与 N04 文档变更无关 |
| 已验收范围 | V27–V34;V33 已完成真实 MySQL/Docker 黑盒验收 |
| N04 入职清单与公司知识介绍 | 实现完成,待真实 MySQL/Docker 黑盒验收 |
| 账号注销冷静期 | 30 天 |
| 本地服务端口 | `8080` |
V35 霍兰德职业兴趣测评目前仅完成设计,尚未进入编码和数据库迁移阶段。
N04 入职清单与公司知识介绍已完成代码、N04 专项测试、OpenAPI 和联调文档;全量 `mvn clean verify` 仍受既有非 N04 测试失败阻塞,待真实 MySQL/Docker 黑盒验收。V35 霍兰德职业兴趣测评目前仅完成设计,尚未进入编码和数据库迁移阶段。
## 仓库结构
@ -123,6 +124,7 @@ python .\docs\verify_openapi.py
- [OpenAPI 契约](docs/openapi-rc.json)
- [前端联调说明](docs/frontend-integration.md)
- [后端开发路线图](docs/backend-development-roadmap.md)
- [入职清单与公司知识介绍](docs/onboarding-api.md)
- [统一求职进度接口](docs/career-progress-api.md)
- [同岗位竞争力排名](docs/competition-ranking-api.md)
- [举报与屏蔽](docs/report-block-api.md)

4
changes/n04-onboard/.spec-superflow.yaml

@ -31,8 +31,10 @@ dp_4_result: approved: SDD selected; W1 serial T1-T2, W2 serial T3-T5 depends W1
dp_4_confirmed: true
dp_4_timestamp: 2026-09-16T07:58:31Z
batches_completed: 2
execution_paused: true
execution_paused: false
paused_at: 2026-09-16T09:51:43Z
paused_reason: user requested pause for the day; T7 tests are complete but independent T7 review and T8 remain pending
resume_from: T7-review
resume_mode: manual
resumed_at: 2026-09-17T01:17:47Z
resume_count: 1

4
changes/n04-onboard/.superpowers/sdd/execution-plan.yaml

@ -20,5 +20,5 @@ waves:
strategy: serial
tasks: [T6, T7, T8]
depends_on: [W2]
state: paused
receipt: T6 pass; T7 tests complete, independent review pending; T8 pending; manual resume only
state: eligible
receipt: T6 pass; T7 pass; T8 pending

3
changes/n04-onboard/.superpowers/sdd/progress.md

@ -15,3 +15,6 @@
- 2026-09-16T09:40:55Z Task T6: complete after focused repair (commit 97b3b16; W3-T6-rereview PASS; 22 focused tests, 85 onboarding tests).
- 2026-09-16T09:51:11Z Task T7: complete (no commit per instruction; RED->GREEN coverage added; OnboardingEndToEndIntegrationTest 7 tests; 2-review/tag/HIDDEN focused gaps; 96 onboarding tests pass; randomized E2E order pass).
- 2026-09-16T09:51:43Z Execution paused by user for the day. T7 tests complete (96 onboarding tests green); independent T7 review and T8 pending. Manual resume from T7-review only.
- 2026-09-17T01:17:47Z Manual resume accepted. W3 resumes at T7 independent review; T6 remains passed and T8 remains pending.
- 2026-09-17T01:21:54Z Task T7: review W3-T7 PASS; 30 focused tests, 96 onboarding regressions, randomized E2E pass.
- 2026-09-17T09:33:54Z Task T8: documentation implementation complete; after adding missing onboarding assertions, verifier RED was `Missing onboarding path: /onboarding/companies/{companyId}/checklist`; static OpenAPI now 135 paths / 172 operations and contains 9 onboarding operations / 6 paths / 20 schemas; `python docs/verify_openapi.py` PASS; N04 focused regression PASS (96 tests); `mvn clean verify` FAIL (1492 tests, 2 failures, 9 errors) in existing CareerProgress/JobReviewDimension/LocalResumeStorage tests unrelated to T8; `mvn -DskipTests package` PASS; no commit; independent T8/W3 review pending.

81
changes/n04-onboard/.superpowers/sdd/reviews/W3-T7.md

@ -0,0 +1,81 @@
# W3 T7 Review: Onboarding End-to-End and Gap Tests
## Verdict
- **Spec compliance:** PASS
- **Task quality:** Approved
- **Overall:** PASS
- **Base:** `97b3b16`
- **Head:** `73afd87`
- **Blocking findings:** none
- **Cannot verify from this task alone:** real MySQL 8.4 execution remains a later verification-gate obligation.
## Verification
- Read `task-reviewer-prompt.md`, `writing-good-tests.md`, the T7 brief, execution contract, both onboarding specs, the full `97b3b16..73afd87` diff, and the complete changed test files where the diff output was truncated.
- Scope check: `git diff --name-only 97b3b16..73afd87` contains no `mydreamjob-server/src/main/**` files. The T7 implementation scope is limited to three test files plus workflow/review artifacts.
- Ran `OnboardingEndToEndIntegrationTest,CompanyKnowledgeServiceTest,OnboardingRepositoryIntegrationTest`: `Tests run: 30, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`.
- Ran `Onboarding*Test,CompanyKnowledge*Test`: `Tests run: 96, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`.
- Ran `OnboardingEndToEndIntegrationTest` with JUnit random method ordering: `Tests run: 7, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`.
- Scoped `git diff --check 97b3b16..73afd87 -- <three T7 test files>` passed. Range-wide `git diff --check` reports two pre-existing blank lines at EOF in `W2-T4.md` and `W2-T5.md`, outside T7.
## Spec Compliance
- All nine distinct onboarding paths are exercised through the full Spring context, MockMvc, JWT filter, H2 `MODE=MySQL`, repositories, and DTO serialization: `OnboardingEndToEndIntegrationTest.java:82-212`.
- Student fallback and merged-company behavior are covered: standard-only after soft deletion, `customized=false`, merged standard/company items, source values, completion count, and rate (`:135-167`, `:202-208`).
- Student absolute-state completion behavior is covered end to end: first completion, idempotent replay with a stale version, cancellation clearing `completedAt`, stale-version conflict, persisted row count, and unchanged state after conflict (`:214-264`).
- Company checklist behavior is covered end to end: list, create, idempotent replay, request-hash conflict, 50-active-item boundary, update, soft delete, repeated delete, cross-company update/delete isolation, and state preservation (`:266-364`).
- Company knowledge behavior is covered across `BASIC_ONLY`, `CONFIGURED`, `EMPTY`, exact two-review `INSUFFICIENT`, and `SUFFICIENT`, including absence of aggregates below three reviews (`:366-441`).
- Public-review filtering excludes `HIDDEN` and non-public moderation states from counts, tags, ratings, and dimension aggregates. Repository coverage is explicit at `OnboardingRepositoryIntegrationTest.java:267-301`; the E2E flow also adds hidden/pending records before asserting the visible totals and aggregates (`OnboardingEndToEndIntegrationTest.java:410-440`).
- Anonymous 401, wrong-role 403, and missing-company/item 404 paths are covered across the endpoint families, with database-state assertions after failures (`OnboardingEndToEndIntegrationTest.java:443-543`).
- Response privacy checks run for every success and error response and cover owner/student IDs, moderation reason, request/content/identity hashes, IP/User-Agent, password/token fields, phone, email, and idempotency keys (`OnboardingEndToEndIntegrationTest.java:545-577`).
- The focused service tests cover the exact two-review threshold, all eight dimension summaries, deterministic tag top-ten limiting, and recursively quoted JSON tags (`CompanyKnowledgeServiceTest.java:292-358`).
## Test Falsifiability
- The E2E tests assert externally observable HTTP status, response JSON, persisted row state, and cross-company ownership effects. They would fail for incorrect routing, authorization, fallback, merging, version handling, soft-delete behavior, or aggregate filtering.
- The threshold tests independently assert the absence of aggregates at zero, one, and two public reviews, and their presence at three. A change to the threshold or query filtering would fail them.
- The repository test independently verifies `PUBLISHED` plus approved moderation filtering and excludes `HIDDEN` records from all aggregate queries. A filter regression would fail the counts, tags, and dimension sample assertions.
- The privacy helper checks serialized response bodies rather than trusting service DTO construction alone.
- The randomized-order E2E run passed without reliance on wall-clock precision, external services, or test-order state.
## Strengths
- The new E2E test uses the real application context and persistence layer, not mocked controllers or repositories.
- Error-path state preservation is asserted at the database level, not only through HTTP status codes.
- The coverage combines a broad endpoint contract flow with focused service/repository tests for threshold, tag, and moderation-filter edge cases.
- No production files were changed; the task stayed within the intended test-hardening scope.
## Issues
### Critical (Must Fix)
None.
### Important (Should Fix)
None.
### Minor (Nice to Have)
- `OnboardingEndToEndIntegrationTest.java:82-212` combines success checks for all nine endpoints into one long flow. It is correct, but a failure gives a broad diagnosis surface. Splitting the endpoint families into smaller E2E tests would improve failure isolation without changing behavior.
- `OnboardingEndToEndIntegrationTest.java:559-576` checks sensitive fields with raw serialized-string substrings. It covers the named fields in the current contract, but a recursive JSON field-name check would be more robust if DTO serialization changes or sensitive data is nested under a renamed field.
- Tag top-ten limiting and recursively quoted JSON parsing are covered at the service layer (`CompanyKnowledgeServiceTest.java:314-358`), while the E2E flow only exercises the three-tag sufficient case (`OnboardingEndToEndIntegrationTest.java:425-434`). This is acceptable for T7 because the focused behavior is covered, but retaining an HTTP-level edge case would provide broader integration evidence.
- Non-T7 range artifact: `git diff --check 97b3b16..73afd87` reports blank lines at EOF in `W2-T4.md:94` and `W2-T5.md:75`. The three T7 test files themselves pass `git diff --check`.
## Residual Verification Warning
- Real MySQL 8.4 behavior has not been run in this environment. H2 `MODE=MySQL` passed; MySQL/Docker black-box verification remains owned by the later verification gate.
- The prior T6 observation about knowledge-service lock-timeout/deadlock exception mapping remains outside T7 and was not modified here.
## Assessment
**Task quality:** Approved
**Reasoning:** The T7 additions provide real full-context coverage for all nine endpoints, the required fallback, idempotency, concurrency-conflict, threshold, filtering, authorization, privacy, and state-preservation behaviors. The focused and full required suites, plus randomized ordering, all pass, and no production files were changed.
Controller receipt command when `ssf` is available:
```bash
ssf execution review changes/n04-onboard --wave W3 --base 97b3b16 --head 73afd87 --report changes/n04-onboard/.superpowers/sdd/reviews/W3-T7.md --verdict pass
```

30
docs/backend-development-roadmap.md

@ -26,11 +26,11 @@
| 项目 | 当前状态 |
|---|---|
| OpenAPI | **126 paths、160 operations** |
| Flyway | **V1–V33** 已存在(V34 为纯接口扩展,不含 Flyway 迁移) |
| 全量测试 | **1275/1275 通过**(本次 `mvn clean verify` 实际数量) |
| 已完成范围 | V24 Candidate Scoring、V25 Governance、V26 Offer Advance、V27 Privacy、V28/V29 Account Deletion、V30 Data Export、V31 Interview Score、V32 Competition Ranking、V33 Report/Block、V34 统一求职进度聚合,及认证、资料、岗位、投递、面试、Offer、看板等核心闭环 |
| 当前结论 | 已达到已实现范围的 Release Candidate;V33 Report/Block 已完成真实 MySQL/Docker 黑盒验收并正式冻结;V34 统一求职进度聚合已完成实现并通过真实 MySQL/Docker 黑盒验收(PASS=99 / FAIL=0 / SKIP=0),待建立正式 Git 检查点 |
| OpenAPI | **135 paths、172 operations** |
| Flyway | **V1–V39** 已存在(V34 为纯接口扩展;N04 新增 V38–V39) |
| 全量测试 | **1492 个执行:1481 通过、2 失败、9 错误**;失败集中在既有 CareerProgress、JobReviewDimension、LocalResumeStorage 测试,与 N04 文档变更无关 |
| 已完成范围 | V24 Candidate Scoring、V25 Governance、V26 Offer Advance、V27 Privacy、V28/V29 Account Deletion、V30 Data Export、V31 Interview Score、V32 Competition Ranking、V33 Report/Block、V34 统一求职进度聚合,以及 N04 入职清单与公司知识介绍;另含认证、资料、岗位、投递、面试、Offer、看板等核心闭环 |
| 当前结论 | N04 入职清单与公司知识介绍已完成代码、N04 专项 96/96 测试和 OpenAPI/文档更新;全量 `mvn clean verify` 仍受既有非 N04 测试失败阻塞,待真实 MySQL/Docker 黑盒验收 |
| 本规划范围 | 代码修复、新功能开发、接口契约、数据权限和治理能力 |
### 2.0 模块状态分类
@ -38,9 +38,9 @@
| 类别 | 模块 |
|---|---|
| 已完成并已验收 | V24 Candidate Scoring、V25 Governance、V26 Offer Advance、V27 Privacy、V28/V29 Account Deletion、V30 Data Export、V31 Interview Score、V32 Competition Ranking、V33 Report/Block、V34 统一求职进度聚合 |
| 已完成、待真实 MySQL/Docker 验收 | — |
| 已完成、待真实 MySQL/Docker 验收 | N04 入职清单与公司知识介绍(V38/V39,9 个接口;专项测试 96/96,全量回归待清除既有失败) |
| 已设计未编码 | 测评系统(Holland/能力测评)、公平性检测、SHAP/规则解释、防刷审计、企业评价管理、企业人才推荐 |
| 未开始 | AI 简历诊断/优化、AI 模拟面试、行业技能词云、入职准备清单、薪资计算器、诚信积分、消息撤回 |
| 未开始 | AI 简历诊断/优化、AI 模拟面试、行业技能词云、薪资计算器、诚信积分、消息撤回 |
### 2.1 已确认的产品决策
@ -76,7 +76,9 @@
- 简历版本管理和简历导出;
- 求职者评分卡;
- Candidate Scoring Engine;
- 评分申诉、审核和回滚。
- 评分申诉、审核和回滚;
- 入职清单:通用项与企业自定义项合并、个人完成状态和完成率;
- 公司知识介绍:企业资料、企业知识内容和公开评价摘要。
### 3.2 企业端
@ -89,7 +91,9 @@
- Offer 管理;
- 面试管理;
- 评价审核基础能力;
- 双向匹配、通知和直聊。
- 双向匹配、通知和直聊;
- 本企业入职清单自定义项管理;
- 本企业公司知识内容维护。
## 4. Figma 与后端覆盖结论
@ -114,6 +118,7 @@
| 企业候选人看板 | `/company/candidates`、`/company/candidates/{id}` | 已完成 |
| 企业评分看板 | `/company/scoring/dashboard/*` | 已完成 |
| 企业认证 | `/companies/me/verification`、`/admin/companies/verifications*` | 已完成 |
| 入职清单与公司知识介绍 | `/onboarding/**`(9 个接口) | **实现完成,待真实 MySQL/Docker 验收** |
### 4.2 部分完成或口径不一致
@ -136,13 +141,12 @@
- AI 简历诊断和优化;
- AI 模拟面试和面试题预测;
- 行业技能词云;
- 入职准备清单和公司知识介绍;
- 薪资计算器和薪资参考;
- 公平性检测;
- SHAP/规则解释;
- ADMIN 防刷审计和规则配置;
- 诚信积分和激励。
- 已非缺失(已实现):个人数据导出(V30)、隐私设置(V27)、账号注销(V28/V29)、举报与屏蔽(V33)、结构化面试评分(V31)、同岗位竞争力排名(V32)、统一求职进度聚合(V34)。
- 已非缺失(已实现):个人数据导出(V30)、隐私设置(V27)、账号注销(V28/V29)、举报与屏蔽(V33)、结构化面试评分(V31)、同岗位竞争力排名(V32)、统一求职进度聚合(V34)、入职清单和公司知识介绍(N04)。
## 5. 已有代码的优先修复项
@ -440,8 +444,6 @@ scoring_risk_actions
- AI 模拟面试;
- 面试题预测;
- 行业技能词云;
- 入职准备清单;
- 公司知识介绍;
- 简历查重;
- 诚信积分;
- 消息撤回。
@ -464,7 +466,7 @@ scoring_risk_actions
| P1 | ADMIN 防刷审计 |
| P2 | SHAP 高级解释 |
| P2 | AI 简历、AI 模拟面试 |
| P2 | 技能词云、入职清单、诚信积分 |
| P2 | 技能词云、入职清单(N04 实现完成,待验收)、诚信积分 |
## 8. 每个模块的统一交付要求

127
docs/frontend-integration.md

@ -125,8 +125,8 @@ Authorization: Bearer <accessToken>
| 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 | 重复 / 非法状态流转 |
| 404 | 4001, 5001, 6001, 7001, 8002, 9102, 9104, 8305, 8501, 9701 | 资源不存在 |
| 409 | 5003, 7003, 8001, 8003, 8301-8302, 8304, 8502-8504, 9101, 9103, 9702-9706 | 重复 / 非法状态流转 / 版本或幂等冲突 |
| 500 | 1002, 9001 | 服务器内部错误 |
**前端策略**:优先判断 HTTP 状态码,再读取 `body.code` 显示业务信息。
@ -734,3 +734,126 @@ GET /profile/me/career-progress?page=0&size=20&status=INTERVIEW # 仅 STUDENT
- TOP10 不是独立接口字段;统一使用 `data.items.slice(0, min(10, data.total))`。
- 匹配度公式为 `round(matchedCount * 100 / min(10, total))`;范围 15 条、命中 5 条时为 `33`。
- 学习路径不返回课程链接、AI 文本、学习时长或进度记录。
## 22. 入职清单与公司知识介绍联调
完整接口、字段和错误示例见 [onboarding-api.md](onboarding-api.md)。Base URL 与认证方式沿用本文第 1、5 节:
```text
http://127.0.0.1:8080/api/v1
Authorization: Bearer <accessToken>
```
### 22.1 接口与角色
| Method | Path | 角色 | 关键交互 |
|---|---|---|---|
| `GET` | `/onboarding/companies/{companyId}/checklist` | STUDENT | 返回通用项与企业活动自定义项;无自定义项时回退通用清单。 |
| `PUT` | `/onboarding/companies/{companyId}/checklist/items/{itemId}/completion` | STUDENT | 绝对状态设置完成/未完成;状态变化时使用 `progressVersion`。 |
| `GET` | `/onboarding/companies/{companyId}/knowledge` | STUDENT | 企业基础资料 + 企业知识 + 公开评价摘要。 |
| `GET` | `/onboarding/company/checklist-items` | COMPANY | 查询本企业活动与软删除自定义项。 |
| `POST` | `/onboarding/company/checklist-items` | COMPANY | 创建本企业自定义项,支持 `idempotencyKey`。 |
| `PUT` | `/onboarding/company/checklist-items/{itemId}` | COMPANY | 使用 `version` 乐观锁更新。 |
| `DELETE` | `/onboarding/company/checklist-items/{itemId}?version={version}` | COMPANY | 软删除;重复删除幂等。 |
| `GET` | `/onboarding/company/knowledge` | COMPANY | 查询本企业知识配置;未配置返回 `configured=false`。 |
| `PUT` | `/onboarding/company/knowledge` | COMPANY | 创建或更新本企业知识配置。 |
### 22.2 请求示例
学生更新完成状态:
```http
PUT /api/v1/onboarding/companies/10/checklist/items/501/completion
Authorization: Bearer <studentAccessToken>
Content-Type: application/json
{"completed": true, "version": 0}
```
企业创建自定义项:
```http
POST /api/v1/onboarding/company/checklist-items
Authorization: Bearer <companyAccessToken>
Content-Type: application/json
{
"title": "领取门禁卡",
"description": "到行政前台领取门禁卡并完成权限激活。",
"category": "ADMIN",
"sortOrder": 1000,
"required": false,
"idempotencyKey": "company-10-access-card-20260917"
}
```
企业更新知识:
```http
PUT /api/v1/onboarding/company/knowledge
Authorization: Bearer <companyAccessToken>
Content-Type: application/json
{
"culture": "以用户价值为中心,保持透明和长期主义。",
"workPolicies": "弹性上班;每周一、三线下办公。",
"onboardingGuide": "报到当天携带身份证件,联系行政同事完成入职登记。",
"version": 1
}
```
### 22.3 响应与前端状态处理
成功响应统一读取 `data`。学生有效清单关键字段:
```json
{
"companyId": 10,
"companyName": "示例科技",
"customized": true,
"totalItems": 13,
"completedItems": 1,
"completionRate": 7.7,
"items": [
{
"itemId": 101,
"source": "STANDARD",
"title": "准备身份证件",
"category": "DOCUMENT",
"required": true,
"sortOrder": 100,
"completed": true,
"completedAt": "2026-09-17T09:00:00",
"progressVersion": 1
}
]
}
```
公司知识回退:
- `knowledgeState=CONFIGURED`:展示 `knowledge`。
- `knowledgeState=BASIC_ONLY`:只展示 `company` 基础资料,不展示知识字段。
- `reviewSummary.state=EMPTY/INSUFFICIENT`:只展示评价数量;不要展示均值、分布和八维汇总。
- `reviewSummary.state=SUFFICIENT`:可以展示平均分、评分分布、热门标签和八维汇总。
错误处理:
| HTTP/code | 前端动作 |
|---|---|
| `401/401` | 清理 Token,跳转登录;不要自动重试。 |
| `403/2002` | 当前角色或企业不匹配,不重试并提示切换正确账号。 |
| `404/5001` | 企业不存在或企业账号未绑定企业,停止当前页面数据加载。 |
| `404/9701` | 条目已删除、跨企业或已不在有效清单;重新加载清单。 |
| `409/9702` | 企业条目版本过期;重新查询 `/company/checklist-items` 后让用户确认再提交。 |
| `409/9703` | 活动自定义项达到 50 条;禁用新增,提示先软删除旧项。 |
| `409/9704` | 幂等键已用于不同请求体;更换 key 或展示原条目,不可盲目重试旧请求。 |
| `409/9705` | 学生进度版本过期;重新查询有效清单,使用最新 `progressVersion`。 |
| `409/9706` | 企业知识版本过期;重新查询配置,使用最新 `version`。 |
幂等与软删除注意:
- 创建重试必须复用原 `idempotencyKey` 和完全相同的标题、说明、分类、排序、必填标记。
- DELETE 只在首次删除时要求当前版本;重复删除虽幂等,但客户端仍应携带 `version` 查询参数。
- 更新/删除成功后必须用响应中的新 `version` 覆盖本地状态,不能继续使用旧值。

433
docs/onboarding-api.md

@ -0,0 +1,433 @@
# 入职清单与公司知识介绍 API(N04)
> 状态:后端实现完成,待真实 MySQL 8.4 / Docker 黑盒验收。
>
> 数据库迁移:`V38__onboarding_checklist_and_company_knowledge.sql`、`V39__onboarding_standard_checklist_seed.sql`。
## 1. Base URL 与认证
本地 Base URL:
```text
http://127.0.0.1:8080/api/v1
```
所有 `/onboarding/**` 接口都要求登录,并携带:
```http
Authorization: Bearer <accessToken>
Content-Type: application/json
X-Request-Id: <optional-trace-id>
```
细粒度角色和所有权在 Service 层校验。除 401 外,接口统一返回 `ApiResponse`:
```json
{
"code": 200,
"message": "ok",
"data": {},
"requestId": "req-...",
"timestamp": "2026-09-17T01:30:00Z"
}
```
## 2. 接口与权限总览
| # | Method | Path | 角色 | 说明 |
|---|---|---|---|---|
| 1 | GET | `/onboarding/companies/{companyId}/checklist` | STUDENT | 读取有效清单、完成状态和完成率 |
| 2 | PUT | `/onboarding/companies/{companyId}/checklist/items/{itemId}/completion` | STUDENT | 设置本人完成或未完成 |
| 3 | GET | `/onboarding/companies/{companyId}/knowledge` | STUDENT | 读取企业资料、知识内容和公开评价摘要 |
| 4 | GET | `/onboarding/company/checklist-items` | COMPANY | 查询本企业活动与软删除自定义项 |
| 5 | POST | `/onboarding/company/checklist-items` | COMPANY | 创建本企业自定义项 |
| 6 | PUT | `/onboarding/company/checklist-items/{itemId}` | COMPANY | 按版本更新本企业条目 |
| 7 | DELETE | `/onboarding/company/checklist-items/{itemId}?version={version}` | COMPANY | 按版本软删除本企业条目 |
| 8 | GET | `/onboarding/company/knowledge` | COMPANY | 查询本企业知识配置 |
| 9 | PUT | `/onboarding/company/knowledge` | COMPANY | 创建或更新本企业知识配置 |
权限规则:
- STUDENT 接口只允许拥有 STUDENT 角色的账号访问。
- COMPANY 接口只允许拥有 COMPANY 角色的账号访问。
- COMPANY 接口不接受 `companyId`;服务端从 JWT 用户解析其所属企业,避免跨企业写入。
- ADMIN 没有 onboarding 专属管理接口;调用 STUDENT/COMPANY 接口时按非目标角色返回 `403/2002`。
- 学生访问企业清单或知识不要求先有 Offer 或投递记录,但目标企业必须存在。
- 企业条目不存在或属于其他企业时统一返回 `404/9701`,不泄露其他企业数据。
## 3. 入职清单语义
### 3.1 通用项与自定义项合并
有效清单由以下两部分组成:
1. 所有活动通用项 `source=STANDARD`;V39 固定写入 12 条。
2. 当前企业所有活动自定义项 `source=COMPANY`。
合并结果按 `sortOrder ASC, id ASC` 稳定排序。企业没有活动自定义项时,`customized=false`,仍返回全部通用项。企业存在活动自定义项时,`customized=true`,返回通用项与企业项。
`category` 固定为:
```text
DOCUMENT | ADMIN | EQUIPMENT | TRAINING | LOGISTICS | OTHER
```
### 3.2 学生读取有效清单
```http
GET /api/v1/onboarding/companies/10/checklist
Authorization: Bearer <studentAccessToken>
```
响应:
```json
{
"code": 200,
"message": "ok",
"data": {
"companyId": 10,
"companyName": "示例科技",
"customized": true,
"totalItems": 13,
"completedItems": 1,
"completionRate": 7.7,
"items": [
{
"itemId": 101,
"source": "STANDARD",
"itemKey": "standard-id-card",
"title": "准备身份证件",
"description": "准备身份证原件或有效身份证明,用于入职登记。",
"category": "DOCUMENT",
"required": true,
"sortOrder": 100,
"active": true,
"completed": true,
"completedAt": "2026-09-17T09:00:00",
"progressVersion": 1
},
{
"itemId": 501,
"source": "COMPANY",
"itemKey": "company-10-8f1c",
"title": "领取门禁卡",
"description": "到行政前台领取门禁卡并完成权限激活。",
"category": "ADMIN",
"required": false,
"sortOrder": 1000,
"active": true,
"completed": false,
"completedAt": null,
"progressVersion": 0
}
]
},
"requestId": "req-onboarding-checklist",
"timestamp": "2026-09-17T01:30:00Z"
}
```
`completionRate = round(completedItems * 100 / totalItems, 1)`;总数为 0 时返回 `0.0`。完成状态只属于当前 STUDENT。
### 3.3 更新完成状态
请求使用绝对状态,而不是切换:
```http
PUT /api/v1/onboarding/companies/10/checklist/items/501/completion
Authorization: Bearer <studentAccessToken>
{"completed": true, "version": 0}
```
响应:
```json
{
"code": 200,
"message": "ok",
"data": {
"itemId": 501,
"completed": true,
"completedAt": "2026-09-17T01:31:00Z",
"progressVersion": 1,
"totalItems": 13,
"completedItems": 2,
"completionRate": 15.4
},
"requestId": "req-onboarding-progress",
"timestamp": "2026-09-17T01:31:00Z"
}
```
版本规则:
- 首次保存某条目时可省略 `version` 或传 `0`。
- 已完成状态变化时必须传当前 `progressVersion`;过期返回 `409/9705`。
- 重复提交相同 `completed` 值保持幂等,不新增记录、不递增版本。
- 首次提交 `completed=false` 不创建记录。
- `completed=false` 会清除 `completedAt`。
- 一名学生与一个清单项最多一条进度记录。
### 3.4 企业创建自定义项
```http
POST /api/v1/onboarding/company/checklist-items
Authorization: Bearer <companyAccessToken>
{
"title": "领取门禁卡",
"description": "到行政前台领取门禁卡并完成权限激活。",
"category": "ADMIN",
"sortOrder": 1000,
"required": false,
"idempotencyKey": "company-10-access-card-20260917"
}
```
响应 `data`:
```json
{
"itemId": 501,
"source": "COMPANY",
"itemKey": "company-10-8f1c",
"title": "领取门禁卡",
"description": "到行政前台领取门禁卡并完成权限激活。",
"category": "ADMIN",
"required": false,
"sortOrder": 1000,
"active": true,
"version": 0
}
```
约束:
- `title`:必填,最多 120 字符。
- `description`:可选,最多 1000 字符。
- `category`:必填,仅允许固定枚举。
- `sortOrder`:可选,默认 `1000`,范围 `0..9999`。
- `required`:可选,默认 `false`。
- `idempotencyKey`:可选,最多 64 字符;服务端会 trim,空串按未提供处理。
- 活动自定义项上限为 50 条;达到上限返回 `409/9703`。
幂等重试:
- 同一企业、同一 `idempotencyKey`、同标题/说明/分类/排序/必填值:返回首次创建的条目。
- 同一企业、同一 key、请求语义不同:返回 `409/9704`。
- 不同企业可使用相同 key,互不冲突。
- 未传 key 时,每次请求创建新条目。
### 3.5 企业查询、更新与软删除
查询本企业全部条目:
```http
GET /api/v1/onboarding/company/checklist-items
Authorization: Bearer <companyAccessToken>
```
响应包含活动与软删除条目,不返回通用项和其他企业项。更新请求:
```http
PUT /api/v1/onboarding/company/checklist-items/501
Authorization: Bearer <companyAccessToken>
{
"title": "领取门禁卡并激活",
"description": "到行政前台领取门禁卡并完成权限激活。",
"category": "ADMIN",
"sortOrder": 900,
"required": true,
"active": true,
"version": 0
}
```
更新使用 `version` 乐观锁,成功递增版本;过期返回 `409/9702`。软删除:
```http
DELETE /api/v1/onboarding/company/checklist-items/501?version=1
Authorization: Bearer <companyAccessToken>
```
首次删除必须使用当前版本,系统只将 `active=false`,不物理删除。重复删除已非活动条目保持幂等成功;接口仍要求携带 `version` 查询参数。
## 4. 公司知识介绍语义
### 4.1 学生读取与回退
```http
GET /api/v1/onboarding/companies/10/knowledge
Authorization: Bearer <studentAccessToken>
```
响应:
```json
{
"code": 200,
"message": "ok",
"data": {
"company": {
"id": 10,
"name": "示例科技",
"logoUrl": "https://cdn.example.com/company-10.png",
"description": "面向企业提供数据基础设施产品。",
"industry": "互联网",
"website": "https://example.com",
"address": "杭州市余杭区",
"verified": true
},
"knowledgeState": "CONFIGURED",
"knowledge": {
"culture": "以用户价值为中心,保持透明和长期主义。",
"workPolicies": "弹性上班;每周一、三线下办公。",
"onboardingGuide": "报到当天携带身份证件,联系行政同事完成入职登记。",
"updatedAt": "2026-09-17T01:33:00Z"
},
"reviewSummary": {
"state": "SUFFICIENT",
"totalReviews": 4,
"averageRating": 4.5,
"ratingDistribution": {
"rating1": 0,
"rating2": 0,
"rating3": 1,
"rating4": 1,
"rating5": 2
},
"topTags": ["发展空间", "同事友好"],
"dimensionSummary": {
"AUTHENTICITY": {"averageScore": 4.5, "sampleCount": 4, "dataState": "SUFFICIENT"},
"BENEFITS": {"averageScore": 4.3, "sampleCount": 4, "dataState": "SUFFICIENT"},
"SALARY_TRANSPARENCY": {"averageScore": 4.0, "sampleCount": 4, "dataState": "SUFFICIENT"},
"JD_CLARITY": {"averageScore": 4.6, "sampleCount": 4, "dataState": "SUFFICIENT"},
"GROWTH": {"averageScore": 4.4, "sampleCount": 4, "dataState": "SUFFICIENT"},
"TEAM_ATMOSPHERE": {"averageScore": 4.7, "sampleCount": 4, "dataState": "SUFFICIENT"},
"PROMOTION": {"averageScore": 4.1, "sampleCount": 4, "dataState": "SUFFICIENT"},
"WORK_LIFE_BALANCE": {"averageScore": 4.0, "sampleCount": 4, "dataState": "SUFFICIENT"}
}
}
},
"requestId": "req-company-overview",
"timestamp": "2026-09-17T01:34:00Z"
}
```
回退与状态:
- 企业无知识记录:仍返回企业基础资料,`knowledgeState=BASIC_ONLY`,`knowledge` 省略。
- 企业有知识记录:`knowledgeState=CONFIGURED`,返回非空知识字段和 `updatedAt`;学生公开读取响应不返回知识配置版本。
- 企业不存在:`404/5001`。
- 响应白名单不包含 `ownerUserId`、用户 ID、手机号、邮箱、评价哈希或审核原因。
评价摘要只聚合企业全部岗位中 `PUBLISHED` 且审核状态为 `AUTO_APPROVED/APPROVED` 的评价:
| `reviewSummary.state` | 条件 | 返回内容 |
|---|---|---|
| `EMPTY` | 0 条 | `state`、`totalReviews=0` |
| `INSUFFICIENT` | 1–2 条 | `state`、真实 `totalReviews`;不返回均值、分布或维度汇总 |
| `SUFFICIENT` | 至少 3 条 | 平均评分、评分分布、热门标签、八维汇总 |
热门标签最多 10 个;八个维度分别返回 `sampleCount` 和 `dataState`。维度的 `dataState` 使用同一 3 条阈值。
### 4.2 企业查询知识配置
```http
GET /api/v1/onboarding/company/knowledge
Authorization: Bearer <companyAccessToken>
```
未配置时:
```json
{
"code": 200,
"message": "ok",
"data": {
"configured": false,
"version": 0
},
"requestId": "req-company-knowledge",
"timestamp": "2026-09-17T01:33:00Z"
}
```
已配置时返回 `configured=true`、非空内容、`updatedAt` 和当前 `version`。空字段按 `@JsonInclude(NON_NULL)` 省略。
### 4.3 企业创建或更新知识配置
```http
PUT /api/v1/onboarding/company/knowledge
Authorization: Bearer <companyAccessToken>
{
"culture": "以用户价值为中心,保持透明和长期主义。",
"workPolicies": "弹性上班;每周一、三线下办公。",
"onboardingGuide": "报到当天携带身份证件,联系行政同事完成入职登记。",
"version": 1
}
```
规则:
- 至少一个内容字段必须非空。
- `culture` 最多 2000 字符,`workPolicies` 和 `onboardingGuide` 各最多 4000 字符。
- 字段按纯文本处理,会 trim;包含 HTML 标签时返回 `400/1001`。
- 首次创建可以省略 `version`,创建后版本为 `0`。
- 更新已有记录必须提交当前版本;版本过期返回 `409/9706`。
- 已有内容与提交内容相同且未传版本时按幂等读取处理;内容不同但未传版本返回 `400/1001`。
## 5. 错误码
| HTTP | code | 含义 | 前端处理 |
|---|---:|---|---|
| 400 | 1001 | 参数、枚举、长度、HTML、缺少必填字段 | 展示字段错误,不自动重试 |
| 401 | 401 | 未登录或 Token 无效 | 清理登录态并重新登录 |
| 403 | 2002 | 角色不匹配、无所属企业或无权限 | 不重试,切换到正确角色 |
| 404 | 5001 | 企业不存在或 COMPANY 无绑定企业 | 检查 `companyId` 或企业绑定状态 |
| 404 | 9701 | 清单项不存在、跨企业或不属于有效清单 | 刷新清单后重新选择条目 |
| 409 | 9702 | 企业清单项版本过期 | 重新 `GET /company/checklist-items`,使用新版本重试 |
| 409 | 9703 | 活动自定义项达到 50 条上限 | 禁用新增,提示先软删除或复用现有项 |
| 409 | 9704 | 幂等键已用于不同请求体 | 更换幂等键,或确认原请求后读取原条目 |
| 409 | 9705 | 学生完成状态版本过期 | 重新读取有效清单并使用新 `progressVersion` |
| 409 | 9706 | 公司知识版本过期 | 重新读取企业知识配置并使用新 `version` |
401 由安全过滤器返回裸 JSON,没有统一 `ApiResponse.requestId`:
```json
{"code": 401, "message": "not authenticated", "timestamp": ""}
```
其他错误示例:
```json
{
"code": 9704,
"message": "idempotency key conflict: request body differs",
"data": null,
"requestId": "req-9704",
"timestamp": "2026-09-17T01:30:00Z"
}
```
## 6. 前端推荐流程
学生端:
1. 登录后调用 `GET /onboarding/companies/{companyId}/checklist`。
2. 使用响应的 `itemId` 和 `progressVersion` 调用完成状态接口。
3. 收到 `9705` 时重新读取清单,避免覆盖其他设备的新状态。
4. 调用 `GET /onboarding/companies/{companyId}/knowledge` 展示企业介绍与评价摘要。
5. 当 `reviewSummary.state=EMPTY/INSUFFICIENT` 时,不展示平均值、分布和维度均值。
企业端:
1. 登录后调用 `GET /onboarding/company/checklist-items` 获取当前版本。
2. 新增时生成稳定 `idempotencyKey` 并保存到重试请求中。
3. 更新或删除前使用最新 `version`,冲突后重新拉取列表。
4. 调用 `GET /onboarding/company/knowledge` 获取配置版本,再使用 `PUT` 保存。
5. 收到 `9703` 时停止自动新增;收到 `9704` 时不要复用旧请求体。

5575
docs/openapi-rc.json

File diff suppressed because it is too large

107
docs/verify_openapi.py

@ -6,9 +6,55 @@ p = PROJECT_ROOT / 'docs' / 'openapi-rc.json'
d = json.load(open(p, encoding='utf-8'))
print('paths:', len(d['paths']))
assert len(d['paths']) == 129, f"Expected 129 paths, got {len(d['paths'])}"
paths = d['paths']
onboarding_paths = [
'/onboarding/companies/{companyId}/checklist',
'/onboarding/companies/{companyId}/checklist/items/{itemId}/completion',
'/onboarding/companies/{companyId}/knowledge',
'/onboarding/company/checklist-items',
'/onboarding/company/checklist-items/{itemId}',
'/onboarding/company/knowledge',
]
for path in onboarding_paths:
assert path in paths, f'Missing onboarding path: {path}'
onboarding_operations = {
('/onboarding/companies/{companyId}/checklist', 'get'): 'getEffectiveChecklist',
('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put'): 'updateCompletion',
('/onboarding/companies/{companyId}/knowledge', 'get'): 'getCompanyKnowledgeOverview',
('/onboarding/company/checklist-items', 'get'): 'listCompanyChecklistItems',
('/onboarding/company/checklist-items', 'post'): 'createCompanyChecklistItem',
('/onboarding/company/checklist-items/{itemId}', 'put'): 'updateCompanyChecklistItem',
('/onboarding/company/checklist-items/{itemId}', 'delete'): 'deleteCompanyChecklistItem',
('/onboarding/company/knowledge', 'get'): 'getCompanyKnowledge',
('/onboarding/company/knowledge', 'put'): 'upsertCompanyKnowledge',
}
http_methods = {'get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'}
for (path, method), operation_id in onboarding_operations.items():
assert method in paths[path], f'Missing {method.upper()} {path}'
assert paths[path][method].get('operationId') == operation_id, f'Wrong operationId for {method.upper()} {path}'
matching_operations = [
(candidate_method, candidate_path)
for candidate_path, path_item in paths.items()
for candidate_method, operation in path_item.items()
if candidate_method in http_methods
and operation.get('operationId') == operation_id
]
assert matching_operations == [(method, path)], (
f'{operation_id} must be globally unique: expected {method.upper()} {path}, got {matching_operations}'
)
assert paths[path][method].get('security') == [{'bearerAuth': []}], (
f'{method.upper()} {path} must require bearerAuth'
)
assert sum(
1
for path_item in paths.values()
for method in path_item
if method in http_methods
) == 172, 'Expected 172 operations after N04 onboarding'
assert len(d['paths']) == 135, f"Expected 135 paths, got {len(d['paths'])}"
required = [
'/review-weight-templates',
@ -205,6 +251,65 @@ print(" Offer advance endpoints all present")
print(" 26 operations all present")
print(" Account deletion endpoints present (3 operations)")
onboarding_success_refs = {
('/onboarding/companies/{companyId}/checklist', 'get'): 'ApiResponseEffectiveChecklistResponse',
('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put'): 'ApiResponseProgressUpdateResponse',
('/onboarding/companies/{companyId}/knowledge', 'get'): 'ApiResponseCompanyKnowledgeOverviewResponse',
('/onboarding/company/checklist-items', 'get'): 'ApiResponseListCompanyChecklistItemView',
('/onboarding/company/checklist-items', 'post'): 'ApiResponseCompanyChecklistItemView',
('/onboarding/company/checklist-items/{itemId}', 'put'): 'ApiResponseCompanyChecklistItemView',
('/onboarding/company/checklist-items/{itemId}', 'delete'): 'ApiResponseCompanyChecklistItemView',
('/onboarding/company/knowledge', 'get'): 'ApiResponseCompanyKnowledgeView',
('/onboarding/company/knowledge', 'put'): 'ApiResponseCompanyKnowledgeView',
}
for (path, method), schema_name in onboarding_success_refs.items():
response = paths[path][method]['responses']['200']
ref = response['content']['*/*']['schema']['$ref']
assert ref == f'#/components/schemas/{schema_name}', f'Wrong success schema for {method.upper()} {path}: {ref}'
for status_code in ('401', '403'):
assert status_code in paths[path][method]['responses'], f'Missing {status_code} for {method.upper()} {path}'
unauthorized = paths[path][method]['responses']['401']
assert unauthorized['content']['application/json']['schema']['$ref'] == '#/components/schemas/UnauthorizedResponse'
forbidden = paths[path][method]['responses']['403']
assert forbidden['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponse'
assert forbidden['content']['application/json']['example']['code'] == 2002
onboarding_schema_fields = {
'CompanyChecklistItemCreateRequest': {'title', 'description', 'category', 'sortOrder', 'required', 'idempotencyKey'},
'CompanyChecklistItemUpdateRequest': {'title', 'description', 'category', 'sortOrder', 'required', 'active', 'version'},
'CompanyChecklistItemView': {'itemId', 'source', 'itemKey', 'title', 'description', 'category', 'required', 'sortOrder', 'active', 'version'},
'EffectiveChecklistResponse': {'companyId', 'companyName', 'customized', 'totalItems', 'completedItems', 'completionRate', 'items'},
'ChecklistItemView': {'itemId', 'source', 'itemKey', 'title', 'description', 'category', 'required', 'sortOrder', 'active', 'completed', 'completedAt', 'progressVersion'},
'ProgressUpdateRequest': {'completed', 'version'},
'ProgressUpdateResponse': {'itemId', 'completed', 'completedAt', 'progressVersion', 'totalItems', 'completedItems', 'completionRate'},
'CompanyKnowledgeUpdateRequest': {'culture', 'workPolicies', 'onboardingGuide', 'version'},
'CompanyKnowledgeView': {'configured', 'culture', 'workPolicies', 'onboardingGuide', 'updatedAt', 'version'},
'CompanyKnowledgeOverviewResponse': {'company', 'knowledgeState', 'knowledge', 'reviewSummary'},
'CompanyView': {'id', 'name', 'logoUrl', 'description', 'industry', 'website', 'address', 'verified'},
'KnowledgeView': {'culture', 'workPolicies', 'onboardingGuide', 'updatedAt', 'version'},
'ReviewSummaryView': {'state', 'totalReviews', 'averageRating', 'ratingDistribution', 'topTags', 'dimensionSummary'},
'DimensionSummaryView': {'averageScore', 'sampleCount', 'dataState'},
}
for schema_name, expected_fields in onboarding_schema_fields.items():
schema = schemas.get(schema_name)
assert schema is not None, f'Missing onboarding schema: {schema_name}'
assert expected_fields <= set(schema.get('properties', {})), (
f'Onboarding schema fields missing: {schema_name}'
)
onboarding_error_paths = {
9701: ('/onboarding/company/checklist-items/{itemId}', 'put', '404'),
9702: ('/onboarding/company/checklist-items/{itemId}', 'put', '409'),
9703: ('/onboarding/company/checklist-items', 'post', '409'),
9704: ('/onboarding/company/checklist-items', 'post', '409'),
9705: ('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put', '409'),
9706: ('/onboarding/company/knowledge', 'put', '409'),
}
for error_code, (path, method, status_code) in onboarding_error_paths.items():
response_text = json.dumps(paths[path][method]['responses'][status_code], ensure_ascii=False)
assert str(error_code) in response_text, f'Missing onboarding error code {error_code} in {method.upper()} {path}'
print(' N04 onboarding endpoints and schemas present (9 operations / 6 paths)')
data = json.dumps(d)
assert '"" : "#/components/schemas/' not in data, "Found empty ref key"
print("No empty ref keys found")

Loading…
Cancel
Save