feat(customer): 新增ICC客户信息同步功能 - OAuth2密码模式鉴权拉取org_base客户、按icc_code幂等upsert写入并记录icc_sync_log,注册每10分钟定时任务;客户档案管理页新增手动'同步客户'按钮(POST /api/customers/sync/icc),同步后自动刷新列表与统计;customer表新增icc_code字段及唯一索引,同步db_seed与设计文档

This commit is contained in:
wk authored and trae-solo-agent committed 2026-10-23 18:20:25 +08:00
1 parent be91d04431
commit 9d3bb13ecf
24 files changed
+1159 -5

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
/**
* ICC 客户(组织)数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)。
*
* ICC 返回的原始字段名 → 本系统 customer 字段。接入真实 ICC 平台时仅需
* 调整本文件与 fetch 路径即可,业务层无需改动。纯映射逻辑,无 SQL 无 DOM。
*/
export interface IccOrgRawRecord {
[field: string]: any;
}
/** 组织分页列表接口路径({version} 由调用方按 ICC_VERSION 替换) */
export const ICC_ORG_ENDPOINT = "/evo-apigw/evo-brm/{version}/organization/page";
/** 分页请求参数名约定:pageNum / pageSize / maxRangeId(调用方构造请求时使用) */
/** 组织类型过滤常量:仅同步 base 组织(对应客户) */
export const ICC_ORG_TYPE_BASE = "org_base";
/** 原始记录关键字段映射:ICC 返回字段名 → 本系统 customer 字段 */
export const ICC_ORG_FIELDS = {
orgCode: "iccCode", // ICC 客户编码(同步匹配键)
orgName: "customerName", // 组织名称 → 客户名称
province: "province", // 省
city: "city", // 市
county: "district" // 县/区 → 区县
} as const;
/** 将 ICC 原始组织记录映射为本系统 customer 落库字段(字符串 trim,空串转 undefined)。 */
export function mapIccOrgRecord(raw: IccOrgRawRecord): {
iccCode: string;
customerName: string;
province?: string;
city?: string;
district?: string;
orgType?: string;
orgNameRaw?: string;
} {
const result: Record<string, any> = {
orgType: toOptionalString(raw.orgType), // 组织类型,用于 org_base 过滤判断
orgNameRaw: toOptionalString(raw.orgName) // 原始组织名称,便于核对
};
// 遍历 ICC_ORG_FIELDS(ICC 返回字段名 → 本系统字段名),从 raw 按 ICC 字段名取值
for (const [iccField, targetField] of Object.entries(ICC_ORG_FIELDS)) {
const value = raw[iccField];
result[targetField] =
iccField === "orgCode" || iccField === "orgName"
? String(value ?? "").trim() // 匹配键/必填名保持空串语义,由调用方判跳
: toOptionalString(value);
}
return result as any;
}
/** 字符串 trim;null / undefined / 空串统一归一为 undefined。 */
function toOptionalString(value: unknown): string | undefined {
if (value === undefined || value === null) return undefined;
const s = String(value).trim();
return s === "" ? undefined : s;
}
/**
* 解析组织分页响应:
* - payload 为数组 → 直接作为列表
* - payload.data 为 PageVO({ currentPage, totalPage, pageSize, totalRows, pageData })→ 取 pageData
* - 结构不匹配或解析异常 → 返回空列表
*/
export function parseOrgPage(payload: any): {
list: IccOrgRawRecord[];
currentPage: number;
totalPage: number;
} {
try {
if (Array.isArray(payload)) {
return { list: payload, currentPage: 1, totalPage: 1 };
}
const page = payload?.data;
if (page && Array.isArray(page.pageData)) {
return {
list: page.pageData,
currentPage: Number(page.currentPage) || 1,
totalPage: Number(page.totalPage) || 1
};
}
return { list: [], currentPage: 1, totalPage: 1 };
} catch {
return { list: [], currentPage: 1, totalPage: 1 };
}
}