From 3a5a0df96f5ba6e8aed9c34673f48a3e6e8e9a33 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 13:58:02 +0800 Subject: [PATCH] fix(aihr): enforce disabled scenario gate --- .../aihr/service/AihrPracticeSeedService.java | 23 ++++++++++++++++++- .../service/AihrPracticeSeedServiceTest.java | 11 +++++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + scripts/demo-check.sh | 1 + 4 files changed, 35 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 6a340dde..cfd39d14 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 @@ -481,7 +481,9 @@ public class AihrPracticeSeedService { } public StartResponse start(StartRequest request) { - ScenarioSeed scenario = resolveScenario(resolveStartScenarioId(request), null); + String scenarioId = resolveStartScenarioId(request); + ensureScenarioEnabledForStart(scenarioId); + ScenarioSeed scenario = resolveScenario(scenarioId, null); RoundSeed firstRound = scenario.rounds().get(0); String sessionId = "seed-" + scenario.id() + "-" + System.currentTimeMillis(); String trainee = resolveTrainee(request, scenario); @@ -2146,6 +2148,25 @@ public class AihrPracticeSeedService { return assignedScenarioId; } + private void ensureScenarioEnabledForStart(String scenarioId) { + if (isBlank(scenarioId)) { + return; + } + try { + List enabledRows = jdbcTemplate.query(""" + SELECT enabled + FROM aihr_practice_scenario + WHERE tenant_id = ? AND scenario_code = ? + LIMIT 1 + """, (rs, rowNum) -> rs.getBoolean("enabled"), tenantId(), scenarioId.trim()); + if (!enabledRows.isEmpty() && !Boolean.TRUE.equals(enabledRows.get(0))) { + throw new ServiceException("训练场景已停用,无法开始"); + } + } catch (DataAccessException ignored) { + // A missing/unavailable scenario table keeps the existing local seed fallback. + } + } + private String resolveExtPartyId(StartRequest request, String trainee) { if (request == null || isBlank(request.extPartyId())) { return trainee; 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 b89b21e3..62370f25 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 @@ -223,6 +223,17 @@ public class AihrPracticeSeedServiceTest { assertEquals("训练会话不存在或无权访问", missing.getMessage()); } + @Test + public void startChecksScenarioEnabledBeforeSeedFallback() throws Exception { + String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java")); + int methodStart = source.indexOf("public StartResponse start(StartRequest request)"); + int methodEnd = source.indexOf("public TurnResponse turn", methodStart); + String method = source.substring(methodStart, methodEnd); + + assertTrue(method.contains("ensureScenarioEnabledForStart(scenarioId)")); + assertTrue(source.contains("训练场景已停用,无法开始")); + } + @Test public void createAssignmentRejectsOutOfProjectAssignee() { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 767f8f60..7451e521 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -270,3 +270,4 @@ - 2026-07-14 当前线上资源复核:生产根站、`/h5/` 和 `/prod-api/auth/tenant/list` 均返回 `200`;线上管理端 `index-CJZ3Ax3Z.js` SHA-256 为 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`,当前分支构建 `index-B3bSjqdi.js` SHA-256 为 `2ff13b11307631f9cd672247d6d08e395fb674ac5b9c6f7c4fb27832eacdf075`;线上 H5 `index-D4-NrEpb.js` SHA-256 为 `ab15bfd17cabe58e2f34b0ac61ee198a837128ed85dc6bb0ddf77bc3e22dcd9c`,当前分支构建 `index-8gZKJTho.js` SHA-256 为 `c6b01faeba4198cb7ff9f5c2ffa88f1cc97c8ecdf20be86f432ca7a1d673f3e1`。本轮只读核验,未执行生产静态资源同步、后端重启或业务数据写入。 - 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,未修改生产数据。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index d75909fc..965a197f 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -518,6 +518,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control 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 "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"