From 7f3eadae416ba32812f05bb96c1fbba691e1ce84 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 14:02:00 +0800 Subject: [PATCH] fix(aihr): protect disabled assignment scenarios --- .../aihr/service/AihrPracticeSeedService.java | 3 ++- .../service/AihrPracticeSeedServiceTest.java | 22 +++++++++++++++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + scripts/demo-check.sh | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index cfd39d14..e5cebfa4 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -1906,6 +1906,7 @@ public class AihrPracticeSeedService { ensureAssignmentTable(); LocalDateTime now = LocalDateTime.now(); String scenarioId = firstNonBlank(request == null ? null : request.scenarioId(), "fee-parking"); + ensureScenarioEnabledForStart(scenarioId); ScenarioSeed scenario = resolveScenario(scenarioId, null); String traineeName = firstNonBlank(request == null ? null : request.traineeName(), extPartyId); // Assignment source is a server-owned classification. Client supplied values @@ -1917,7 +1918,7 @@ public class AihrPracticeSeedService { INSERT INTO aihr_practice_assignment (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, '待训练', ?, ?) - """, tenantId(), extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, createdAt, createdAt); + """, tenantId(), extPartyId, traineeName, scenario.id(), scenario.name(), source, reason, createdAt, createdAt); List rows = jdbcTemplate.query(""" SELECT id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, answer_text, answer_audio_url, answer_audio_oss_id, feedback, correct, score diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index 3f8dfaff..ff566282 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -118,6 +118,28 @@ public class AihrPracticeSeedServiceTest { assertTrue(method.contains("String source = \"manual\"")); assertFalse(method.contains("request.source()")); + assertTrue(method.contains("ensureScenarioEnabledForStart(scenarioId)")); + assertTrue(method.contains("scenario.id(), scenario.name(), source")); + assertFalse(method.contains("request.scenarioName()")); + } + + @Test + public void assignmentRejectsDisabledDatabaseScenario() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + when(jdbcTemplate.query(contains("SELECT enabled"), any(RowMapper.class), eq("000000"), eq("disabled-scene"))) + .thenAnswer(invocation -> mapOne(invocation, Map.of("enabled", "false"))); + AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); + + ServiceException error = assertThrows(ServiceException.class, () -> service.createAssignment(new PracticeAssignmentRequest( + "employee-a", + "员工甲", + "disabled-scene", + "伪造的场景名称", + "manual", + "主管派发专项训练" + ))); + + assertEquals("训练场景已停用,无法开始", error.getMessage()); } @Test diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 7451e521..da4871e5 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -271,3 +271,4 @@ - 2026-07-14 BRD 4.4 场景派发覆盖复核:数据库已有 12 个生活顾问场景,但移动端主管“指派专项”和“复盘后再练”此前只提供 3 个静态场景,导致其余已启用场景无法从主管入口使用;现改为 API 优先加载已启用场景,失败时保留 3 个本地兜底。BRD 所称 7–14 天专项训练营目前仍只有 `source='camp'` 标签,没有训练营计划、天数、每日任务、进度和完成规则,需 HR 确认内容与运营口径后再实现,不以标签冒充训练营闭环。 - 2026-07-14 BRD G3/M5 派发来源完整性复核:主管派发接口此前信任客户端 `source`,理论上可把手工派发伪装成 `daily`、`retry` 或 `camp`,污染每日题、低分再练和训练营统计;现将主管创建任务的来源固定为服务端 `manual`,`daily/retry` 仍只由各自系统路径生成。新增服务源码回归测试与 `demo-check` marker,未修改生产数据。 - 2026-07-14 BRD M1 场景启停边界复核:停用场景此前在新训练启动时可能因内置 seed 回退而继续可用,管理端“停用”不具备实际阻断效果;现仅在新开始训练前检查数据库场景的 `enabled` 状态,已停用场景明确拒绝,历史训练/复盘读取不受影响。新增服务源码回归测试与 `demo-check` marker,未修改生产数据。 +- 2026-07-14 BRD M1 派发场景状态与展示一致性复核:修复停用场景仍可通过直接派发接口进入待训练,以及客户端传入场景名称可与场景 ID 不一致的问题;主管派发现在复用启停校验,记录名称始终取服务端场景定义。历史任务读取不受影响,未修改生产数据。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 965a197f..838bcb5a 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -519,6 +519,7 @@ contains mobile-uni/src/pages/supervisor/review/index.vue "标记已复盘" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "source = 'retry'" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java 'String source = "manual"' contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "训练场景已停用,无法开始" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "scenario.id(), scenario.name(), source" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "isWithinNewHireWindow" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "allow-legacy-daily-drill-fallback" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "isLegacyDailyDrillFallbackEnabled"