diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java index e9365a9b..9e27e62a 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java @@ -28,6 +28,7 @@ public final class AihrOrgSyncDto { int syncedCount, int skippedCount, int phoneLinked, + int maskedPhone, int suspectText, List warnings ) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java index 146fc612..40b2eb8a 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java @@ -42,6 +42,9 @@ public class AihrOrgSyncService { private static final int MAX_PAGE_SIZE = 500; private static final int DEFAULT_MAX_PAGES = 200; private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(30); + private static final String[] PHONE_FIELDS = {"phone", "phone_number", "phoneNumber", "mobile", "mobile_phone", + "mobilePhone", "mobile_tel", "mobileTel", "cellphone", "cellPhone", "contact_phone", + "contactPhone", "contact_mobile", "contactMobile", "phonenumber", "telephone", "tel", "手机号", "手机", "联系电话"}; private final ObjectMapper objectMapper; private final JdbcTemplate jdbcTemplate; @@ -85,11 +88,15 @@ public class AihrOrgSyncService { List rows = new ArrayList<>(); int skipped = 0; + int maskedPhone = 0; for (JsonNode employee : employeeItems) { OrgRow row = orgRow(employee, companies, departments); if (row == null) { skipped++; } else { + if (row.personPhone().isBlank() && hasUnusablePhoneCandidate(employee)) { + maskedPhone++; + } rows.add(row); } } @@ -101,6 +108,9 @@ public class AihrOrgSyncService { if (suspectText > 0) { warnings.add("外部员工快照存在 " + suspectText + " 条疑似乱码字段,请检查开放接口编码或导入链路"); } + if (maskedPhone > 0) { + warnings.add("外部员工快照存在 " + maskedPhone + " 条脱敏/不可用手机号,不能用于移动端正式登录映射"); + } if (!dryRun && replaceExisting && rows.isEmpty()) { throw new IllegalArgumentException("外部员工快照为空,已阻止覆盖本地组织人员快照"); } @@ -118,6 +128,7 @@ public class AihrOrgSyncService { dryRun ? 0 : rows.size(), skipped, phoneLinked, + maskedPhone, suspectText, List.copyOf(warnings) ); @@ -525,10 +536,7 @@ public class AihrOrgSyncService { } private static String phone(JsonNode employee) { - String[] fields = {"phone", "phone_number", "phoneNumber", "mobile", "mobile_phone", - "mobilePhone", "mobile_tel", "mobileTel", "cellphone", "cellPhone", "contact_phone", - "contactPhone", "contact_mobile", "contactMobile", "phonenumber", "telephone", "tel", "手机号", "手机", "联系电话"}; - for (String field : fields) { + for (String field : PHONE_FIELDS) { String normalized = normalizeMobilePhone(text(employee, field)); if (!normalized.isBlank()) { return normalized; @@ -545,6 +553,16 @@ public class AihrOrgSyncService { return digits.matches("1[3-9]\\d{9}") ? digits : ""; } + static boolean hasUnusablePhoneCandidate(JsonNode employee) { + for (String field : PHONE_FIELDS) { + String value = text(employee, field); + if (!value.isBlank() && normalizeMobilePhone(value).isBlank()) { + return true; + } + } + return false; + } + private static boolean hasSuspectText(OrgRow row) { return hasQuestionMark(row.projectName()) || hasQuestionMark(row.deptName()) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java index 58ca38ef..423151fb 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java @@ -1,9 +1,12 @@ package org.dromara.aihr.service; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Tag; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; @Tag("dev") public class AihrOrgSyncServiceTest { @@ -14,4 +17,13 @@ public class AihrOrgSyncServiceTest { assertEquals("", AihrOrgSyncService.normalizeMobilePhone("139****1111")); assertEquals("", AihrOrgSyncService.normalizeMobilePhone("025-88888888")); } + + @Test + public void unusablePhoneCandidateDetectsMaskedPhone() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + assertTrue(AihrOrgSyncService.hasUnusablePhoneCandidate(mapper.readTree("{\"phone\":\"139****1111\"}"))); + assertFalse(AihrOrgSyncService.hasUnusablePhoneCandidate(mapper.readTree("{\"phone\":\"13900001111\"}"))); + assertFalse(AihrOrgSyncService.hasUnusablePhoneCandidate(mapper.readTree("{\"name\":\"张三\"}"))); + } } diff --git a/docs/PC管理端功能差异TODO.md b/docs/PC管理端功能差异TODO.md index 796fccd3..33f39f4c 100644 --- a/docs/PC管理端功能差异TODO.md +++ b/docs/PC管理端功能差异TODO.md @@ -21,5 +21,5 @@ - 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。 - 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。 - `组织与权限` 页暴露手机号覆盖数和疑似乱码数,只做质量提示,不展示手机号明细。 -- 组织同步预检同步返回手机号覆盖数和疑似乱码数;手机号只统计可用于移动端登录的 11 位手机号。 +- 组织同步预检同步返回手机号覆盖数、脱敏手机号数和疑似乱码数;手机号只统计可用于移动端登录的 11 位手机号。 - 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。 diff --git a/frontend/src/api/aihr/mobile-admin.ts b/frontend/src/api/aihr/mobile-admin.ts index fcab7577..0ffba3c5 100644 --- a/frontend/src/api/aihr/mobile-admin.ts +++ b/frontend/src/api/aihr/mobile-admin.ts @@ -114,6 +114,7 @@ export type OrgSyncResponse = { syncedCount: number; skippedCount: number; phoneLinked: number; + maskedPhone: number; suspectText: number; warnings: string[]; }; diff --git a/frontend/src/views/admin/org-permission.vue b/frontend/src/views/admin/org-permission.vue index 83c6ab0f..cb257fd3 100644 --- a/frontend/src/views/admin/org-permission.vue +++ b/frontend/src/views/admin/org-permission.vue @@ -156,6 +156,9 @@
手机号/疑似乱码{{ syncResult.phoneLinked }} / {{ syncResult.suspectText }}
+
+ 脱敏手机号{{ syncResult.maskedPhone }} +

{{ syncResult.warnings.join(';') }}

同步接口已返回结果。