fix(aihr): honor hire date for daily drills

This commit is contained in:
2026-07-14 10:39:45 +08:00
parent 9810c9253c
commit 7e82d0c7eb
4 changed files with 48 additions and 1 deletions
@@ -1769,7 +1769,7 @@ public class AihrPracticeSeedService {
} }
private void ensureDailyDrills(String extPartyId) { private void ensureDailyDrills(String extPartyId) {
if (isBlank(extPartyId) || countMobileRecords(extPartyId) >= 3) { if (isBlank(extPartyId) || !dailyDrillEligible(extPartyId)) {
return; return;
} }
int existingToday = count(""" 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<LocalDate> findHireDate(String extPartyId) {
List<LocalDate> 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) { public PracticeAssignmentResponse createAssignment(PracticeAssignmentRequest request) {
String extPartyId = request == null ? "" : firstNonBlank(request.extPartyId(), ""); String extPartyId = request == null ? "" : firstNonBlank(request.extPartyId(), "");
if (isBlank(extPartyId)) { if (isBlank(extPartyId)) {
@@ -109,6 +109,17 @@ public class AihrPracticeSeedServiceTest {
assertEquals("缺少员工ID,无法派发专项训练", error.getMessage()); 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 @Test
public void mobilePracticeSessionRejectsForeignAndUnknownOwner() { public void mobilePracticeSessionRejectsForeignAndUnknownOwner() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+1
View File
@@ -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 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 用户仍可能调用全局统计分支;现要求只有 APP 用户且身份非空才读取员工/主管动态数据,后台系统用户和异常身份统一返回无业务统计的公开 seed。AIHR 全量测试 `70/70` 通过,未修改生产环境。
- 2026-07-14 BRD G3 音频读取权限复核:发现移动端音频下载接口对已登录 SYS 用户未校验角色,普通后台账号理论上可枚举 `ossId` 读取训练录音;现复用 `superadmin/hr_operator` 门禁,APP 用户继续按本人/主管项目范围校验,其他身份返回 403。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`,不能把回退口径当成正式新员工资格。
+1
View File
@@ -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 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 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 "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/AihrOrgSyncService.java "instr(coalesce(person_name, '')"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "maskSensitiveText" 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" contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/assignments"