From 92afb51902f86c31c2e469337fd94bfe8b1121a0 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 10:45:22 +0800 Subject: [PATCH] fix(aihr): preserve case contributor attribution --- .../dromara/aihr/service/AihrCaseService.java | 31 ++++++++++++++----- .../aihr/service/AihrCaseServiceTest.java | 8 +++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + scripts/demo-check.sh | 1 + 4 files changed, 33 insertions(+), 8 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 4b019288..4796849e 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 @@ -69,7 +69,7 @@ public class AihrCaseService { MediaRef media = storeSourceAudio(file); String caseId = "case-" + UUID.randomUUID(); CaseState state = new CaseState(caseId, fileName, scopedProject, transcript, tagsFromText(transcript), null, - media.ossId(), media.url()); + media.ossId(), media.url(), currentCaseOwner()); cases.put(caseId, state); saveCase(state, "已转写", caseTitle(List.of(), fileName), transcript); return new UploadResponse(caseId, fileName, transcript, state.tags(), "real-asr", media.url(), media.ossId()); @@ -85,7 +85,7 @@ public class AihrCaseService { CaseState state = requireCase(request == null ? null : request.caseId(), projectScopes); CaseSummary summary = organizeWithModel(state).orElseGet(() -> localSummary(state)); CaseState next = new CaseState(state.id(), state.fileName(), state.projectExtOrgId(), state.transcript(), summary.tags(), summary.summary(), - state.mediaOssId(), state.mediaUrl()); + state.mediaOssId(), state.mediaUrl(), state.owner()); cases.put(state.id(), next); saveCase(next, "已整理", caseTitle(summary.summary(), state.fileName()), summary.aiSummary()); return new OrganizeResponse(state.id(), summary.summary(), summary.tags(), summary.aiSummary(), summary.source()); @@ -104,7 +104,7 @@ public class AihrCaseService { primaryTag(state.tags()), "已入库", LocalDateTime.now().format(TIME_FORMAT), - "培训组", + firstNonBlank(state.owner(), "培训组"), detailSummary.isBlank() ? state.transcript() : detailSummary ); saveCase(state, "已入库", record.title(), record.summary()); @@ -233,7 +233,7 @@ public class AihrCaseService { jdbcTemplate.update(""" INSERT INTO aihr_case_record (tenant_id, case_id, file_name, project_ext_org_id, title, type, status, transcript, summary, media_oss_id, media_url, owner, create_time, update_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '培训组', now(), now()) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now()) ON DUPLICATE KEY UPDATE file_name = VALUES(file_name), project_ext_org_id = VALUES(project_ext_org_id), @@ -256,7 +256,8 @@ public class AihrCaseService { truncate(maskSensitiveText(state.transcript()), 4000), truncate(maskSensitiveText(summary), 1000), state.mediaOssId(), - truncate(state.mediaUrl(), 500) + truncate(state.mediaUrl(), 500), + firstNonBlank(state.owner(), "培训组") ); } @@ -403,7 +404,7 @@ public class AihrCaseService { ensureCaseTable(); List persisted = jdbcTemplate.query(""" SELECT case_id, file_name, project_ext_org_id, transcript, type - , media_oss_id, media_url + , media_oss_id, media_url, owner FROM aihr_case_record WHERE tenant_id = ? AND case_id = ? LIMIT 1 @@ -415,7 +416,8 @@ public class AihrCaseService { tagsFromText(firstNonBlank(rs.getString("type"), "现场案例")), null, rs.getObject("media_oss_id", Long.class), - firstNonBlank(rs.getString("media_url"), "") + firstNonBlank(rs.getString("media_url"), ""), + firstNonBlank(rs.getString("owner"), "培训组") ), tenantId(), normalizedCaseId); state = persisted.stream().findFirst().orElse(null); if (state != null) { @@ -535,6 +537,18 @@ public class AihrCaseService { return clean(AihrSensitiveText.forModel(text)); } + private String currentCaseOwner() { + return caseOwner(LoginHelper.getUsername()); + } + + static String caseOwner(String identity) { + String value = clean(identity); + if (value.matches("1[3-9]\\d{9}")) { + return "员工" + value.substring(value.length() - 4); + } + return firstNonBlank(value, "培训组"); + } + static boolean isSupportedAudioFile(String fileName) { String lower = clean(fileName).toLowerCase(Locale.ROOT); return SUPPORTED_AUDIO_EXTENSIONS.stream().anyMatch(lower::endsWith); @@ -557,7 +571,8 @@ public class AihrCaseService { List tags, List summary, Long mediaOssId, - String mediaUrl + String mediaUrl, + String owner ) { } 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 0d90d30d..11cb2fae 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 @@ -163,6 +163,14 @@ class AihrCaseServiceTest { assertFalse(AihrCaseService.isCaseContributorPosition("普通员工", "一线")); } + @Test + @Tag("dev") + void caseOwnerKeepsAttributionWithoutPersistingRawMobileNumber() { + assertEquals("员工1111", AihrCaseService.caseOwner("13900001111")); + assertEquals("hr_operator", AihrCaseService.caseOwner("hr_operator")); + assertEquals("培训组", AihrCaseService.caseOwner(" ")); + } + private static class CaseJdbcTemplate extends JdbcTemplate { private Object[] updateArgs = new Object[0]; diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 32ae722a..ea21c10c 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -176,3 +176,4 @@ - 2026-07-14 BRD G3 音频读取权限复核:发现移动端音频下载接口对已登录 SYS 用户未校验角色,普通后台账号理论上可枚举 `ossId` 读取训练录音;现复用 `superadmin/hr_operator` 门禁,APP 用户继续按本人/主管项目范围校验,其他身份返回 403。AIHR 全量测试 `70/70` 通过,未修改生产环境。 - 2026-07-14 BRD 4.4 每日三题资格补强:每日派题现在在组织快照存在 `hire_date` 列时按“入职日期至今不超过 3 个月”判断,边界按自然月且包含当天;旧 Demo 快照没有该列时继续使用“移动端训练记录少于 3 次”的兼容回退。正式试点仍需在外部组织同步落地并校验 `hire_date`,不能把回退口径当成正式新员工资格。 - 2026-07-14 BRD G3 候选资料审核状态复核:候选人资料审核接口此前在空请求体时默认写回“待审核”,可能覆盖已有“已通过/已驳回”状态;现要求审核动作显式提交三态之一,空值和未知状态直接拒绝,避免后台误操作回退审核结论。 +- 2026-07-14 BRD 案例署名复核:案例库已有负责人字段,但此前上传、整理和入库记录都硬编码为“培训组”,无法追溯案例贡献者;现复用既有 `owner` 字段保存首次上传身份,整理和入库阶段保留原署名,手机号身份仅显示员工末四位,避免把原始手机号写入案例展示。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index f7a40f3d..20733788 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -514,6 +514,7 @@ 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/AihrCaseService.java "position_level IN ('主管', '项目经理')" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "media_oss_id" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "storeSourceAudio" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "currentCaseOwner" contains mobile-uni/src/pages/user/cases/index.vue "case-audio" contains frontend/src/views/knowledge/cases.vue "case-audio" contains backend/script/sql/aihr_practice_mysql8.sql "aihr_case_record"