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 ce7ba025..4445a956 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 @@ -1769,7 +1769,7 @@ public class AihrPracticeSeedService { } private void ensureDailyDrills(String extPartyId) { - if (isBlank(extPartyId) || countMobileRecords(extPartyId) >= 3) { + if (isBlank(extPartyId) || !dailyDrillEligible(extPartyId)) { return; } int existingToday = count(""" @@ -1803,6 +1803,40 @@ public class AihrPracticeSeedService { } } + private boolean dailyDrillEligible(String extPartyId) { + if (orgSnapshotTableExists() && orgSnapshotColumnExists("hire_date")) { + return findHireDate(extPartyId) + .map(hireDate -> isWithinNewHireWindow(hireDate, LocalDate.now())) + .orElse(false); + } + // The legacy Demo snapshot has no hire_date. Keep its deterministic fallback + // until the external organization sync supplies the formal field. + return countMobileRecords(extPartyId) < 3; + } + + private Optional findHireDate(String extPartyId) { + List hireDates = jdbcTemplate.query(""" + SELECT hire_date + FROM aihr_org_snapshot + WHERE tenant_id = ? AND employment_status = 'active' + AND (ext_party_id = ? OR person_phone = ?) + AND hire_date IS NOT NULL + ORDER BY hire_date DESC + LIMIT 1 + """, (rs, rowNum) -> { + java.sql.Date hireDate = rs.getDate("hire_date"); + return hireDate == null ? null : hireDate.toLocalDate(); + }, tenantId(), extPartyId, extPartyId); + return hireDates.stream().filter(java.util.Objects::nonNull).findFirst(); + } + + static boolean isWithinNewHireWindow(LocalDate hireDate, LocalDate asOf) { + if (hireDate == null || asOf == null || hireDate.isAfter(asOf)) { + return false; + } + return !hireDate.isBefore(asOf.minusMonths(3)); + } + public PracticeAssignmentResponse createAssignment(PracticeAssignmentRequest request) { String extPartyId = request == null ? "" : firstNonBlank(request.extPartyId(), ""); if (isBlank(extPartyId)) { 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 8a8a8949..838588dd 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 @@ -109,6 +109,17 @@ public class AihrPracticeSeedServiceTest { assertEquals("缺少员工ID,无法派发专项训练", error.getMessage()); } + @Test + public void newHireWindowUsesInclusiveThreeMonthBoundary() { + LocalDate asOf = LocalDate.of(2026, 7, 14); + + assertTrue(AihrPracticeSeedService.isWithinNewHireWindow(asOf, asOf)); + assertTrue(AihrPracticeSeedService.isWithinNewHireWindow(asOf.minusMonths(3), asOf)); + assertFalse(AihrPracticeSeedService.isWithinNewHireWindow(asOf.minusMonths(3).minusDays(1), asOf)); + assertFalse(AihrPracticeSeedService.isWithinNewHireWindow(asOf.plusDays(1), asOf)); + assertFalse(AihrPracticeSeedService.isWithinNewHireWindow(null, asOf)); + } + @Test public void mobilePracticeSessionRejectsForeignAndUnknownOwner() { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 3003f3fd..6dc8aaf0 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -174,3 +174,4 @@ - 2026-07-14 BRD 4.4 每日三题口径复核:当前 `ensureDailyDrills` 以“移动端训练记录少于 3 次”作为新员工近似条件;`aihr_org_snapshot` 只有快照日期,没有入职/任职起始日期,因此不能证明“入职 3 个月内”,也不能用账号创建时间替代。当前保留该近似仅用于 Demo;正式试点需由组织同步提供并确认 `hire_date/entry_date` 字段后,再把每日派题资格改为真实任职日期判断。 - 2026-07-14 BRD G3 移动首页权限复核:发现公开首页方法对已登录 SYS 用户仍可能调用全局统计分支;现要求只有 APP 用户且身份非空才读取员工/主管动态数据,后台系统用户和异常身份统一返回无业务统计的公开 seed。AIHR 全量测试 `70/70` 通过,未修改生产环境。 - 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`,不能把回退口径当成正式新员工资格。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 4c9d98fa..801813e5 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -470,6 +470,7 @@ contains mobile-uni/src/pages/supervisor/review/index.vue "人工贡献分" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/reviews/{id}/reviewed" 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 "isWithinNewHireWindow" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "instr(coalesce(person_name, '')" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "maskSensitiveText" contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/assignments"