fix(aihr): persist org hire dates

This commit is contained in:
2026-07-14 14:48:48 +08:00
parent 96a9cffc82
commit 3f39be7fee
8 changed files with 75 additions and 14 deletions
@@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -353,8 +354,8 @@ public class AihrOrgSyncService {
jdbcTemplate.batchUpdate("""
insert into aihr_org_snapshot
(tenant_id, project_code, project_name, dept_name, ext_party_id, person_phone, person_name,
position_name, position_level, employment_status, snapshot_date, create_time)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
position_name, position_level, employment_status, hire_date, snapshot_date, create_time)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
on duplicate key update
project_code = values(project_code),
project_name = values(project_name),
@@ -364,6 +365,7 @@ public class AihrOrgSyncService {
position_name = values(position_name),
position_level = values(position_level),
employment_status = values(employment_status),
hire_date = values(hire_date),
snapshot_date = values(snapshot_date),
create_time = now()
""", rows.stream().map(row -> row.args(tenantId())).toList());
@@ -397,6 +399,8 @@ public class AihrOrgSyncService {
String positionName = firstNonBlank(text(employee, "position_name", "positionName", "job_title", "jobTitle",
"post_name", "postName", "job_name", "jobName", "role_name", "roleName", "position", "title"), "员工");
String positionLevel = firstNonBlank(text(employee, "position_level", "positionLevel", "job_level", "jobLevel"), level(positionName));
LocalDate hireDate = parseDate(firstNonBlank(
text(employee, "hire_date", "hireDate", "entry_date", "entryDate", "employment_date", "employmentDate")));
boolean suspectText = hasQuestionMark(projectName)
|| hasQuestionMark(deptName)
|| hasQuestionMark(personName)
@@ -411,6 +415,7 @@ public class AihrOrgSyncService {
positionName,
positionLevel(positionLevel, positionName),
status(text(employee, "status", "employment_status", "employmentStatus")),
hireDate,
LocalDate.now(),
suspectText
);
@@ -530,6 +535,7 @@ public class AihrOrgSyncService {
throw new IllegalArgumentException("请先执行 aihr_org_snapshot_mysql8.sql 初始化组织人员快照表");
}
ensureSnapshotColumn("person_phone", "ALTER TABLE aihr_org_snapshot ADD COLUMN `person_phone` varchar(30) DEFAULT NULL COMMENT '手机号(仅用于登录身份映射)' AFTER `ext_party_id`");
ensureSnapshotColumn("hire_date", "ALTER TABLE aihr_org_snapshot ADD COLUMN `hire_date` date DEFAULT NULL COMMENT '入职日期(用于新员工三个月资格判断)' AFTER `employment_status`");
ensureSnapshotIndex("idx_aihr_org_phone", "ALTER TABLE aihr_org_snapshot ADD KEY `idx_aihr_org_phone` (`tenant_id`, `person_phone`)");
} catch (DataAccessException e) {
throw new IllegalStateException("组织人员快照表检查失败(处理错误已隐藏)", e);
@@ -705,6 +711,19 @@ public class AihrOrgSyncService {
return value == null ? "" : value.trim();
}
private static LocalDate parseDate(String value) {
String text = clean(value);
if (text.isBlank()) {
return null;
}
String dateText = text.length() >= 10 ? text.substring(0, 10) : text;
try {
return LocalDate.parse(dateText);
} catch (DateTimeParseException ignored) {
return null;
}
}
private static String truncate(String value, int maxChars) {
String text = clean(value);
return text.length() <= maxChars ? text : text.substring(0, maxChars);
@@ -726,13 +745,14 @@ public class AihrOrgSyncService {
String positionName,
String positionLevel,
String employmentStatus,
LocalDate hireDate,
LocalDate snapshotDate,
boolean suspectText
) {
Object[] args(String tenantId) {
return new Object[] {
tenantId, projectCode, projectName, deptName, extPartyId, personPhone, personName,
positionName, positionLevel, employmentStatus, snapshotDate
positionName, positionLevel, employmentStatus, hireDate, snapshotDate
};
}
}
@@ -1858,7 +1858,11 @@ public class AihrPracticeSeedService {
if (orgSnapshotTableExists() && orgSnapshotColumnExists("hire_date")) {
return findHireDate(extPartyId)
.map(hireDate -> isWithinNewHireWindow(hireDate, LocalDate.now()))
.orElse(false);
.orElseGet(() -> isLegacyDailyDrillEligible(
isLegacyDailyDrillFallbackEnabled(allowLegacyDailyDrillFallback, activeProfiles),
extPartyId,
countMobileRecords(extPartyId)
));
}
// Formal production data must provide hire_date. The record-count fallback is
// intentionally opt-in for the local Demo only; it must not infer employment
@@ -12,6 +12,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -62,18 +63,20 @@ public class AihrOrgSyncServiceTest {
ObjectMapper mapper = new ObjectMapper();
AihrOrgSyncService service = new AihrOrgSyncService(new ObjectMapper(), mock(JdbcTemplate.class), mock(TransactionTemplate.class));
var employee = mapper.readTree("""
{"id":"EMP-1","name":"测试员工","department_name":"项目管理","project_name":"翡翠湾","position_name":"主管","phone":"13900001111"}
{"id":"EMP-1","name":"测试员工","department_name":"项目管理","project_name":"翡翠湾","position_name":"主管","phone":"13900001111","hire_date":"2026-07-01"}
""");
ReflectionTestUtils.setField(service, "storeDisplayFields", false);
Object masked = ReflectionTestUtils.invokeMethod(service, "orgRow", employee, Map.of(), Map.of());
assertEquals("", ReflectionTestUtils.getField(masked, "personName"));
assertEquals("", ReflectionTestUtils.getField(masked, "deptName"));
assertEquals(LocalDate.of(2026, 7, 1), ReflectionTestUtils.getField(masked, "hireDate"));
ReflectionTestUtils.setField(service, "storeDisplayFields", true);
Object demo = ReflectionTestUtils.invokeMethod(service, "orgRow", employee, Map.of(), Map.of());
assertEquals("测试员工", ReflectionTestUtils.getField(demo, "personName"));
assertEquals("项目管理", ReflectionTestUtils.getField(demo, "deptName"));
assertEquals(LocalDate.of(2026, 7, 1), ReflectionTestUtils.getField(demo, "hireDate"));
}
@Test
@@ -16,6 +16,7 @@ CREATE TABLE `aihr_org_snapshot` (
`position_name` varchar(100) DEFAULT NULL COMMENT '岗位',
`position_level` varchar(30) DEFAULT NULL COMMENT '岗位层级(一线/主管/项目经理)',
`employment_status` varchar(20) DEFAULT 'active' COMMENT '在职状态',
`hire_date` date DEFAULT NULL COMMENT '入职日期(用于新员工三个月资格判断)',
`snapshot_date` date DEFAULT NULL COMMENT '快照日期',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
@@ -0,0 +1,17 @@
-- AIHR 组织快照入职日期迁移(MySQL 8.x)
-- 仅补齐结构,不写入业务数据;可重复执行。
SET @aihr_hire_date_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'aihr_org_snapshot'
AND COLUMN_NAME = 'hire_date'
);
SET @aihr_hire_date_ddl := IF(
@aihr_hire_date_exists = 0,
'ALTER TABLE aihr_org_snapshot ADD COLUMN `hire_date` date DEFAULT NULL COMMENT ''入职日期(用于新员工三个月资格判断)'' AFTER `employment_status`',
'SELECT 1'
);
PREPARE aihr_hire_date_stmt FROM @aihr_hire_date_ddl;
EXECUTE aihr_hire_date_stmt;
DEALLOCATE PREPARE aihr_hire_date_stmt;