feat(aihr): trace sop feedback to search review

This commit is contained in:
2026-07-14 04:18:09 +08:00
parent 976b0b241c
commit 07ab28fd7b
9 changed files with 21 additions and 8 deletions
@@ -38,13 +38,13 @@ public final class AihrSopDto {
public record SopReviewResponse(Long id, Boolean usable, String status, String reviewedAt) { public record SopReviewResponse(Long id, Boolean usable, String status, String reviewedAt) {
} }
public record AnswerFeedbackRequest(String queryText, Long fragmentId, List<Long> fragmentIds, String verdict, String category, String position, String source) { public record AnswerFeedbackRequest(String queryText, Long fragmentId, List<Long> fragmentIds, String verdict, String category, String position, String source, Long reviewId) {
} }
public record AnswerFeedbackResponse(Integer saved, String verdict, String reviewStatus) { 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, 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 createTime, String updateTime) {
} }
public record AnswerFeedbackReviewResponse(Long id, String reviewStatus) { public record AnswerFeedbackReviewResponse(Long id, String reviewStatus) {
@@ -232,14 +232,15 @@ public class AihrSopSeedService {
for (Long fragmentId : fragmentIds) { for (Long fragmentId : fragmentIds) {
saved += jdbcTemplate.update(""" saved += jdbcTemplate.update("""
INSERT INTO aihr_knowledge_answer_feedback INSERT INTO aihr_knowledge_answer_feedback
(tenant_id, query_text, query_norm, fragment_id, verdict, category, position, source, review_status, create_time, update_time) (tenant_id, query_text, query_norm, fragment_id, verdict, category, position, source, review_id, review_status, create_time, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
query_text = VALUES(query_text), query_text = VALUES(query_text),
verdict = VALUES(verdict), verdict = VALUES(verdict),
category = VALUES(category), category = VALUES(category),
position = VALUES(position), position = VALUES(position),
source = VALUES(source), source = VALUES(source),
review_id = VALUES(review_id),
review_status = VALUES(review_status), review_status = VALUES(review_status),
update_time = VALUES(update_time) update_time = VALUES(update_time)
""", """,
@@ -251,6 +252,7 @@ public class AihrSopSeedService {
firstNonBlank(request.category(), "sop"), firstNonBlank(request.category(), "sop"),
firstNonBlank(request.position(), "生活顾问"), firstNonBlank(request.position(), "生活顾问"),
normalizeSearchSource(request.source()), normalizeSearchSource(request.source()),
request.reviewId(),
reviewStatus, reviewStatus,
now, now,
now now
@@ -264,7 +266,7 @@ public class AihrSopSeedService {
String reviewStatus = firstNonBlank(status, "待复核"); String reviewStatus = firstNonBlank(status, "待复核");
int limitValue = Math.max(1, Math.min(limit, 200)); int limitValue = Math.max(1, Math.min(limit, 200));
return jdbcTemplate.query(""" return jdbcTemplate.query("""
SELECT id, query_text, query_norm, fragment_id, verdict, category, position, source, review_status, create_time, update_time SELECT id, query_text, query_norm, fragment_id, verdict, category, position, source, review_id, review_status, create_time, update_time
FROM aihr_knowledge_answer_feedback FROM aihr_knowledge_answer_feedback
WHERE tenant_id = ? WHERE tenant_id = ?
AND (? = '' OR review_status = ?) AND (? = '' OR review_status = ?)
@@ -279,6 +281,7 @@ public class AihrSopSeedService {
rs.getString("category"), rs.getString("category"),
rs.getString("position"), rs.getString("position"),
rs.getString("source"), rs.getString("source"),
rs.getObject("review_id") == null ? null : rs.getLong("review_id"),
rs.getString("review_status"), rs.getString("review_status"),
formatTimestamp(rs.getTimestamp("create_time")), formatTimestamp(rs.getTimestamp("create_time")),
formatTimestamp(rs.getTimestamp("update_time")) formatTimestamp(rs.getTimestamp("update_time"))
@@ -1968,6 +1971,7 @@ public class AihrSopSeedService {
`category` varchar(100) DEFAULT NULL COMMENT '知识分类', `category` varchar(100) DEFAULT NULL COMMENT '知识分类',
`position` varchar(80) DEFAULT NULL COMMENT '岗位', `position` varchar(80) DEFAULT NULL COMMENT '岗位',
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源', `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
`review_id` bigint DEFAULT NULL COMMENT '对应SOP问答评审ID',
`review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态', `review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态',
`create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间',
@@ -1977,6 +1981,7 @@ public class AihrSopSeedService {
KEY `idx_aihr_answer_feedback_review` (`tenant_id`, `review_status`, `create_time`) KEY `idx_aihr_answer_feedback_review` (`tenant_id`, `review_status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 知识答案反馈'; ) 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`");
answerFeedbackTableReady = true; answerFeedbackTableReady = true;
} }
} }
@@ -231,6 +231,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_answer_feedback` (
`category` varchar(100) DEFAULT NULL COMMENT '知识分类', `category` varchar(100) DEFAULT NULL COMMENT '知识分类',
`position` varchar(80) DEFAULT NULL COMMENT '岗位', `position` varchar(80) DEFAULT NULL COMMENT '岗位',
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源', `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
`review_id` bigint DEFAULT NULL COMMENT '对应SOP问答评审ID',
`review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态', `review_status` varchar(30) DEFAULT '待复核' COMMENT '复核状态',
`create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+1 -1
View File
@@ -39,7 +39,7 @@
| 智能错题本 | 3.1、4.4、TechSpec P1 | 已补 `/api/aihr/mobile/practice/mistakes` 和员工“学”页错题本:按本人聚合低分/红线回合,展示原因、分数、再练状态,支持按现有归因筛选,并可跳已有 retry assignment;尚未做独立错题表和主管侧聚合 | 后续复用现有 session/assignment/annotation 补复盘详情关联和主管侧聚合,不新增独立错题事实表 | | 智能错题本 | 3.1、4.4、TechSpec P1 | 已补 `/api/aihr/mobile/practice/mistakes` 和员工“学”页错题本:按本人聚合低分/红线回合,展示原因、分数、再练状态,支持按现有归因筛选,并可跳已有 retry assignment;尚未做独立错题表和主管侧聚合 | 后续复用现有 session/assignment/annotation 补复盘详情关联和主管侧聚合,不新增独立错题事实表 |
| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API;仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 | | 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API;仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 |
| 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 | | 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 |
| RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;住宅核心流程覆盖率与可用率仍需正式评审数据。员工反馈目前按规范化问题+fragmentId去重,没有保存同一次搜索的 `reviewId`/答案版本,尚不足以做严格的答案版本追溯 | 先由业务确定问答评审与员工反馈的关联契约,再补评审批次/答案版本字段;在此之前按现有 query、fragment、source 口径统计,不宣称完成逐问答追溯 | | RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;本轮已让员工反馈保存同一次搜索的 `reviewId`,但尚未保存模型/提示词答案版本;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 下一步由业务确认答案版本口径,再把 `prompt_version`/检索批次纳入评审批次;先用当前 `reviewId + query + fragment` 做可追溯统计,不伪造模型版本 |
| 方言语音与多模态训练验收 | 4.3.2、4.3.3、L1-C | ASR/TTS、音频 OSS 留痕和文本降级链路已有;但当前没有四川话/粤语等方言供应商配置、样本集和准确率/响应时延证据,也没有把图像/视频训练输入纳入一期验收口径 | 先确定一期方言厂商和可接受指标,用真实短音频做至少一条方言路径回归;图像/视频输入另按场景与合规边界评审,不用通用文本链路冒充完成 | | 方言语音与多模态训练验收 | 4.3.2、4.3.3、L1-C | ASR/TTS、音频 OSS 留痕和文本降级链路已有;但当前没有四川话/粤语等方言供应商配置、样本集和准确率/响应时延证据,也没有把图像/视频训练输入纳入一期验收口径 | 先确定一期方言厂商和可接受指标,用真实短音频做至少一条方言路径回归;图像/视频输入另按场景与合规边界评审,不用通用文本链路冒充完成 |
### P2/P3:明确暂缓,不应混入当前修复 ### P2/P3:明确暂缓,不应混入当前修复
+1
View File
@@ -57,6 +57,7 @@ export type AnswerFeedbackItem = {
category: string; category: string;
position: string; position: string;
source: string; source: string;
reviewId?: number | null;
reviewStatus: string; reviewStatus: string;
createTime: string; createTime: string;
updateTime: string; updateTime: string;
+2 -1
View File
@@ -313,7 +313,8 @@ const feedback = async (verdict: AnswerFeedbackVerdict) => {
await submitAnswerFeedback({ await submitAnswerFeedback({
queryText: result.value.queryText || query.value, queryText: result.value.queryText || query.value,
fragmentIds, fragmentIds,
verdict verdict,
reviewId: result.value.reviewId
}); });
feedbackVerdict.value = verdict; feedbackVerdict.value = verdict;
uni.showToast({ title: verdict === 'down' ? '已进入复核' : '已记录', icon: 'none' }); uni.showToast({ title: verdict === 'down' ? '已进入复核' : '已记录', icon: 'none' });
+3 -1
View File
@@ -48,6 +48,7 @@ export const submitAnswerFeedback = (params: {
queryText: string; queryText: string;
fragmentIds: number[]; fragmentIds: number[];
verdict: AnswerFeedbackVerdict; verdict: AnswerFeedbackVerdict;
reviewId?: number | null;
}) => }) =>
apiRequest<{ saved: number }>({ apiRequest<{ saved: number }>({
url: '/api/knowledge/answer-feedback', url: '/api/knowledge/answer-feedback',
@@ -58,7 +59,8 @@ export const submitAnswerFeedback = (params: {
verdict: params.verdict, verdict: params.verdict,
category: 'sop', category: 'sop',
position: getSelectedPosition(), position: getSelectedPosition(),
source: 'mobile_uni_sop' source: 'mobile_uni_sop',
reviewId: params.reviewId ?? null
}, },
timeout: 15000 timeout: 15000
}); });
+1
View File
@@ -101,6 +101,7 @@ export interface KnowledgeSearchResponse {
records: unknown[]; records: unknown[];
keyPoints?: string[]; keyPoints?: string[];
cautions?: string[]; cautions?: string[];
reviewId?: number | null;
} }
export interface PositionSopTopic { export interface PositionSopTopic {
+2
View File
@@ -395,6 +395,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "/embeddings" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "/embeddings"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "review_id"
contains mobile-uni/src/services/knowledge.ts "reviewId: params.reviewId"
contains backend/script/sql/aihr_knowledge_mysql8.sql "embedding_json" contains backend/script/sql/aihr_knowledge_mysql8.sql "embedding_json"
contains frontend/src/views/knowledge/sop.vue "上传文档" contains frontend/src/views/knowledge/sop.vue "上传文档"
contains frontend/src/views/knowledge/sop.vue ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx" contains frontend/src/views/knowledge/sop.vue ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"