diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java index 7b6dc1aa..9d48465e 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java @@ -106,7 +106,9 @@ public class AihrSopController { @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/answer-feedback/{id}/review") public R reviewAnswerFeedback(@PathVariable Long id) { - AnswerFeedbackReviewResponse response = sopSeedService.reviewAnswerFeedback(id); + LoginUser loginUser = LoginHelper.getLoginUser(); + String operator = loginUser == null ? "unknown" : loginUser.getUsername(); + AnswerFeedbackReviewResponse response = sopSeedService.reviewAnswerFeedback(id, operator); return response == null ? R.fail("答案反馈记录不存在") : R.ok(response); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java index 7d5486b3..b0cb42f1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java @@ -44,10 +44,10 @@ public final class AihrSopDto { public record AnswerFeedbackResponse(Integer saved, String verdict, String reviewStatus) { } - public record AnswerFeedbackItemResponse(Long id, String queryText, String queryNorm, Long fragmentId, String verdict, String category, String position, String source, Long reviewId, String reviewStatus, String createTime, String updateTime) { + public record AnswerFeedbackItemResponse(Long id, String queryText, String queryNorm, Long fragmentId, String verdict, String category, String position, String source, Long reviewId, String reviewStatus, String reviewer, String reviewedTime, String createTime, String updateTime) { } - public record AnswerFeedbackReviewResponse(Long id, String reviewStatus) { + public record AnswerFeedbackReviewResponse(Long id, String reviewStatus, String reviewer, String reviewedTime) { } public record SummaryCardResponse(String title, List steps, List objections, List scripts, List reminders) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index a036494b..bdd37012 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -271,7 +271,7 @@ public class AihrSopSeedService { String reviewStatus = firstNonBlank(status, "待复核"); int limitValue = Math.max(1, Math.min(limit, 200)); return jdbcTemplate.query(""" - SELECT id, query_text, query_norm, fragment_id, verdict, category, position, source, review_id, review_status, create_time, update_time + SELECT id, query_text, query_norm, fragment_id, verdict, category, position, source, review_id, review_status, reviewer, reviewed_time, create_time, update_time FROM aihr_knowledge_answer_feedback WHERE tenant_id = ? AND (? = '' OR review_status = ?) @@ -288,26 +288,35 @@ public class AihrSopSeedService { rs.getString("source"), rs.getObject("review_id") == null ? null : rs.getLong("review_id"), rs.getString("review_status"), + rs.getString("reviewer"), + formatTimestamp(rs.getTimestamp("reviewed_time")), formatTimestamp(rs.getTimestamp("create_time")), formatTimestamp(rs.getTimestamp("update_time")) ), tenantId(), reviewStatus, reviewStatus, limitValue); } public AnswerFeedbackReviewResponse reviewAnswerFeedback(Long id) { + return reviewAnswerFeedback(id, null); + } + + public AnswerFeedbackReviewResponse reviewAnswerFeedback(Long id, String operator) { if (id == null) { return null; } ensureAnswerFeedbackTable(); + Timestamp reviewedTime = Timestamp.valueOf(java.time.LocalDateTime.now()); int updated = jdbcTemplate.update(""" UPDATE aihr_knowledge_answer_feedback - SET review_status = '已复核', update_time = ? + SET review_status = '已复核', reviewer = ?, reviewed_time = ?, update_time = ? WHERE tenant_id = ? AND id = ? """, - Timestamp.valueOf(java.time.LocalDateTime.now()), + firstNonBlank(operator, "人工复核"), + reviewedTime, + reviewedTime, tenantId(), id ); - return updated == 0 ? null : new AnswerFeedbackReviewResponse(id, "已复核"); + return updated == 0 ? null : new AnswerFeedbackReviewResponse(id, "已复核", firstNonBlank(operator, "人工复核"), formatTimestamp(reviewedTime)); } public SummaryCardResponse summaryCard(SummaryCardRequest request) { @@ -1990,6 +1999,8 @@ public class AihrSopSeedService { `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源', `review_id` bigint DEFAULT NULL COMMENT '对应SOP问答评审ID', `review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态', + `reviewer` varchar(100) DEFAULT NULL COMMENT '复核人', + `reviewed_time` datetime DEFAULT NULL COMMENT '复核时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), @@ -1999,6 +2010,8 @@ public class AihrSopSeedService { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 知识答案反馈'; """); ensureColumn("aihr_knowledge_answer_feedback", "review_id", "ALTER TABLE aihr_knowledge_answer_feedback ADD COLUMN `review_id` bigint DEFAULT NULL COMMENT '对应SOP问答评审ID' AFTER `source`"); + ensureColumn("aihr_knowledge_answer_feedback", "reviewer", "ALTER TABLE aihr_knowledge_answer_feedback ADD COLUMN `reviewer` varchar(100) DEFAULT NULL COMMENT '复核人' AFTER `review_status`"); + ensureColumn("aihr_knowledge_answer_feedback", "reviewed_time", "ALTER TABLE aihr_knowledge_answer_feedback ADD COLUMN `reviewed_time` datetime DEFAULT NULL COMMENT '复核时间' AFTER `reviewer`"); answerFeedbackTableReady = true; } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java index dc4f296c..bcec7e64 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -117,6 +117,18 @@ public class AihrSopSeedServiceTest { assertTrue(code.contains("reviewSearch(id, request, operator)")); } + @Test + @Tag("dev") + public void answerFeedbackReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrSopController.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java"); + } + String code = Files.readString(source); + + assertTrue(code.contains("reviewAnswerFeedback(id, operator)")); + } + private static AihrSopSeedService.KnowledgeHit hit(Long fragmentId, String title) { return new AihrSopSeedService.KnowledgeHit(fragmentId, title, "sop", "", "doc-" + fragmentId, "片段内容", 1, 1.0); } diff --git a/backend/script/sql/aihr_practice_mysql8.sql b/backend/script/sql/aihr_practice_mysql8.sql index 233bc0aa..06a6961c 100644 --- a/backend/script/sql/aihr_practice_mysql8.sql +++ b/backend/script/sql/aihr_practice_mysql8.sql @@ -257,6 +257,8 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_answer_feedback` ( `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源', `review_id` bigint DEFAULT NULL COMMENT '对应SOP问答评审ID', `review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态', + `reviewer` varchar(100) DEFAULT NULL COMMENT '复核人', + `reviewed_time` datetime DEFAULT NULL COMMENT '复核时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index fa05c4d0..13f023d1 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -179,3 +179,4 @@ - 2026-07-14 BRD 案例署名复核:案例库已有负责人字段,但此前上传、整理和入库记录都硬编码为“培训组”,无法追溯案例贡献者;现复用既有 `owner` 字段保存首次上传身份,整理和入库阶段保留原署名,手机号身份仅显示员工末四位,避免把原始手机号写入案例展示。 - 2026-07-14 BRD 候选人关联完整性复核:候选人—员工关联接口此前只校验员工主体在职,允许给不存在的任意 `candidateId` 建立入职关联;现要求候选人必须来自内置候选档案、活动面试会话或当前租户已持久化面试记录,保留已有幂等关联和历史数据兼容。 - 2026-07-14 BRD SOP 审核留痕复核:SOP 问答评审接口此前接受请求体中的 `reviewer` 并直接入库,登录用户可伪造审核人;现由控制器从当前登录用户传入服务端操作人,请求体仅保留评审结论和备注,保留旧服务调用的默认“人工评审”兼容。 +- 2026-07-14 BRD 知识答案反馈复核:原先只更新 `review_status`,没有复核人和复核时间;现由服务端记录登录操作人到 `reviewer/reviewed_time`,管理端待复核表同步展示留痕。 diff --git a/frontend/src/api/aihr/sop.ts b/frontend/src/api/aihr/sop.ts index c8200453..98ea095d 100644 --- a/frontend/src/api/aihr/sop.ts +++ b/frontend/src/api/aihr/sop.ts @@ -61,6 +61,8 @@ export type AnswerFeedbackItem = { source: string; reviewId?: number | null; reviewStatus: string; + reviewer?: string | null; + reviewedTime?: string | null; createTime: string; updateTime: string; }; @@ -68,6 +70,8 @@ export type AnswerFeedbackItem = { export type AnswerFeedbackReviewResponse = { id: number; reviewStatus: string; + reviewer?: string | null; + reviewedTime?: string | null; }; export type SopUploadResponse = { diff --git a/frontend/src/views/knowledge/sop.vue b/frontend/src/views/knowledge/sop.vue index 742dfc41..353e920f 100644 --- a/frontend/src/views/knowledge/sop.vue +++ b/frontend/src/views/knowledge/sop.vue @@ -115,6 +115,8 @@ + +