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 1c1d1887..e9365a9b 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 @@ -27,6 +27,8 @@ public final class AihrOrgSyncDto { int employeeCount, int syncedCount, int skippedCount, + int phoneLinked, + 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 5aef0c55..146fc612 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 @@ -93,6 +93,14 @@ public class AihrOrgSyncService { rows.add(row); } } + int phoneLinked = (int) rows.stream().filter(row -> !row.personPhone().isBlank()).count(); + int suspectText = (int) rows.stream().filter(AihrOrgSyncService::hasSuspectText).count(); + if (!rows.isEmpty() && phoneLinked < rows.size()) { + warnings.add("外部员工快照手机号覆盖 " + phoneLinked + "/" + rows.size() + ",移动端正式登录仅可映射已带手机号人员"); + } + if (suspectText > 0) { + warnings.add("外部员工快照存在 " + suspectText + " 条疑似乱码字段,请检查开放接口编码或导入链路"); + } if (!dryRun && replaceExisting && rows.isEmpty()) { throw new IllegalArgumentException("外部员工快照为空,已阻止覆盖本地组织人员快照"); } @@ -109,6 +117,8 @@ public class AihrOrgSyncService { employeeItems.size(), dryRun ? 0 : rows.size(), skipped, + phoneLinked, + suspectText, List.copyOf(warnings) ); } @@ -515,8 +525,35 @@ public class AihrOrgSyncService { } private static String phone(JsonNode employee) { - return text(employee, "phone", "mobile", "mobile_phone", "mobilePhone", "contact_phone", - "contactPhone", "phonenumber", "telephone", "tel"); + 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) { + String normalized = normalizeMobilePhone(text(employee, field)); + if (!normalized.isBlank()) { + return normalized; + } + } + return ""; + } + + static String normalizeMobilePhone(String value) { + String digits = clean(value).replaceAll("\\D", ""); + if (digits.length() == 13 && digits.startsWith("86")) { + digits = digits.substring(2); + } + return digits.matches("1[3-9]\\d{9}") ? digits : ""; + } + + private static boolean hasSuspectText(OrgRow row) { + return hasQuestionMark(row.projectName()) + || hasQuestionMark(row.deptName()) + || hasQuestionMark(row.personName()) + || hasQuestionMark(row.positionName()); + } + + private static boolean hasQuestionMark(String value) { + return clean(value).contains("?"); } private static String text(JsonNode node, String... fields) { 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 new file mode 100644 index 00000000..58ca38ef --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java @@ -0,0 +1,17 @@ +package org.dromara.aihr.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Tag("dev") +public class AihrOrgSyncServiceTest { + + @Test + public void normalizeMobilePhoneOnlyKeepsUsableMobileNumber() { + assertEquals("13900001111", AihrOrgSyncService.normalizeMobilePhone("+86 139-0000-1111")); + assertEquals("", AihrOrgSyncService.normalizeMobilePhone("139****1111")); + assertEquals("", AihrOrgSyncService.normalizeMobilePhone("025-88888888")); + } +} diff --git a/docs/PC管理端功能差异TODO.md b/docs/PC管理端功能差异TODO.md index 10968ce6..796fccd3 100644 --- a/docs/PC管理端功能差异TODO.md +++ b/docs/PC管理端功能差异TODO.md @@ -21,4 +21,5 @@ - 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。 - 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。 - `组织与权限` 页暴露手机号覆盖数和疑似乱码数,只做质量提示,不展示手机号明细。 +- 组织同步预检同步返回手机号覆盖数和疑似乱码数;手机号只统计可用于移动端登录的 11 位手机号。 - 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。 diff --git a/frontend/src/api/aihr/mobile-admin.ts b/frontend/src/api/aihr/mobile-admin.ts index 3dfa0ad6..fcab7577 100644 --- a/frontend/src/api/aihr/mobile-admin.ts +++ b/frontend/src/api/aihr/mobile-admin.ts @@ -113,6 +113,8 @@ export type OrgSyncResponse = { employeeCount: number; syncedCount: number; skippedCount: number; + phoneLinked: number; + suspectText: number; warnings: string[]; }; diff --git a/frontend/src/views/admin/org-permission.vue b/frontend/src/views/admin/org-permission.vue index 43b41027..83c6ab0f 100644 --- a/frontend/src/views/admin/org-permission.vue +++ b/frontend/src/views/admin/org-permission.vue @@ -153,6 +153,9 @@
写入/跳过{{ syncResult.syncedCount }} / {{ syncResult.skippedCount }}
+
+ 手机号/疑似乱码{{ syncResult.phoneLinked }} / {{ syncResult.suspectText }} +

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

同步接口已返回结果。