From 6b84f1ab60851b0c5b5f64c2331bca54e9c95a3c Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 04:44:57 +0800 Subject: [PATCH] fix(aihr): reload persisted case sessions --- .../dromara/aihr/service/AihrCaseService.java | 23 +++++++++- .../aihr/service/AihrCaseServiceTest.java | 43 ++++++++++++++++++- docs/BRD_IMPLEMENTATION_AUDIT.md | 4 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java index 2a79bbb1..fce14b08 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java @@ -226,7 +226,28 @@ public class AihrCaseService { } private CaseState requireCase(String caseId) { - CaseState state = cases.get(clean(caseId)); + String normalizedCaseId = clean(caseId); + CaseState state = cases.get(normalizedCaseId); + if (state == null && !normalizedCaseId.isBlank()) { + ensureCaseTable(); + List persisted = jdbcTemplate.query(""" + SELECT case_id, file_name, project_ext_org_id, transcript, type + FROM aihr_case_record + WHERE tenant_id = '000000' AND case_id = ? + LIMIT 1 + """, (rs, rowNum) -> new CaseState( + rs.getString("case_id"), + firstNonBlank(rs.getString("file_name"), "case-audio"), + firstNonBlank(rs.getString("project_ext_org_id"), ""), + firstNonBlank(rs.getString("transcript"), ""), + tagsFromText(firstNonBlank(rs.getString("type"), "现场案例")), + null + ), normalizedCaseId); + state = persisted.stream().findFirst().orElse(null); + if (state != null) { + cases.put(normalizedCaseId, state); + } + } if (state == null) { throw new IllegalArgumentException("案例不存在或已过期,请重新上传语音"); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java index 2c773961..77006ee1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java @@ -5,12 +5,18 @@ import org.dromara.aihr.domain.AihrCaseDto.UploadResponse; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import org.springframework.mock.web.MockMultipartFile; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class AihrCaseServiceTest { @@ -36,7 +42,22 @@ class AihrCaseServiceTest { assertFalse(response.transcript().contains("2-304")); } - private static final class CaseJdbcTemplate extends JdbcTemplate { + @Test + @Tag("dev") + void curateCanReloadCaseAfterServiceRestart() { + PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate(); + AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate); + + var response = service.curate(new org.dromara.aihr.domain.AihrCaseDto.CurateRequest( + "persisted-case", "可复用培训案例", 1 + )); + + assertEquals(List.of("persisted-case"), response.selectedCaseIds()); + assertEquals("已入库", response.records().get(0).status()); + assertEquals("persisted-case", jdbcTemplate.lastCaseId); + } + + private static class CaseJdbcTemplate extends JdbcTemplate { private Object[] updateArgs = new Object[0]; @Override @@ -50,4 +71,24 @@ class AihrCaseServiceTest { return 1; } } + + private static final class PersistentCaseJdbcTemplate extends CaseJdbcTemplate { + private String lastCaseId; + + @Override + public List query(String sql, RowMapper rowMapper, Object... args) { + this.lastCaseId = String.valueOf(args[0]); + try { + ResultSet resultSet = mock(ResultSet.class); + when(resultSet.getString("case_id")).thenReturn("persisted-case"); + when(resultSet.getString("file_name")).thenReturn("case.webm"); + when(resultSet.getString("project_ext_org_id")).thenReturn("P1"); + when(resultSet.getString("transcript")).thenReturn("业主投诉后完成跟进"); + when(resultSet.getString("type")).thenReturn("投诉处理"); + return List.of(rowMapper.mapRow(resultSet, 0)); + } catch (SQLException e) { + throw new AssertionError(e); + } + } + } } diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index f51a6480..98fb4855 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -18,7 +18,7 @@ | 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位,需继续保留人工复核证据 | | SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 | | 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 主闭环已在位;低分再练、错题归因仍需更明确的用户可见闭环 | -| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,但当前仓库没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型;不能用假播放器宣称案例学习闭环完成 | +| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 | ## 3. 未完成项与优先级 @@ -38,7 +38,7 @@ | 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 已补 `GET /api/knowledge/position-sop` 和员工学习页状态提示:一期只确认生活顾问内置学习导航;尚无 `sop_applicability`、`onboard_task`、`qualification_gate` 正式数据闭环,不能作为上岗资格判断 | 先接正式岗位/项目与 SOP 适用范围数据,再补培训任务状态;资格 gating 必须由 HR 明确规则和证据来源后实现 | | 认证等级与激励规则可配置 | 4.5、10.2-A、TechSpec M6 | 成长激励页已有初/中/高级证据包和学分口径展示,但规则仍是前端硬编码;原“排行榜”开关无实际业务作用,已移除并明确标记只读;尚无 `cert_rule`/`incentive_rule` 的持久化、版本、角色权限和生效记录 | 先由 HR 确认学分来源、分值、防刷和初/中/高级条件,再设计规则表与管理员 CRUD;在规则契约明确前,不把静态展示称为可配置 | | 智能错题本 | 3.1、4.4、TechSpec P1 | 已补员工本人错题本和主管 `/api/aihr/mobile/practice/team` 内的 `mistakes` 聚合:按当前项目权限统计场景、归因、次数、影响人数、平均分和最近发生时间,员工端仍可跳已有 retry assignment;不新增独立错题事实表 | 继续用正式组织快照和试点窗口验证主管只看到本项目数据;后续可在真实错题量上来后补趋势/导出,不把本地演示样本当正式指标 | -| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API;仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 | +| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 已能从 `aihr_case_record` 恢复转写案例,避免后端重启后案例整理/入库失效;但仍没有稳定 media URL、学习点、主管点评和案例详情 API,仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 | | 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 | | RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;员工反馈保存同一次搜索的 `reviewId`,SOP 评审记录和搜索响应现在保留答案生成 `promptVersion`;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 后续由业务确认模型/检索批次的正式版本口径,再补检索批次与模型版本;当前先用 `reviewId + promptVersion + query + fragment` 做可追溯统计,不伪造正式可用率 | | 方言语音与多模态训练验收 | 4.3.2、4.3.3、L1-C | ASR/TTS、音频 OSS 留痕和文本降级链路已有;但当前没有四川话/粤语等方言供应商配置、样本集和准确率/响应时延证据,也没有把图像/视频训练输入纳入一期验收口径 | 先确定一期方言厂商和可接受指标,用真实短音频做至少一条方言路径回归;图像/视频输入另按场景与合规边界评审,不用通用文本链路冒充完成 |