feat(aihr): map supervisor phone to org scope

This commit is contained in:
2026-07-09 12:54:33 +08:00
parent 9bd567a72f
commit 5f92847c00
5 changed files with 89 additions and 9 deletions
@@ -280,13 +280,14 @@ public class AihrOrgSyncService {
}
jdbcTemplate.batchUpdate("""
insert into aihr_org_snapshot
(tenant_id, project_code, project_name, dept_name, ext_party_id, person_name,
(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())
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
on duplicate key update
project_code = values(project_code),
project_name = values(project_name),
dept_name = values(dept_name),
person_phone = values(person_phone),
person_name = values(person_name),
position_name = values(position_name),
position_level = values(position_level),
@@ -320,16 +321,18 @@ public class AihrOrgSyncService {
deptName,
"组织架构"
);
String positionName = firstNonBlank(text(employee, "position_name", "positionName", "job_title", "jobTitle", "title"), "员工");
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));
return new OrgRow(
projectCode,
projectName,
deptName,
extPartyId,
phone(employee),
firstNonBlank(text(employee, "name", "person_name", "personName", "employee_name", "employeeName"), "-"),
positionName,
positionLevel,
positionLevel(positionLevel, positionName),
status(text(employee, "status", "employment_status", "employmentStatus")),
LocalDate.now()
);
@@ -437,11 +440,35 @@ public class AihrOrgSyncService {
if (count == null || count == 0) {
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`");
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.getMessage(), e);
}
}
private void ensureSnapshotColumn(String column, String ddl) {
Integer exists = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aihr_org_snapshot' AND COLUMN_NAME = ?
""", Integer.class, column);
if (exists == null || exists == 0) {
jdbcTemplate.execute(ddl);
}
}
private void ensureSnapshotIndex(String index, String ddl) {
Integer exists = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aihr_org_snapshot' AND INDEX_NAME = ?
""", Integer.class, index);
if (exists == null || exists == 0) {
jdbcTemplate.execute(ddl);
}
}
private static String status(String value) {
String text = clean(value).toLowerCase();
if (text.isBlank() || text.equals("1") || text.equals("active") || text.equals("enabled")
@@ -466,6 +493,20 @@ public class AihrOrgSyncService {
return "一线";
}
private static String positionLevel(String rawLevel, String positionName) {
String inferred = level(positionName);
String level = clean(rawLevel);
if (level.isBlank() || (("一线".equals(level) || "员工".equals(level)) && !"一线".equals(inferred))) {
return inferred;
}
return level;
}
private static String phone(JsonNode employee) {
return text(employee, "phone", "mobile", "mobile_phone", "mobilePhone", "contact_phone",
"contactPhone", "phonenumber", "telephone", "tel");
}
private static String text(JsonNode node, String... fields) {
for (String field : fields) {
JsonNode child = node.path(field);
@@ -564,6 +605,7 @@ public class AihrOrgSyncService {
String projectName,
String deptName,
String extPartyId,
String personPhone,
String personName,
String positionName,
String positionLevel,
@@ -572,7 +614,7 @@ public class AihrOrgSyncService {
) {
Object[] args() {
return new Object[] {
TENANT_ID, projectCode, projectName, deptName, extPartyId, personName,
TENANT_ID, projectCode, projectName, deptName, extPartyId, personPhone, personName,
positionName, positionLevel, employmentStatus, snapshotDate
};
}
@@ -1366,12 +1366,22 @@ public class AihrPracticeSeedService {
if (owner.isEmpty() || !orgSnapshotTableExists()) {
return TeamScope.unscoped();
}
boolean hasPhone = orgSnapshotColumnExists("person_phone");
List<Object> ownerArgs = new ArrayList<>();
ownerArgs.add(TENANT_ID);
ownerArgs.add(owner);
String ownerWhere = "ext_party_id = ?";
if (hasPhone) {
ownerWhere = "(ext_party_id = ? OR person_phone = ?)";
ownerArgs.add(owner);
}
List<OrgScopeRow> owners = jdbcTemplate.query("""
SELECT project_code, position_level
FROM aihr_org_snapshot
WHERE tenant_id = ? AND ext_party_id = ? AND employment_status = 'active'
WHERE tenant_id = ? AND """ + ownerWhere + """
AND employment_status = 'active'
LIMIT 1
""", (rs, rowNum) -> new OrgScopeRow(rs.getString("project_code"), rs.getString("position_level")), TENANT_ID, owner);
""", (rs, rowNum) -> new OrgScopeRow(rs.getString("project_code"), rs.getString("position_level")), ownerArgs.toArray());
if (owners.isEmpty()) {
return TeamScope.unscoped();
}
@@ -1397,6 +1407,15 @@ public class AihrPracticeSeedService {
return count != null && count > 0;
}
private boolean orgSnapshotColumnExists(String column) {
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aihr_org_snapshot' AND COLUMN_NAME = ?
""", Integer.class, column);
return count != null && count > 0;
}
private static boolean canSeeProject(String positionLevel) {
return "主管".equals(positionLevel) || "项目经理".equals(positionLevel);
}
@@ -93,6 +93,19 @@ public class AihrPracticeSeedServiceTest {
assertEquals(List.of("000000", "SUP-1", 5), jdbcTemplate.reviewArgs);
}
@Test
public void pendingReviewRecordsResolvesSupervisorPhone() {
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("主管", List.of("SUP-1", "EMP-1"));
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
service.pendingReviewRecords("13800000001", "", "", 5);
assertTrue(jdbcTemplate.ownerSql.contains("person_phone = ?"));
assertEquals(List.of("000000", "13800000001", "13800000001"), jdbcTemplate.ownerArgs);
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?,?)"));
assertEquals(List.of("000000", "SUP-1", "EMP-1", 5), jdbcTemplate.reviewArgs);
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static <T> List<T> mapOne(InvocationOnMock invocation, Map<String, String> values) throws SQLException {
RowMapper mapper = invocation.getArgument(1);
@@ -116,6 +129,8 @@ public class AihrPracticeSeedServiceTest {
private static final class PracticeReviewJdbcTemplate extends JdbcTemplate {
private final String ownerLevel;
private final List<String> scopeIds;
private String ownerSql = "";
private List<Object> ownerArgs = List.of();
private String reviewSql = "";
private List<Object> reviewArgs = List.of();
@@ -148,6 +163,8 @@ public class AihrPracticeSeedServiceTest {
return List.of();
}
if (sql.contains("SELECT project_code") && sql.contains("LIMIT 1")) {
ownerSql = sql;
ownerArgs = List.of(args);
return mapRows(rowMapper, List.of(Map.of(
"project_code", "P1",
"position_level", ownerLevel
@@ -11,6 +11,7 @@ CREATE TABLE `aihr_org_snapshot` (
`project_name` varchar(100) NOT NULL COMMENT '项目名称',
`dept_name` varchar(100) DEFAULT NULL COMMENT '部门/条线',
`ext_party_id` varchar(100) NOT NULL COMMENT '外部主体ID(只存ID,不回写外部系统)',
`person_phone` varchar(30) DEFAULT NULL COMMENT '手机号(仅用于登录身份映射)',
`person_name` varchar(100) DEFAULT NULL COMMENT '姓名(Demo 快照允许,生产走脱敏闸门G3)',
`position_name` varchar(100) DEFAULT NULL COMMENT '岗位',
`position_level` varchar(30) DEFAULT NULL COMMENT '岗位层级(一线/主管/项目经理)',
@@ -19,6 +20,7 @@ CREATE TABLE `aihr_org_snapshot` (
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_aihr_org_party` (`tenant_id`, `ext_party_id`),
KEY `idx_aihr_org_phone` (`tenant_id`, `person_phone`),
KEY `idx_aihr_org_project` (`tenant_id`, `project_code`, `position_level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='组织人员静态快照(Demo)';