# n04-onboard 设计 ## Facts and Constraints - 当前后端为 Java 17、Spring Boot 3.5.16、Spring MVC、Spring Security、Spring Data JPA、Flyway、MySQL 8.4,测试使用 JUnit、Mockito、MockMvc 和 H2 `MODE=MySQL`。 - 现有迁移已到 V37。本模块使用 V38 建表、V39 写入通用清单种子,不修改既有迁移或既有表结构。 - `companies.owner_user_id` 唯一,现有 `CompanyService` 已提供 COMPANY 角色与所有权校验模式。 - `job_reviews` 关联 `job_id`,企业评价必须通过 `jobs.company_id` 关联;公开评价限定为 `status=PUBLISHED` 且 `moderation_status IN (AUTO_APPROVED, APPROVED)`。 - 现有八维评价维度由 `ReviewDimension` 枚举统一维护;公司知识摘要复用该枚举,不新增维度定义。 - 统一响应使用 `ApiResponse`,错误使用 `BusinessException` 与 `ErrorCode`,请求 ID 从 `X-Request-Id` 读取。 - 产品要求企业未配置时回退通用清单,公司信息不足时回退企业基础信息,且不调用 AI。 ## Goals - 用最少的数据表和接口完成学生清单、个人完成状态、企业清单配置、公司知识配置与公开评价摘要。 - 保持角色、企业所有权、响应字段和错误码可测试。 - 保持创建幂等、更新并发可控、软删除可重复执行。 - 不改变企业、岗位、Offer、评价等既有接口语义。 ## Non-Goals - 不实现 ADMIN 模板治理、审核发布、版本回滚和 CMS。 - 不实现通知、内容推荐、试用期提醒、入职回访、在职反馈。 - 不按岗位、投递或 Offer 生成个性化内容。 - 不存储或渲染 HTML/富文本,不接入 AI 生成或 AI 审核。 - 不实现前端页面和前端缓存策略。 ## Architecture 新增包 `com.mydreamjob.onboarding`: ```text onboarding/ ├─ OnboardingChecklistController.java ├─ CompanyKnowledgeController.java ├─ OnboardingChecklistService.java ├─ CompanyKnowledgeService.java ├─ OnboardingChecklistItem.java ├─ StudentOnboardingProgress.java ├─ CompanyKnowledge.java ├─ OnboardingChecklistItemRepository.java ├─ StudentOnboardingProgressRepository.java ├─ CompanyKnowledgeRepository.java └─ dto/ ├─ ChecklistItemView.java ├─ EffectiveChecklistResponse.java ├─ ProgressUpdateRequest.java ├─ ProgressUpdateResponse.java ├─ CompanyChecklistItemCreateRequest.java ├─ CompanyChecklistItemUpdateRequest.java ├─ CompanyChecklistItemView.java ├─ CompanyKnowledgeUpdateRequest.java ├─ CompanyKnowledgeView.java ├─ CompanyKnowledgeOverviewResponse.java └─ ReviewSummaryView.java ``` `SecurityConfig` 将 `/onboarding/**` 设为认证访问;Controller/Service 再按角色和所有权执行细粒度校验。所有接口返回 `ApiResponse`。 ## API Contract 所有路径均位于 `/api/v1` 之下。 | Method | Path | Role | Request | Success Response | |---|---|---|---|---| | GET | `/onboarding/companies/{companyId}/checklist` | STUDENT | path `companyId` | `EffectiveChecklistResponse` | | PUT | `/onboarding/companies/{companyId}/checklist/items/{itemId}/completion` | STUDENT | `ProgressUpdateRequest` | `ProgressUpdateResponse` | | GET | `/onboarding/companies/{companyId}/knowledge` | STUDENT | path `companyId` | `CompanyKnowledgeOverviewResponse` | | GET | `/onboarding/company/checklist-items` | COMPANY | 无 | `List` | | POST | `/onboarding/company/checklist-items` | COMPANY | `CompanyChecklistItemCreateRequest` | `CompanyChecklistItemView` | | PUT | `/onboarding/company/checklist-items/{itemId}` | COMPANY | `CompanyChecklistItemUpdateRequest` | `CompanyChecklistItemView` | | DELETE | `/onboarding/company/checklist-items/{itemId}?version={version}` | COMPANY | query `version` | `CompanyChecklistItemView`,`active=false` | | GET | `/onboarding/company/knowledge` | COMPANY | 无 | `CompanyKnowledgeView` | | PUT | `/onboarding/company/knowledge` | COMPANY | `CompanyKnowledgeUpdateRequest` | `CompanyKnowledgeView` | ### Effective Checklist Response ```json { "companyId": 2, "companyName": "本源科技", "customized": true, "totalItems": 13, "completedItems": 4, "completionRate": 30.8, "items": [ { "itemId": 11, "source": "COMPANY", "itemKey": "company-2-uuid", "title": "提交导师联系方式", "description": "入职前由 HR 补充", "category": "ADMIN", "required": false, "sortOrder": 200, "active": true, "completed": false, "completedAt": null, "version": 0 } ] } ``` 通用项使用 `source=STANDARD`;企业项使用 `source=COMPANY`。完成率保留一位小数,最大为 100.0;清单为空时完成率为 0.0。 ### Completion Request ```json { "completed": true, "version": 0 } ``` - 无完成记录时 `version` 可省略或为 0。 - 已有记录且请求值不同,必须提供当前 `version`;版本不一致返回 409 `ONBOARDING_PROGRESS_CONCURRENT_UPDATE`。 - 已有记录且请求值与当前值相同,按幂等成功返回,不因旧版本报错。 ### Company Item Create Request ```json { "title": "提交导师联系方式", "description": "入职前由 HR 补充", "category": "ADMIN", "sortOrder": 200, "required": false, "idempotencyKey": "client-generated-key" } ``` - `title` 必填,长度 1-120。 - `description` 可空,最大 1000。 - `category` 仅允许 `DOCUMENT`、`ADMIN`、`EQUIPMENT`、`TRAINING`、`LOGISTICS`、`OTHER`。 - `sortOrder` 范围 0-9999,默认 1000。 - `required` 默认 `false`。 - `idempotencyKey` 可空,最大 64;服务端保存规范化请求体 SHA-256,用于同 key 不同内容冲突检测。 ### Company Item Update/Delete 更新请求包含 `version`;删除请求通过 `version` 查询参数传入。两者版本不匹配返回 409 `ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE`。删除采用软删除,重复删除按成功返回。 ### Company Knowledge Request ```json { "culture": "重视透明沟通和学习", "workPolicies": "弹性工作制;每周三远程", "onboardingGuide": "9:30 到 A 座 8 层前台报到", "version": 0 } ``` - 三个文本字段可空,但至少一个非空。 - `culture` 最大 2000,`workPolicies` 最大 4000,`onboardingGuide` 最大 4000。 - 首次创建不要求版本;已有记录必须提供当前版本,冲突返回 409 `ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE`。 ### Student Company Knowledge Response ```json { "company": { "id": 2, "name": "本源科技", "logoUrl": null, "description": "企业简介", "industry": "互联网", "website": "https://example.com", "address": "上海市", "verified": true }, "knowledgeState": "CONFIGURED", "knowledge": { "culture": "重视透明沟通和学习", "workPolicies": "弹性工作制;每周三远程", "onboardingGuide": "9:30 到 A 座 8 层前台报到", "updatedAt": "2026-09-16T07:00:00" }, "reviewSummary": { "state": "SUFFICIENT", "totalReviews": 12, "averageRating": 4.4, "ratingDistribution": { "rating1": 0, "rating2": 1, "rating3": 2, "rating4": 4, "rating5": 5 }, "topTags": ["成长空间", "团队氛围"], "dimensionSummary": { "TEAM_ATMOSPHERE": { "averageScore": 4.5, "sampleCount": 8, "dataState": "SUFFICIENT" } } } } ``` 无知识记录时 `knowledgeState=BASIC_ONLY`、`knowledge=null`,企业基础信息仍返回。评价少于 3 条时 `reviewSummary.state=INSUFFICIENT` 或 `EMPTY`,且 `averageRating`、`ratingDistribution`、`topTags`、`dimensionSummary` 为空。 ## Data Model ### `onboarding_checklist_items` ```text id BIGINT PK scope_type VARCHAR(20) NOT NULL -- STANDARD / COMPANY scope_id BIGINT NOT NULL -- 0 或 company_id company_id BIGINT NULL -- 企业项外键;通用项为 NULL item_key VARCHAR(80) NOT NULL title VARCHAR(120) NOT NULL description VARCHAR(1000) NULL category VARCHAR(30) NOT NULL sort_order INT NOT NULL DEFAULT 1000 required BOOLEAN NOT NULL DEFAULT FALSE active BOOLEAN NOT NULL DEFAULT TRUE idempotency_key VARCHAR(64) NULL request_hash CHAR(64) NULL version BIGINT NOT NULL DEFAULT 0 created_at DATETIME NOT NULL updated_at DATETIME NOT NULL UNIQUE (scope_type, scope_id, item_key) UNIQUE (company_id, idempotency_key) INDEX (scope_type, scope_id, active, sort_order, id) ``` `scope_type/scope_id` 为公司项唯一性提供稳定语义,同时允许通用项使用 `scope_id=0`;`company_id` 仅对企业项建立外键,标准项为 NULL。应用层检查 `scope_type=COMPANY` 时 `scope_id=company_id` 且 `company_id` 非空。 ### `student_onboarding_progress` ```text id BIGINT PK student_user_id BIGINT NOT NULL item_id BIGINT NOT NULL completed BOOLEAN NOT NULL completed_at DATETIME NULL version BIGINT NOT NULL DEFAULT 0 created_at DATETIME NOT NULL updated_at DATETIME NOT NULL UNIQUE (student_user_id, item_id) FOREIGN KEY student_user_id -> users(id) FOREIGN KEY item_id -> onboarding_checklist_items(id) ``` 完成状态采用绝对值 Upsert;同一学生与条目唯一。`completedAt` 在首次置为完成时写入,取消完成时清空。 ### `company_knowledge` ```text id BIGINT PK company_id BIGINT NOT NULL culture VARCHAR(2000) NULL work_policies VARCHAR(4000) NULL onboarding_guide VARCHAR(4000) NULL version BIGINT NOT NULL DEFAULT 0 created_at DATETIME NOT NULL updated_at DATETIME NOT NULL UNIQUE (company_id) FOREIGN KEY company_id -> companies(id) ``` ## Decisions ### Decision 1: 使用统一清单表并保留标准/企业来源 - **Choice**:`onboarding_checklist_items` 使用 `scope_type` 区分 `STANDARD` 与 `COMPANY`;有效清单查询返回两类记录。 - **Rationale**:学生只需要一个稳定 itemId 更新完成状态,统一表避免两套进度表、两套排序和重复响应逻辑。 - **Alternatives**:标准表与企业表分离;企业配置 JSON 存储在一个字段中。 - **Consequences**:需要额外维护 scope 一致性检查;换来了统一的完成状态和软删除语义。 ### Decision 2: 通用清单固定种子,企业项只做增量 - **Choice**:V39 写入通用清单;企业自定义项只追加,不允许覆盖或替换通用项。 - **Rationale**:产品明确要求企业未配置时显示通用清单,固定基线可保证所有企业至少有可用内容。 - **Alternatives**:允许企业禁用通用项;允许企业完全替换模板。 - **Consequences**:MVP 不支持企业裁剪通用项;后续若需要,需新增停用映射而不是修改现有响应语义。 ### Decision 3: 创建幂等与更新乐观锁分离 - **Choice**:创建使用 `idempotencyKey + request_hash`;更新、删除和知识内容使用 `@Version` 乐观锁。 - **Rationale**:创建需要处理网络重试,更新需要避免管理员或企业多端并发覆盖。 - **Alternatives**:所有写接口只依赖数据库唯一键;所有接口都使用请求幂等键。 - **Consequences**:客户端创建时需保存幂等键;更新时必须先取得当前版本。 ### Decision 4: 公司知识摘要只使用公开评价并执行小样本保护 - **Choice**:只聚合 `PUBLISHED` 且审核为 `AUTO_APPROVED/APPROVED` 的评价;少于 3 条不返回均值。 - **Rationale**:与现有公开评价语义一致,降低小样本隐私和误导风险。 - **Alternatives**:聚合全部评价;无阈值返回所有均值。 - **Consequences**:新企业或评价较少企业只能看到基础资料与评价数量,不显示汇总分数。 ### Decision 5: 学生访问不绑定 Offer - **Choice**:任何 STUDENT 可按公开存在的 `companyId` 读取清单和知识,完成状态只属于当前学生。 - **Rationale**:产品没有要求入职资格校验,企业基础信息和通用清单也不属于敏感业务数据;此设计避免跨模块状态耦合。 - **Alternatives**:必须存在 ACCEPTED Offer;必须存在投递记录。 - **Consequences**:后续若要增加资格门槛,需新增权限规则和兼容测试,不改变现有路径。 ## Error Codes 新增 97xx: ```text 9701 ONBOARDING_CHECKLIST_ITEM_NOT_FOUND 9702 ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE 9703 ONBOARDING_ITEM_LIMIT_EXCEEDED 9704 ONBOARDING_IDEMPOTENCY_CONFLICT 9705 ONBOARDING_PROGRESS_CONCURRENT_UPDATE 9706 ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE ``` 复用 `FORBIDDEN`、`UNAUTHORIZED`、`BAD_REQUEST`、`COMPANY_NOT_FOUND`。 ## Risks and Verification | Risk | Verification | |---|---| | V38/V39 与 MySQL/H2 类型不一致 | 迁移集成测试检查三张表、唯一键、外键、种子数量与排序 | | 企业跨租户访问条目 | Service/MockMvc 测试使用两个企业,断言跨企业返回 404 且数据未变化 | | 重复创建产生重复项 | 持久化测试断言相同 idempotencyKey 只有一行;不同请求体断言 9704 | | 并发更新静默覆盖 | 持久化测试和乐观锁冲突测试断言 9702/9706 | | 完成状态重复或丢失 | 同一学生并发/重复 PUT 测试断言唯一记录、完成率和 completedAt 正确 | | 评价聚合暴露小样本或内部字段 | DTO 白名单测试、少于 3 条测试、无知识回退测试 | | 公司知识内容注入 HTML | 请求字段按纯文本存储和返回;长度与内容验证测试 | | 既有模块回归 | 扩展评价/企业相关测试后执行 `mvn clean verify`,并与 baseline 对比路径数 |