# V42 企业人才邀约与授权沟通——编码执行指令 开始实现 V42「企业人才邀约与授权沟通」。 权威设计文档: ```text C:\Users\15642\Desktop\zhilai\docs\v42-enterprise-talent-invitation-design.md ``` 请先完整阅读权威设计文档,再核查现有 V41 人才发现、V39 岗位画像、企业认证、账号注销、屏蔽关系、消息会话、审计及安全配置代码。设计中的 10 项产品决策已经负责人确认,禁止自行扩大或改变范围。 ## 一、开始前只读核查 先执行并记录: ```powershell git status --short git diff --cached --name-only git log -5 --oneline ``` 当前 Git 基线应包含: ```text 5cc2c86 feat: deliver V41 enterprise talent discovery 2698d06 feat: deliver V40 company review replies 55e3975 feat: deliver V39 job recommendations ``` 要求: 1. 确认当前最新 Flyway 是 V39;V42 使用 `V40__enterprise_talent_invitations.sql`。 2. 确认 V41 仅有 2 个 path / 3 个 operation,当前 OpenAPI 为 161 paths / 199 operations。 3. 确认 `CandidateKeyGenerator` 的域隔离串、日期桶、HMAC 算法和 URL-safe 无 padding 编码。 4. 确认 `TalentDiscoveryCandidateSource` 的资格过滤、同企业历史投递排除、账号注销排除和批量查询能力。 5. 确认现有 MessagingService 对企业—学生 DIRECT 会话要求 MATCHED 投递,不能为 V42 全局取消该守卫。 6. 确认 conversation、conversation_members、屏蔽关系、公司 owner、岗位状态、岗位画像状态的真实字段和仓库方法。 7. 确认现有审计服务中“成功审计”和“拒绝型安全审计”的事务语义。 8. 确认以下路径不存在: - `POST /companies/me/jobs/{jobId}/talent-invitations` - `GET /companies/me/talent-invitations` - `GET /companies/me/talent-invitations/{invitationId}` - `GET /profile/me/talent-invitations` - `GET /profile/me/talent-invitations/{invitationId}` - `POST /profile/me/talent-invitations/{invitationId}/decision` 如果设计文档与真实代码存在关键冲突,立即停止并报告;不得自行改接口、迁移号、状态机、错误码或隐私边界。 不得 reset、restore、checkout、删除或覆盖既有未跟踪文件;不得执行 `git add -A`、commit 或 push。 ## 二、硬性业务边界 1. 企业只能使用 V41 当前返回的 opaque `candidateKey` 创建邀约,绝不接收 `studentUserId`。 2. candidateKey 不可逆;服务端必须在当前合格候选池中重新生成 key 并使用常量时间比较匹配。 3. 只有已认证 COMPANY、自己的 PUBLISHED 岗位、APPROVED 且 enabled 的岗位画像可以创建邀约。 4. 学生关闭人才发现、账号进入注销限制、与该企业已有任意历史投递、推荐信号全部失效时,不可创建邀约。 5. 只有学生 ACCEPT 后才建立 `TALENT_INVITATION` 站内会话。 6. REJECTED、EXPIRED 不创建会话。 7. 企业不得看到学生身份、联系方式、简历、测评明细、候选评分或 fitScore。 8. 本版不做企业撤回/编辑/删除、批量或自动邀约、联系方式申请、短信邮件、RabbitMQ 通知、ADMIN 审核、邀约曝光历史或 AI 文案。 9. 不修改 V1–V39;不改变 V35–V41 既有业务语义。 10. 不写 `candidate_dimension_scores`,不新增 feature flag。 11. 不启动或停止 Docker,不做真实 MySQL 黑盒。 12. 不提交、不推送、不进入 V43。 ## 三、Flyway V40 新增: ```text mydreamjob-server/src/main/resources/db/migration/V40__enterprise_talent_invitations.sql ``` 新增两张表。 ### 3.1 talent_invitations 至少包含: - `id BIGINT AUTO_INCREMENT PRIMARY KEY` - `company_id BIGINT NOT NULL` - `job_id BIGINT NOT NULL` - `student_user_id BIGINT NOT NULL` - `candidate_key_digest CHAR(64) NOT NULL` - `message VARCHAR(300) NOT NULL` - `status VARCHAR(20) NOT NULL` - `idempotency_key VARCHAR(64) NOT NULL` - `active_key VARCHAR(200) NULL` - `conversation_id BIGINT NULL` - `expires_at DATETIME NOT NULL` - `decided_at DATETIME NULL` - `version BIGINT NOT NULL DEFAULT 0` - `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` - `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` 约束: - FK company/job/student/conversation; - `CHECK(status IN ('PENDING','ACCEPTED','REJECTED','EXPIRED'))`; - `UNIQUE(company_id,idempotency_key)`; - `UNIQUE(active_key)`; - 建议 `UNIQUE(conversation_id)`,NULL 可重复; - PENDING 时 `active_key=companyId:jobId:studentUserId`,终态必须置 NULL; - 索引 `(student_user_id,status,created_at)`、`(company_id,status,created_at)`、`(job_id,status,created_at)`、`(status,expires_at)`; - 不保存原始 candidateKey,只保存 SHA-256 十六进制摘要。 ### 3.2 talent_invitation_history 至少包含: - id、invitation_id、action、actor_user_id、created_at; - action 仅 `CREATED/ACCEPTED/REJECTED/EXPIRED`; - FK invitation,`ON DELETE CASCADE`; - 索引 `(invitation_id,created_at)`; - 不保存 message、candidateKey、studentUserId、联系方式、测评或评分。 Flyway 必须兼容 MySQL 8 与 H2 MODE=MySQL,不修改已执行迁移。 ## 四、实体、仓库与内部组件 建议新增包 `com.mydreamjob.talentinvitation`,至少包含: - `TalentInvitation` - `TalentInvitationHistory` - 两个 Repository - `TalentInvitationCandidateResolver` - `TalentInvitationConversationService` - `TalentInvitationExpiryService` - `TalentInvitationService` - `TalentInvitationController` - 白名单 DTO 要求: - invitation 使用 JPA `@Version`; - 状态和历史 action 使用字符串枚举; - Entity 不得直接返回 HTTP; - Repository 支持带 companyId/studentUserId 的归属查询、分页、条件状态更新、幂等查询、活跃/冷却/每日限额统计和批量过期; - 列表批量加载岗位/公司/会话信息,不得逐条 findById; - Controller 不写 SQL、不接收 companyId/studentUserId/ownerUserId/conversationId。 ## 五、candidateKey 安全解析 `TalentInvitationCandidateResolver` 必须: 1. 单次请求生成一个 `capturedNow`; 2. 完成企业、岗位、画像门禁; 3. 复用 V41 候选资格和批量数据源,获取该岗位当前全部合格候选; 4. 使用 V41 完全相同的域隔离、companyId、jobId、studentUserId 和 `yyyy-MM-dd` 日期桶重新生成 key; 5. 使用 `MessageDigest.isEqual` 做常量时间字节比较; 6. 必须恰好匹配一个候选人;零个或异常重复均返回统一的候选不可邀约; 7. 解析成功后的 studentUserId 只能留在服务内部和数据库 FK 中,不得写日志、审计 details 或企业 DTO; 8. 禁止添加 candidateKey→学生详情接口,禁止普通 Base64 解码、可逆加密或字符串拼接; 9. 禁止读取 assessment/ability answers、attempt questions、question feedback、candidate scoring 表。 无效 key、授权关闭、注销限制、历史投递、信号失效对企业使用相同安全错误,不解释具体原因。 ## 六、接口与权限 新增 6 paths / 6 operations: ```text POST /companies/me/jobs/{jobId}/talent-invitations GET /companies/me/talent-invitations GET /companies/me/talent-invitations/{invitationId} GET /profile/me/talent-invitations GET /profile/me/talent-invitations/{invitationId} POST /profile/me/talent-invitations/{invitationId}/decision ``` ### 6.1 创建请求 ```json { "candidateKey": "opaque-url-safe-value", "message": "我们正在招聘后端开发实习生,欢迎了解岗位详情。", "idempotencyKey": "3c989a31-6774-4520-9700-e58416cc1845" } ``` 校验: - candidateKey 非空且长度不超过 128; - message trim 后 1–300 字符; - message 禁止手机号、邮箱、URL、QQ/微信等外部联系方式;复用现有安全规则的公共能力,禁止在 Controller 复制正则; - idempotencyKey 为 UUID,最长 64; - jobId > 0;分页 page>=0、size 1–50。 ### 6.2 学生决策请求 ```json { "decision": "ACCEPT", "expectedVersion": 0 } ``` decision 仅 ACCEPT/REJECT,expectedVersion 必填且必须匹配。 权限矩阵: - 企业接口:仅已认证 COMPANY,且仅本公司;STUDENT/ADMIN 403/2002,匿名 401; - 学生接口:仅本人 STUDENT;COMPANY/ADMIN 403/2002,匿名 401; - 跨公司岗位和邀约统一 403/2002; - 他人学生邀约统一按不可见处理,不能泄露存在性。 ## 七、状态、过期、冷却与限额 状态机: ```text PENDING -> ACCEPTED PENDING -> REJECTED PENDING -> EXPIRED ``` - 三个终态不可再次改变; - 默认有效期 7 天;`expiresAt <= capturedNow` 已过期; - 读取和决策必须支持惰性过期;调度器只能改善及时性,不能承担正确性; - 同企业同学生任何终态后 30 天内不能再次邀约; - 每公司每岗位每天最多 20 次成功创建; - 每公司每天最多 100 次成功创建; - 所有限制使用数据库查询和索引,不依赖单机内存; - 并发创建由 `UNIQUE(active_key)` 兜底; - 状态决策采用 expectedVersion + status + expiresAt 条件更新; - 并发 ACCEPT/REJECT/EXPIRE 只能一个胜者,无 500。 屏蔽关系:学生点击 ACCEPT 时若双方任一方向存在屏蔽,返回 `403/2002`,邀约保持 PENDING,不泄露谁屏蔽谁。 ## 八、幂等与事务 创建: - 同 companyId、idempotencyKey、jobId、candidateKeyDigest、message 返回原邀约; - 同 companyId、idempotencyKey 但业务内容不同返回 409/10207; - 同公司、同岗位、同学生已有 PENDING 返回 409/10203; - 唯一键异常后不得在污染的 Hibernate Session 中继续查询;必要时使用 `REQUIRES_NEW` writer 和独立定判事务。 决策: - 同目标终态幂等重放可返回原结果; - 已是另一终态或版本冲突返回 409; - 只有条件更新胜者写 history 和 audit; - ACCEPT 必须在单一事务内完成状态、activeKey 清空、conversation、members、history、audit;任一步失败整体回滚; - 重放和并发不得重复创建会话、成员、历史或审计。 ## 九、接受后站内会话 - conversation.type=`TALENT_INVITATION`; - 成员仅企业 owner user 与当前学生; - 通过内部服务创建,绝不开放传 participantUserId 的 V42 公网入口; - 每份 ACCEPTED invitation 只对应一个 conversation; - 保留现有 DIRECT 会话 `MATCH_REQUIRED_FOR_CONVERSATION` 规则;只允许已 ACCEPTED 的 V42 内部流程创建 TALENT_INVITATION 会话; - 会话标题使用岗位标题安全模板; - 不自动把企业自由文案写成首条消息; - 后续发消息继续遵守成员、屏蔽和内容规则。 ## 十、DTO 与脱敏 企业 DTO 只允许: - invitationId、jobId、jobTitle; - candidateLabel=`开放人才候选人`; - message、status、createdAt、expiresAt、decidedAt; - conversationId 仅 ACCEPTED 后返回; - 默认不回显 candidateKey。 学生 DTO 可包含企业公开名称、岗位公开信息、message、状态、期限、conversationId。 对企业和学生真实序列化 JSON 断言不得包含: ```text studentUserId, ownerUserId, candidateId, phone, email, resumeUrl, attemptId, resultId, answerKey, scoringRule, questionCode, competencyTag, dimensionScores, overallScore, percentile, fitScore, candidateKeyDigest, activeKey, password, token, secret ``` ## 十一、错误码 在 ErrorCode 新增并同步 GlobalExceptionHandler: - 10201 TALENT_INVITATION_NOT_FOUND → 404 - 10202 TALENT_INVITATION_CANDIDATE_UNAVAILABLE → 409 - 10203 TALENT_INVITATION_ALREADY_ACTIVE → 409 - 10204 TALENT_INVITATION_INVALID_STATE → 409 - 10205 TALENT_INVITATION_EXPIRED → 409 - 10206 TALENT_INVITATION_CONCURRENT_UPDATE → 409 - 10207 TALENT_INVITATION_IDEMPOTENCY_CONFLICT → 409 - 10208 TALENT_INVITATION_CONTENT_INVALID → 400 - 10209 TALENT_INVITATION_RATE_LIMITED → 429 不得新增其他 V42 错误码,不得改变既有错误码含义。跨角色与屏蔽复用 2002,基础参数非法复用 1001。 ## 十二、审计与隐私 新增动作: - TALENT_INVITATION_CREATED - TALENT_INVITATION_ACCEPTED - TALENT_INVITATION_REJECTED - TALENT_INVITATION_EXPIRED - TALENT_INVITATION_CREATE_REJECTED 成功审计 details 仅允许 invitationId、jobId、status、requestId。禁止记录 candidateKey/digest、studentUserId、message、联系方式、简历、测评、评分、IP/UA 明文或凭据。 拒绝型安全审计沿用独立事务,内容同样不得包含 candidateKey 或学生 ID。每次真实状态变化最多一条 history 和一条对应成功审计。 ## 十三、查询与 N+1 - 企业列表按 companyId 分页;学生列表按 studentUserId 分页; - 排序 `createdAt DESC, id DESC`; - 当前页岗位、公司、会话信息批量读取; - 不逐邀请查询 Job/Company/User/Conversation; - 详情查询直接绑定归属条件; - 批量过期固定批次条件更新; - 测试用 Spy/统计断言查询次数不随 items 数线性增长。 ## 十四、必须新增测试 至少新增: 1. `TalentInvitationCandidateResolverTest` - 正确、篡改、过期日期桶 key; - 常量时间比较; - 授权/注销/历史投递/信号变化后的重新校验; - 其他企业投递不误排; - 无答案表、题目表、候选评分查询; - 批量查询无 N+1。 2. `TalentInvitationServiceTest` - 创建、文案安全、幂等和冲突; - 活跃唯一、30 天冷却、20/100 日限额边界; - 惰性过期; - ACCEPT 唯一会话、REJECT/EXPIRE 无会话; - 并发创建和并发决策无 500; - 重放不重复 history/audit/conversation; - 屏蔽关系保持 PENDING。 3. `TalentInvitationControllerTest` - COMPANY/STUDENT/ADMIN/匿名和跨租户矩阵; - 参数校验; - 企业/学生真实 JSON 脱敏; - 不存在详情解锁、联系方式或直接会话接口。 4. `TalentInvitationPersistenceIntegrationTest` - V40 迁移、FK、CHECK、UNIQUE、@Version; - activeKey 终态释放; - conversation 唯一; - history 不含敏感列; - 并发创建/决策; - 列表 N+1; - candidate_dimension_scores 不变。 5. 回归测试 - V41 推荐/授权; - V39 岗位画像; - V40 评价回复; - DIRECT 会话仍要求 MATCHED; - 只有 ACCEPTED 邀约可创建 TALENT_INVITATION 会话; - 屏蔽与消息权限不变。 ## 十五、SecurityConfig、OpenAPI 和文档 SecurityConfig 仅新增: - `/companies/me/**/talent-invitations/**` → COMPANY; - `/profile/me/talent-invitations/**` → STUDENT。 路径匹配必须覆盖实际 6 个 operation,但不能误放宽其他 `/companies/me/**` 或 `/profile/me/**` 接口。 新增: - `docs/enterprise-talent-invitation-api.md` 更新: - `docs/frontend-integration.md` - `docs/backend-development-roadmap.md` - `docs/v42-enterprise-talent-invitation-design.md` - `docs/openapi-rc.json` - `docs/verify_openapi.py` OpenAPI 目标:从 161/199 增加 6 paths / 6 operations,预计 **167 paths / 205 operations**,以实际结果为准。 校验脚本必须断言: - V42 恰好 6 paths / 6 operations; - POST 请求均有非空 requestBody schema; - 6 个成功响应均有非空 schema; - 企业/学生 schema 不含敏感字段; - 不存在候选详情、联系方式解锁、企业直接建会话、撤回/编辑/删除/批量邀约接口; - OpenAPI 总数与实际一致。 ## 十六、自动化验证命令 依次执行并记录真实结果: ```powershell mvn -Dtest="*TalentInvitation*Test,*TalentDiscovery*Test,*Messaging*Test" test mvn test mvn clean verify python docs/verify_openapi.py ``` 若 `clean verify` 因运行中的 JAR 锁定 target 失败,不得结束未知进程;只报告 PID、端口和阻塞原因,等待负责人确认。 ## 十七、最终停止条件 完成代码、测试、OpenAPI 和文档后立即停止: - 不启动/停止 Docker; - 不做真实 MySQL 黑盒; - 不提交或推送 Git; - 不执行 `git add -A`; - 不删除未跟踪文件; - 不进入 V43。 最终报告必须包含: 1. 精确修改文件; 2. Flyway V40 在测试环境的执行情况; 3. 实际测试数、Failures、Errors、Skipped; 4. OpenAPI 实际 paths/operations; 5. candidateKey 重算匹配与失效重新校验证据; 6. 7 天过期、30 天冷却、20/100 限额结果; 7. 并发创建/决策及会话唯一性; 8. 企业/学生权限矩阵; 9. 完整 JSON、审计、history 脱敏结果; 10. N+1 查询证明; 11. DIRECT 与 TALENT_INVITATION 会话规则回归; 12. 是否写 `candidate_dimension_scores`; 13. 是否修改 V1–V39; 14. 是否执行 Docker/MySQL 黑盒; 15. 当前 git status; 16. 明确说明未执行 `git add -A`、commit、push。 任何失败或 SKIP 必须如实报告,不得伪造 PASS。