20 KiB
V40 企业评价管理与官方回复:详细设计
状态:实现已完成,专项/全量自动化测试、
mvn clean verify与真实 MySQL/Docker 黑盒均已通过;待建立 Git 检查点。本文定义并约束 V40 的后端交付边界。实现不会修改已有 V17/V18 的学生评价、管理员评价审核与防刷语义;不会新增测评、评分、推荐接口。
1. 目标、范围与现状
1.1 交付目标
让已认证企业能够:
- 查看自己名下岗位的评价及处理状态;
- 对每条评价提交一条官方回复;
- 在回复被拦截或管理员驳回后,查看安全的处理状态并修订后重新提交;
- 让公众仅在评价和官方回复都满足公开条件时看到该回复。
企业的“管理”在 V40 中是查看、筛选、回复和查看回复审核状态;不是删除、隐藏、改写或自行下架学生评价。企业针对评价真实性的申诉、投诉工单、运营统计和通知策略不纳入本版,留给后续独立模块。
1.2 已有能力与约束
| 项目 | 当前事实 | V40 处理 |
|---|---|---|
| 学生岗位评价 | job_reviews,同一学生对同一岗位唯一;已有评分、标签、八维评分 |
复用,不重建 |
| 评价审核 | V18:AUTO_APPROVED、PENDING、APPROVED、REJECTED,管理员已有评价审核接口 |
保持原链路;新增“回复审核”并与评价审核分开 |
| 企业权限 | 角色为 COMPANY;岗位归属可按公司 owner 与岗位 companyId 校验 |
所有企业接口均由 JWT 身份和岗位归属决定 |
| 审计 | 已有 audit_logs |
对回复生命周期写白名单审计 |
| 当前迁移 | 最新为 V37__job_recommendation_profiles.sql |
V40 若编码,新增 V38__company_review_replies.sql |
| 当前 OpenAPI 基线 | V39 后为 154 paths / 190 operations | 预计增至 159 paths / 196 operations,编码后实测为准 |
1.3 明确不做
- 不新增学生写评价、改评价、删评价接口;
- 不让企业删除、隐藏、编辑评分或审核学生评价;
- 不把评价人
studentUserId、手机号、邮箱、IP、User-Agent、内容哈希、风控分或内部原因返回给企业; - 不新增企业查看测评、候选人隐私数据或候选评分明细的能力;
- 不接入 AI 自动生成回复,不声称现有确定性规则为 AI;
- 不做评价申诉/争议工单、回复点赞、企业客服工单、通知推送或数据看板;
- 不改 V35–V39 迁移及语义,不写
candidate_dimension_scores。
2. 产品规则与关键决策
2.1 不可变规则
- 评价的作者始终是学生;企业对评价没有写、删、隐藏权限。
- 企业只能操作其本人拥有公司下的岗位评价;跨公司访问统一返回
403/2002,不通过“是否存在”泄露其他公司数据。 - 每条评价最多存在一条当前官方回复,且回复归属由评价所在岗位的公司确定,客户端不得传
companyId、jobId或评价作者 ID 来决定归属。 - 一条公开评价只在官方回复为
AUTO_APPROVED或APPROVED时返回officialReply。PENDING、REJECTED及内部审核信息绝不出现在公开接口。 - 企业不可删除官方回复。被
APPROVED或AUTO_APPROVED的回复不可编辑;被REJECTED的回复可使用乐观锁修订并重新进入预审。
2.2 回复状态机
无回复
└─ 企业创建 ─→ AUTO_APPROVED ───────────────→ 公众可见(终态)
或
PENDING ── 管理员同意 ───────→ APPROVED → 公众可见(终态)
└─ 管理员驳回 ─────→ REJECTED
└─ 企业修订并重提 ─→ AUTO_APPROVED / PENDING
- 预审无风险:
AUTO_APPROVED,可随公开评价直接展示。 - 命中敏感词、联系方式、URL、异常重复字符等已有确定性规则:
PENDING,仅管理员可审核。 PENDING只能由ADMIN变更为APPROVED或REJECTED。REJECTED的公司可提交修订版本;版本号递增、审核痕迹保留。AUTO_APPROVED与APPROVED为对外稳定文本。若未来需要更正,应另做“公开回复修订/版本化”需求,不能在 V40 中通过覆盖原文实现。
2.3 公开身份脱敏(V40 必做修正)
现有 JobReviewService.toView(...) 会在公开岗位评价载荷中输出 studentUserId。该字段会使企业或任意访客将评价与学生内部主键关联,不满足“评价对对方脱敏”的边界。
V40 编码必须将公开评价 DTO 改为白名单响应,移除 studentUserId,使用固定的 reviewerLabel: "匿名求职者"(或不返回该字段)。学生本人视图可保留“是否为我创建”的布尔信息,但不得把任何学生 ID 交给企业、公众或其他学生。此项是对已有公开契约的隐私修复;前端必须在联调时同步移除对 studentUserId 的依赖。
3. 数据模型与 Flyway
3.1 迁移
新增 V38__company_review_replies.sql。不改已执行的 V1–V37,也不通过 ddl-auto 补表。
3.2 company_review_replies
| 列 | 类型/约束 | 说明 |
|---|---|---|
id |
BIGINT PK | 回复 ID |
review_id |
BIGINT NOT NULL, UNIQUE, FK → job_reviews(id) ON DELETE CASCADE |
一条评价仅一个当前官方回复 |
company_id |
BIGINT NOT NULL, FK → companies(id) |
由服务端从岗位归属推导 |
content |
VARCHAR(500) NOT NULL | 官方回复正文,1–500 字符 |
moderation_status |
VARCHAR(20) NOT NULL | AUTO_APPROVED / PENDING / APPROVED / REJECTED |
moderation_reason |
VARCHAR(500) NULL | 内部安全原因码 JSON;不公开给企业或公众 |
moderation_score |
DECIMAL(5,2) NULL | 内部风控分,不公开 |
risk_level |
VARCHAR(20) NULL | 内部风险等级,不公开 |
reviewed_by_user_id |
BIGINT NULL FK → users(id) |
管理员审核人 |
reviewed_at |
DATETIME NULL | 最后一次管理员审核时间 |
version |
BIGINT NOT NULL DEFAULT 0 | JPA @Version;企业修订携带 expectedVersion |
created_at / updated_at |
DATETIME | 审计时间 |
约束与索引:
UNIQUE(review_id);CHECK(moderation_status IN ('AUTO_APPROVED','PENDING','APPROVED','REJECTED'));INDEX(company_id, moderation_status, updated_at)支撑企业列表;INDEX(moderation_status, created_at)支撑管理员待审列表。
3.3 company_review_reply_history
保留回复生命周期而非覆盖历史。
| 列 | 类型/约束 | 说明 |
|---|---|---|
id |
BIGINT PK | 历史记录 ID |
reply_id |
BIGINT NOT NULL FK → reply ON DELETE CASCADE | 对应回复 |
action |
VARCHAR(30) NOT NULL | CREATED、AUTO_APPROVED、PENDING、RESUBMITTED、APPROVED、REJECTED |
actor_user_id |
BIGINT NULL | 操作者;管理端可审计,企业/公众不返回 |
reason_codes |
VARCHAR(500) NULL | 安全原因码,不含敏感词原文 |
human_reason |
VARCHAR(500) NULL | 管理员驳回说明,仅向该公司展示,禁止包含评价人身份信息 |
created_at |
DATETIME NOT NULL | 时间 |
索引:INDEX(reply_id, created_at)。历史表不保存正文副本、IP、UA、学生 ID、题目或候选人数据。
3.4 审计日志
写入既有 audit_logs,详情严格白名单:
{
"reviewId": 120,
"replyId": 42,
"jobId": 9,
"status": "PENDING",
"requestId": "..."
}
动作:COMPANY_REVIEW_REPLY_CREATED、COMPANY_REVIEW_REPLY_RESUBMITTED、REVIEW_REPLY_AUTO_APPROVED、REVIEW_REPLY_APPROVED、REVIEW_REPLY_REJECTED。详情不得包含正文、评价作者、手机号、邮箱、IP、UA、哈希、token 或审核规则原文。
4. 接口契约
所有身份均从 SecurityContext / JWT subject 获取,不接收 userId、companyId、studentUserId 等身份参数。统一响应为既有 ApiResponse{code,message,data,requestId,timestamp};分页 page 从 0 开始、size 范围 1–50。
4.1 企业端(仅 COMPANY)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /companies/me/job-reviews |
分页查看本人公司岗位评价;支持 jobId、rating、replyStatus、createdFrom、createdTo 筛选 |
| GET | /companies/me/job-reviews/{reviewId} |
查看一条本人公司岗位评价及本人官方回复状态 |
| POST | /companies/me/job-reviews/{reviewId}/reply |
创建唯一官方回复;请求需 content、idempotencyKey |
| PUT | /companies/me/job-reviews/{reviewId}/reply |
仅允许修订 REJECTED 回复;请求需 content、expectedVersion |
企业列表/详情最小响应:
{
"reviewId": 120,
"job": { "id": 9, "title": "后端工程师" },
"rating": 4,
"content": "岗位描述较清晰。",
"tags": ["成长空间"],
"dimensionScores": { "SALARY": 4, "WORK_LIFE_BALANCE": 3 },
"reviewCreatedAt": "2026-09-04T10:00:00",
"reply": {
"id": 42,
"content": "感谢反馈,我们会继续优化沟通。",
"status": "PENDING",
"version": 0,
"updatedAt": "2026-09-04T10:05:00",
"rejectionReason": null
}
}
禁止返回:studentUserId、评价作者显示名、手机号、邮箱、IP、UA、内容哈希、审核分、评价审核内部原因、回复预审内部原因、管理员 ID。
idempotencyKey 必须为 UUID、最多 64 字符。同键同评价同正文返回已有回复;同键不同评价或正文返回 409/10106。创建时已有回复:PENDING / AUTO_APPROVED / APPROVED 返回 409/10102;REJECTED 必须走 PUT 重提。
4.2 管理员端(仅 ADMIN)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/review-replies/pending |
分页查看 PENDING 官方回复;可按 companyId、jobId、riskLevel、时间筛选 |
| POST | /admin/review-replies/{replyId}/decision |
APPROVE / REJECT;仅 PENDING 可决策;驳回时 reason 必填,1–500 字符 |
管理员响应可看安全原因码、风险等级、岗位/公司信息与回复正文,用于审核;仍不得返回评价学生的联系电话、邮箱、IP/UA 明文或密码/JWT。管理员决策重复提交或非 PENDING 状态返回冲突。
4.3 现有公开评价接口的兼容扩展
既有以下接口路径不新增操作:
GET /jobs/{jobId}/reviewsGET /jobs/{jobId}/reviews/{reviewId}
每条公开评价可新增可选 officialReply:
{
"content": "感谢反馈。",
"repliedAt": "2026-09-04T10:05:00"
}
仅当评价本身为 PUBLISHED 且评价审核状态为 AUTO_APPROVED / APPROVED,并且回复为 AUTO_APPROVED / APPROVED 时出现。该对象没有 replyId、companyId、审核人、状态、原因码和版本字段。无公开回复时字段省略或为 null,前端须兼容两种情况。
4.4 权限矩阵
| 行为 | STUDENT | COMPANY(所属) | COMPANY(非所属) | ADMIN | 匿名 |
|---|---|---|---|---|---|
| 公开评价/公开回复 | 可读 | 可读 | 可读 | 可读 | 可读 |
| 企业评价管理列表/详情 | 403/2002 | 可读 | 403/2002 | 403/2002 | 401 |
| 创建/修订官方回复 | 403/2002 | 可写 | 403/2002 | 403/2002 | 401 |
| 待审官方回复/决策 | 403/2002 | 403/2002 | 403/2002 | 可读/可决策 | 401 |
| 删除/隐藏学生评价 | 不新增该权限 | 永不允许 | 永不允许 | 继续沿用既有审核而非删除 | 不适用 |
5. 服务、查询与一致性设计
5.1 推荐组件
CompanyReviewManagementService:企业列表、详情、归属校验;CompanyReviewReplyService:创建、预审、修订、状态机、审计;AdminReviewReplyService:待审列表与管理员决策;CompanyReviewReplyModerationService:复用现有确定性文本安全规则的公共部分,不复制 Controller 中的正则或把回复伪称为 AI 审核;CompanyReviewReply、CompanyReviewReplyHistory及对应 Repository/DTO/Controller。
5.2 企业归属校验
服务端按当前用户找到企业,再由 review.jobId → job.companyId 比对。所有按 reviewId 的企业操作先检查岗位归属;不把“评价存在但不属于你”与“不存在”区分为可枚举信息。禁止以请求内 companyId 决定查询范围。
5.3 防 N+1 查询
企业列表采用“岗位评价页 + 批量补齐”策略:
- 分页查询当前公司岗位的评价,并 join/批量取得岗位标题;
- 对本页
reviewId一次性查询job_review_dimension_scores; - 对本页
reviewId一次性查询官方回复; - 在内存按 ID 映射组装。
不得逐条调用 findById、逐条查询维度分或回复。管理员待审列表同样使用 reply 与岗位/公司 JOIN FETCH 或投影查询。编码测试应证明查询次数与页面条目数无关。
5.4 并发与幂等
- 创建回复:数据库
UNIQUE(review_id)是最终防线;服务先处理同idempotencyKey重放,再依赖唯一键处理并发创建。 - 修订回复:请求带
expectedVersion,实体使用@Version;过期版本抛409/10104。 - 管理员决策:条件更新
WHERE id=? AND moderation_status='PENDING' AND version=?,影响行数为 0 时返回409/10103或10104;仅成功事务生成一次历史和审计。 - 任何唯一键或乐观锁异常都不得映射为 500 或泛化为错误的“已存在”。
6. 审核、内容安全和异常语义
6.1 文本规则
回复正文限制 1–500 字符,拒绝空白文本。复用已有规则中可泛化的:敏感词、手机号、QQ/微信引流、URL、异常重复字符。对企业回复不使用“学生快速提交”“跨学生重复评价”“学生批量刷评”一类依赖学生行为的规则。
规则命中时内容可以保存为 PENDING,但不得公开;管理员作出决定后才变为公开或驳回。原因码仅给管理员;企业在被驳回时只看到管理员填写的安全说明,不展示敏感词、内部阈值、风控分。
6.2 错误码(拟新增 101xx)
| 错误码 | HTTP | 含义 |
|---|---|---|
10101 REVIEW_REPLY_NOT_FOUND |
404 | 回复不存在,或不属于本企业时按权限策略处理 |
10102 REVIEW_REPLY_ALREADY_EXISTS |
409 | 评价已有当前官方回复 |
10103 REVIEW_REPLY_INVALID_STATE |
409 | 状态不允许创建修订/决策 |
10104 REVIEW_REPLY_CONCURRENT_UPDATE |
409 | 乐观锁或条件更新冲突 |
10105 REVIEW_REPLY_CONTENT_INVALID |
400 | 空白、超长或不符合字段校验 |
10106 REVIEW_REPLY_IDEMPOTENCY_CONFLICT |
409 | 同幂等键但评价/正文不同 |
10107 REVIEW_REPLY_DECISION_INVALID |
400 | 非法审核决策或驳回原因缺失 |
角色不匹配继续使用既有 FORBIDDEN/2002,未登录继续使用 UNAUTHORIZED/401,不增加重复错误码。
7. DTO 白名单
| 载荷 | 可以返回 | 绝不返回 |
|---|---|---|
| 企业评价列表/详情 | 评价文本、星级、标签、维度聚合、岗位公开名称、时间、该企业自身回复状态 | 学生 ID/联系方式、IP/UA、哈希、评价审核内部信息 |
| 企业回复 | 内容、状态、版本、更新时间、管理员驳回说明 | 审核人 ID、内部原因码、风控分、内部请求 ID |
| 管理员待审回复 | 回复文本、公司/岗位、风险等级与安全原因码 | 学生联系信息、IP/UA 明文、密码、JWT |
| 公开评价回复 | content、repliedAt |
ID、公司 ID、状态、版本、审核细节、所有评价人信息 |
任何 response DTO 禁止用 Entity 直接序列化;使用显式 DTO 或白名单 Map。对 officialReply、企业列表、管理员列表分别写 JSON 序列化脱敏断言。
8. 预期 OpenAPI 与前端协作
预计新增 5 paths / 6 operations:
GET /companies/me/job-reviews
GET /companies/me/job-reviews/{reviewId}
POST /companies/me/job-reviews/{reviewId}/reply
PUT /companies/me/job-reviews/{reviewId}/reply
GET /admin/review-replies/pending
POST /admin/review-replies/{replyId}/decision
预计 OpenAPI:154/190 → 159 paths / 196 operations。实际编码后必须由运行时 /v3/api-docs 与 docs/openapi-rc.json 对账,python docs/verify_openapi.py 通过后才可记录最终数字。
前端需要:
- 企业侧支持评价筛选、无回复/待审/已公开/已驳回状态;
- 已驳回回复展示安全说明与“修订后重新提交”入口;
- 公开岗位评价页仅在
officialReply存在时展示“企业官方回复”; - 移除任何对公开
studentUserId的依赖; - 不把企业端的审核状态、管理员页面或内部 reason codes 暴露给公众。
9. 测试与验收计划
9.1 单元测试
- 回复状态机:安全文本自动通过、风险文本待审、待审审批/驳回、仅驳回可重提、公开终态不可改;
- 内容校验:空白、501 字符、手机号/URL/敏感词等;
- 幂等:同键同体返回同一回复,同键异体
10106; - 乐观锁和并发创建:同评价并发只成功一条,不出现 500;
- 脱敏 mapper:企业/公开/管理员三个视图字段严格不同。
9.2 MockMvc 与权限测试
- COMPANY 可访问所属岗位评价并创建回复;
- STUDENT、ADMIN、未归属 COMPANY、匿名分别获得既定 403/401;
- 企业没有 DELETE 评价/回复的路由与权限;
- PUBLIC 仅看见审核通过的
officialReply; - 新旧公开评价 JSON 均不含
studentUserId、联系方式、内部审核字段。
9.3 H2 持久化测试
- V38 迁移可应用,FK、唯一约束、CHECK、
@Version生效; - 评价删除时回复与历史级联删除;
- 企业列表分页、批量维度分/回复加载,验证无 N+1;
- 管理员并发决策仅一条历史与一条审计落库;
- V17/V18 的学生评价审核与汇总回归通过。
9.4 Docker MySQL 黑盒
- 学生创建公开评价 → 所属企业列表/详情可见但无评价者 ID;
- 企业创建安全回复 → 公开评价出现
officialReply; - 企业创建风险回复 → 公众不可见,管理员待审、批准/驳回链路正确;
- 驳回后企业修订可重提;公开后不可覆盖或删除;
- 非归属企业、STUDENT、ADMIN、匿名越权验证;
- 同一评价并发创建、同键重试、管理员并发决策逐请求校验 HTTP 状态/业务码,无 500;
audit_logs只含白名单详情,不含正文、评价人或凭据;RabbitMQ 队列终态存在且 ready/unacked 均为 0;- 回归 V17/V18 公开评价、V33 举报屏蔽、V34 求职进度、V35/V36 测评、V37 风控、V38 评分、V39 推荐关键接口。
关闭门槛:专项测试与 mvn clean verify 全绿、OpenAPI 对账通过、真实 MySQL/Docker 黑盒 FAIL=0,关键权限与脱敏项不得 SKIP。
10. 回滚与兼容
- V40 只增加两张回复表、六个操作与公开响应的可选
officialReply;不改变学生评价的评分、审核或删除语义。 - 回滚采用新的前向迁移停用/删除 V40 回复表,禁止修改已执行的 V38;生产执行前另写并评审回滚迁移。
- 若企业回复功能需临时关闭,服务层应拒绝新建/重提而保留既有公开回复可读;是否加入独立 feature flag 需在编码前确认,默认不增加以免无业务需求引入额外配置。
11. 已确认的编码边界(2026-09-04)
负责人已确认以下方案,编码必须以此为准:
- 采用
V38__company_review_replies.sql,新增回复与回复历史两张表; - 每条评价仅有一条当前官方回复;公开后不可覆盖,驳回后可修订重提;
- 使用确定性预审:无风险自动公开,风险回复转管理员审核;
- V40 立即移除公开评价载荷中的
studentUserId,关闭既有隐私泄露; - 本版不做企业申诉/投诉评价、自动通知、运营统计或公开回复编辑历史;
- 接口范围锁定为 5 paths / 6 operations,OpenAPI 预计 159 paths / 196 operations;
- 不新增企业回复独立 feature flag。
V40 编码、测试、OpenAPI 更新与真实 MySQL/Docker 黑盒验收均已完成(PASS=62 FAIL=0 SKIP=0);仍须遵守模块完成后再建立独立 Git 检查点的治理要求。