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
要求:
- 确认当前最新 Flyway 是 V39;V42 使用
V40__enterprise_talent_invitations.sql。 - 确认 V41 仅有 2 个 path / 3 个 operation,当前 OpenAPI 为 161 paths / 199 operations。
- 确认
CandidateKeyGenerator的域隔离串、日期桶、HMAC 算法和 URL-safe 无 padding 编码。 - 确认
TalentDiscoveryCandidateSource的资格过滤、同企业历史投递排除、账号注销排除和批量查询能力。 - 确认现有 MessagingService 对企业—学生 DIRECT 会话要求 MATCHED 投递,不能为 V42 全局取消该守卫。
- 确认 conversation、conversation_members、屏蔽关系、公司 owner、岗位状态、岗位画像状态的真实字段和仓库方法。
- 确认现有审计服务中“成功审计”和“拒绝型安全审计”的事务语义。
- 确认以下路径不存在:
POST /companies/me/jobs/{jobId}/talent-invitationsGET /companies/me/talent-invitationsGET /companies/me/talent-invitations/{invitationId}GET /profile/me/talent-invitationsGET /profile/me/talent-invitations/{invitationId}POST /profile/me/talent-invitations/{invitationId}/decision
如果设计文档与真实代码存在关键冲突,立即停止并报告;不得自行改接口、迁移号、状态机、错误码或隐私边界。
不得 reset、restore、checkout、删除或覆盖既有未跟踪文件;不得执行 git add -A、commit 或 push。
二、硬性业务边界
- 企业只能使用 V41 当前返回的 opaque
candidateKey创建邀约,绝不接收studentUserId。 - candidateKey 不可逆;服务端必须在当前合格候选池中重新生成 key 并使用常量时间比较匹配。
- 只有已认证 COMPANY、自己的 PUBLISHED 岗位、APPROVED 且 enabled 的岗位画像可以创建邀约。
- 学生关闭人才发现、账号进入注销限制、与该企业已有任意历史投递、推荐信号全部失效时,不可创建邀约。
- 只有学生 ACCEPT 后才建立
TALENT_INVITATION站内会话。 - REJECTED、EXPIRED 不创建会话。
- 企业不得看到学生身份、联系方式、简历、测评明细、候选评分或 fitScore。
- 本版不做企业撤回/编辑/删除、批量或自动邀约、联系方式申请、短信邮件、RabbitMQ 通知、ADMIN 审核、邀约曝光历史或 AI 文案。
- 不修改 V1–V39;不改变 V35–V41 既有业务语义。
- 不写
candidate_dimension_scores,不新增 feature flag。 - 不启动或停止 Docker,不做真实 MySQL 黑盒。
- 不提交、不推送、不进入 V43。
三、Flyway V40
新增:
mydreamjob-server/src/main/resources/db/migration/V40__enterprise_talent_invitations.sql
新增两张表。
3.1 talent_invitations
至少包含:
id BIGINT AUTO_INCREMENT PRIMARY KEYcompany_id BIGINT NOT NULLjob_id BIGINT NOT NULLstudent_user_id BIGINT NOT NULLcandidate_key_digest CHAR(64) NOT NULLmessage VARCHAR(300) NOT NULLstatus VARCHAR(20) NOT NULLidempotency_key VARCHAR(64) NOT NULLactive_key VARCHAR(200) NULLconversation_id BIGINT NULLexpires_at DATETIME NOT NULLdecided_at DATETIME NULLversion BIGINT NOT NULL DEFAULT 0created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_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,至少包含:
TalentInvitationTalentInvitationHistory- 两个 Repository
TalentInvitationCandidateResolverTalentInvitationConversationServiceTalentInvitationExpiryServiceTalentInvitationServiceTalentInvitationController- 白名单 DTO
要求:
- invitation 使用 JPA
@Version; - 状态和历史 action 使用字符串枚举;
- Entity 不得直接返回 HTTP;
- Repository 支持带 companyId/studentUserId 的归属查询、分页、条件状态更新、幂等查询、活跃/冷却/每日限额统计和批量过期;
- 列表批量加载岗位/公司/会话信息,不得逐条 findById;
- Controller 不写 SQL、不接收 companyId/studentUserId/ownerUserId/conversationId。
五、candidateKey 安全解析
TalentInvitationCandidateResolver 必须:
- 单次请求生成一个
capturedNow; - 完成企业、岗位、画像门禁;
- 复用 V41 候选资格和批量数据源,获取该岗位当前全部合格候选;
- 使用 V41 完全相同的域隔离、companyId、jobId、studentUserId 和
yyyy-MM-dd日期桶重新生成 key; - 使用
MessageDigest.isEqual做常量时间字节比较; - 必须恰好匹配一个候选人;零个或异常重复均返回统一的候选不可邀约;
- 解析成功后的 studentUserId 只能留在服务内部和数据库 FK 中,不得写日志、审计 details 或企业 DTO;
- 禁止添加 candidateKey→学生详情接口,禁止普通 Base64 解码、可逆加密或字符串拼接;
- 禁止读取 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_NEWwriter 和独立定判事务。
决策:
- 同目标终态幂等重放可返回原结果;
- 已是另一终态或版本冲突返回 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 数线性增长。
十四、必须新增测试
至少新增:
-
TalentInvitationCandidateResolverTest- 正确、篡改、过期日期桶 key;
- 常量时间比较;
- 授权/注销/历史投递/信号变化后的重新校验;
- 其他企业投递不误排;
- 无答案表、题目表、候选评分查询;
- 批量查询无 N+1。
-
TalentInvitationServiceTest- 创建、文案安全、幂等和冲突;
- 活跃唯一、30 天冷却、20/100 日限额边界;
- 惰性过期;
- ACCEPT 唯一会话、REJECT/EXPIRE 无会话;
- 并发创建和并发决策无 500;
- 重放不重复 history/audit/conversation;
- 屏蔽关系保持 PENDING。
-
TalentInvitationControllerTest- COMPANY/STUDENT/ADMIN/匿名和跨租户矩阵;
- 参数校验;
- 企业/学生真实 JSON 脱敏;
- 不存在详情解锁、联系方式或直接会话接口。
-
TalentInvitationPersistenceIntegrationTest- V40 迁移、FK、CHECK、UNIQUE、@Version;
- activeKey 终态释放;
- conversation 唯一;
- history 不含敏感列;
- 并发创建/决策;
- 列表 N+1;
- candidate_dimension_scores 不变。
-
回归测试
- 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.mddocs/backend-development-roadmap.mddocs/v42-enterprise-talent-invitation-design.mddocs/openapi-rc.jsondocs/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。
最终报告必须包含:
- 精确修改文件;
- Flyway V40 在测试环境的执行情况;
- 实际测试数、Failures、Errors、Skipped;
- OpenAPI 实际 paths/operations;
- candidateKey 重算匹配与失效重新校验证据;
- 7 天过期、30 天冷却、20/100 限额结果;
- 并发创建/决策及会话唯一性;
- 企业/学生权限矩阵;
- 完整 JSON、审计、history 脱敏结果;
- N+1 查询证明;
- DIRECT 与 TALENT_INVITATION 会话规则回归;
- 是否写
candidate_dimension_scores; - 是否修改 V1–V39;
- 是否执行 Docker/MySQL 黑盒;
- 当前 git status;
- 明确说明未执行
git add -A、commit、push。
任何失败或 SKIP 必须如实报告,不得伪造 PASS。