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.
 
 
 

20 KiB

V40 企业评价管理与官方回复:详细设计

状态:实现已完成,专项/全量自动化测试、mvn clean verify 与真实 MySQL/Docker 黑盒均已通过;待建立 Git 检查点。

本文定义并约束 V40 的后端交付边界。实现不会修改已有 V17/V18 的学生评价、管理员评价审核与防刷语义;不会新增测评、评分、推荐接口。

1. 目标、范围与现状

1.1 交付目标

让已认证企业能够:

  1. 查看自己名下岗位的评价及处理状态;
  2. 对每条评价提交一条官方回复;
  3. 在回复被拦截或管理员驳回后,查看安全的处理状态并修订后重新提交;
  4. 让公众仅在评价和官方回复都满足公开条件时看到该回复。

企业的“管理”在 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 不可变规则

  1. 评价的作者始终是学生;企业对评价没有写、删、隐藏权限。
  2. 企业只能操作其本人拥有公司下的岗位评价;跨公司访问统一返回 403/2002,不通过“是否存在”泄露其他公司数据。
  3. 每条评价最多存在一条当前官方回复,且回复归属由评价所在岗位的公司确定,客户端不得传 companyId、jobId 或评价作者 ID 来决定归属。
  4. 一条公开评价只在官方回复为 AUTO_APPROVED 或 APPROVED 时返回 officialReply。PENDING、REJECTED 及内部审核信息绝不出现在公开接口。
  5. 企业不可删除官方回复。被 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}/reviews
  • GET /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 查询

企业列表采用“岗位评价页 + 批量补齐”策略:

  1. 分页查询当前公司岗位的评价,并 join/批量取得岗位标题;
  2. 对本页 reviewId 一次性查询 job_review_dimension_scores;
  3. 对本页 reviewId 一次性查询官方回复;
  4. 在内存按 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 通过后才可记录最终数字。

前端需要:

  1. 企业侧支持评价筛选、无回复/待审/已公开/已驳回状态;
  2. 已驳回回复展示安全说明与“修订后重新提交”入口;
  3. 公开岗位评价页仅在 officialReply 存在时展示“企业官方回复”;
  4. 移除任何对公开 studentUserId 的依赖;
  5. 不把企业端的审核状态、管理员页面或内部 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 黑盒

  1. 学生创建公开评价 → 所属企业列表/详情可见但无评价者 ID;
  2. 企业创建安全回复 → 公开评价出现 officialReply;
  3. 企业创建风险回复 → 公众不可见,管理员待审、批准/驳回链路正确;
  4. 驳回后企业修订可重提;公开后不可覆盖或删除;
  5. 非归属企业、STUDENT、ADMIN、匿名越权验证;
  6. 同一评价并发创建、同键重试、管理员并发决策逐请求校验 HTTP 状态/业务码,无 500;
  7. audit_logs 只含白名单详情,不含正文、评价人或凭据;RabbitMQ 队列终态存在且 ready/unacked 均为 0;
  8. 回归 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)

负责人已确认以下方案,编码必须以此为准:

  1. 采用 V38__company_review_replies.sql,新增回复与回复历史两张表;
  2. 每条评价仅有一条当前官方回复;公开后不可覆盖,驳回后可修订重提;
  3. 使用确定性预审:无风险自动公开,风险回复转管理员审核;
  4. V40 立即移除公开评价载荷中的 studentUserId,关闭既有隐私泄露;
  5. 本版不做企业申诉/投诉评价、自动通知、运营统计或公开回复编辑历史;
  6. 接口范围锁定为 5 paths / 6 operations,OpenAPI 预计 159 paths / 196 operations;
  7. 不新增企业回复独立 feature flag。

V40 编码、测试、OpenAPI 更新与真实 MySQL/Docker 黑盒验收均已完成(PASS=62 FAIL=0 SKIP=0);仍须遵守模块完成后再建立独立 Git 检查点的治理要求。