You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

17 KiB

V42 企业人才邀约与授权沟通——编码执行指令

开始实现 V42「企业人才邀约与授权沟通」。

权威设计文档:

C:\Users\15642\Desktop\zhilai\docs\v42-enterprise-talent-invitation-design.md

请先完整阅读权威设计文档,再核查现有 V41 人才发现、V39 岗位画像、企业认证、账号注销、屏蔽关系、消息会话、审计及安全配置代码。设计中的 10 项产品决策已经负责人确认,禁止自行扩大或改变范围。

一、开始前只读核查

先执行并记录:

git status --short
git diff --cached --name-only
git log -5 --oneline

当前 Git 基线应包含:

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

新增:

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:

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 创建请求

{
  "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 学生决策请求

{
  "decision": "ACCEPT",
  "expectedVersion": 0
}

decision 仅 ACCEPT/REJECT,expectedVersion 必填且必须匹配。

权限矩阵:

  • 企业接口:仅已认证 COMPANY,且仅本公司;STUDENT/ADMIN 403/2002,匿名 401;
  • 学生接口:仅本人 STUDENT;COMPANY/ADMIN 403/2002,匿名 401;
  • 跨公司岗位和邀约统一 403/2002;
  • 他人学生邀约统一按不可见处理,不能泄露存在性。

七、状态、过期、冷却与限额

状态机:

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 断言不得包含:

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 总数与实际一致。

十六、自动化验证命令

依次执行并记录真实结果:

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。