feat(aihr): surface org snapshot data quality

This commit is contained in:
2026-07-09 21:38:35 +08:00
parent f00f9045eb
commit f38dfeeae0
5 changed files with 54 additions and 1 deletions
@@ -35,6 +35,8 @@ public final class AihrOrgSyncDto {
int total, int total,
int active, int active,
int departed, int departed,
int phoneLinked,
int suspectText,
int pageNum, int pageNum,
int pageSize, int pageSize,
List<OrgProjectOption> projects, List<OrgProjectOption> projects,
@@ -124,6 +124,17 @@ public class AihrOrgSyncService {
int total = countOrgRows("select count(*) from aihr_org_snapshot" + where, args); int total = countOrgRows("select count(*) from aihr_org_snapshot" + where, args);
int active = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'active'", args); int active = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'active'", args);
int departed = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'departed'", args); int departed = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'departed'", args);
int phoneLinked = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and person_phone is not null and person_phone <> ''", args);
int suspectText = countOrgRows("""
select count(*) from aihr_org_snapshot
""" + where + """
and (
person_name like '%?%'
or project_name like '%?%'
or dept_name like '%?%'
or position_name like '%?%'
)
""", args);
List<Object> rowArgs = new ArrayList<>(args); List<Object> rowArgs = new ArrayList<>(args);
rowArgs.add(safePageSize); rowArgs.add(safePageSize);
rowArgs.add(offset); rowArgs.add(offset);
@@ -147,7 +158,7 @@ public class AihrOrgSyncService {
rs.getString("employment_status"), rs.getString("employment_status"),
rs.getString("snapshot_date") rs.getString("snapshot_date")
), rowArgs.toArray()); ), rowArgs.toArray());
return new OrgSnapshotResponse(total, active, departed, safePageNum, safePageSize, orgProjects(), orgLevels(), rows); return new OrgSnapshotResponse(total, active, departed, phoneLinked, suspectText, safePageNum, safePageSize, orgProjects(), orgLevels(), rows);
} }
private List<JsonNode> fetchOptional(String baseUrl, String token, SyncRequest req, String resourceType, private List<JsonNode> fetchOptional(String baseUrl, String token, SyncRequest req, String resourceType,
+1
View File
@@ -20,4 +20,5 @@
- 不改登录、密钥、模型配置、支付、部署脚本。 - 不改登录、密钥、模型配置、支付、部署脚本。
- 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。 - 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。
- 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。 - 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。
- `组织与权限` 页暴露手机号覆盖数和疑似乱码数,只做质量提示,不展示手机号明细。
- 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。 - 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。
+2
View File
@@ -148,6 +148,8 @@ export type OrgSnapshotResponse = {
total: number; total: number;
active: number; active: number;
departed: number; departed: number;
phoneLinked: number;
suspectText: number;
pageNum: number; pageNum: number;
pageSize: number; pageSize: number;
projects: OrgProjectOption[]; projects: OrgProjectOption[];
@@ -38,8 +38,21 @@
<el-tag>总数 {{ orgSnapshot?.total ?? 0 }}</el-tag> <el-tag>总数 {{ orgSnapshot?.total ?? 0 }}</el-tag>
<el-tag type="success">在职 {{ orgSnapshot?.active ?? 0 }}</el-tag> <el-tag type="success">在职 {{ orgSnapshot?.active ?? 0 }}</el-tag>
<el-tag type="info">离职 {{ orgSnapshot?.departed ?? 0 }}</el-tag> <el-tag type="info">离职 {{ orgSnapshot?.departed ?? 0 }}</el-tag>
<el-tag :type="mobileLoginReady ? 'success' : 'warning'">可登录 {{ orgSnapshot?.phoneLinked ?? 0 }}</el-tag>
<el-tag v-if="orgSnapshot?.suspectText" type="danger">疑似乱码 {{ orgSnapshot.suspectText }}</el-tag>
</div> </div>
</div> </div>
<el-alert
v-if="orgQualityWarnings.length"
class="quality-alert"
type="warning"
:closable="false"
show-icon
>
<template #title>
{{ orgQualityWarnings.join(';') }}
</template>
</el-alert>
<div class="org-filters"> <div class="org-filters">
<el-input v-model="orgFilters.keyword" clearable placeholder="姓名/岗位/部门/外部ID" @keyup.enter="searchOrgSnapshot" /> <el-input v-model="orgFilters.keyword" clearable placeholder="姓名/岗位/部门/外部ID" @keyup.enter="searchOrgSnapshot" />
<el-select v-model="orgFilters.projectCode" clearable placeholder="项目"> <el-select v-model="orgFilters.projectCode" clearable placeholder="项目">
@@ -233,6 +246,26 @@ const projectScopes = computed(() =>
})) }))
); );
const mobileLoginReady = computed(() => {
const snapshot = orgSnapshot.value;
return Boolean(snapshot?.total && snapshot.phoneLinked >= snapshot.active);
});
const orgQualityWarnings = computed(() => {
const snapshot = orgSnapshot.value;
if (!snapshot?.total) {
return [];
}
const warnings: string[] = [];
if (snapshot.phoneLinked < snapshot.active) {
warnings.push(`移动端登录映射不完整:当前筛选 ${snapshot.active} 名在职人员,仅 ${snapshot.phoneLinked} 名带手机号`);
}
if (snapshot.suspectText > 0) {
warnings.push(`组织字段存在 ${snapshot.suspectText} 条疑似乱码,请检查同步源或 SQL 导入字符集`);
}
return warnings;
});
const loadOrgSnapshot = async () => { const loadOrgSnapshot = async () => {
orgLoading.value = true; orgLoading.value = true;
try { try {
@@ -377,6 +410,10 @@ onMounted(loadOrgSnapshot);
margin-bottom: 14px; margin-bottom: 14px;
} }
.quality-alert {
margin-bottom: 14px;
}
.sync-result { .sync-result {
display: grid; display: grid;
gap: 10px; gap: 10px;