|
|
@@ -252,6 +252,119 @@ export const buildEnterpriseRow = (
|
|
|
return row;
|
|
|
};
|
|
|
|
|
|
+/* ------------------------------------------------------------------ *
|
|
|
+ * 1888 工商信息(数据源 = company_info.profile.data)
|
|
|
+ * ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * `profile.data` → 1888 列。
|
|
|
+ *
|
|
|
+ * ⚠️ 这些列**本来就是照着企查查这套字段建的**(DMS 侧从源表 `qcc_enterprise` 灌数据时用的同一套),
|
|
|
+ * 所以是直接对应,没有派生计算。2026-09-20 用户要求把这些字段也同步进来
|
|
|
+ * (此前只写身份三字段 + 分类标签)。
|
|
|
+ */
|
|
|
+export const PROFILE_FIELD_MAP: Record<string, string> = {
|
|
|
+ Name: "c_name",
|
|
|
+ CreditCode: "c_credit_code",
|
|
|
+ OperName: "c_oper_name",
|
|
|
+ No: "c_no",
|
|
|
+ BelongOrg: "c_belong_org",
|
|
|
+ OperId: "c_oper_id",
|
|
|
+ EntType: "c_ent_type",
|
|
|
+ EconKind: "c_econ_kind",
|
|
|
+ Status: "c_status",
|
|
|
+ Province: "c_province",
|
|
|
+ AreaCode: "c_area_code",
|
|
|
+ Address: "c_address",
|
|
|
+ StartDate: "c_start_date",
|
|
|
+ EndDate: "c_end_date",
|
|
|
+ TermStart: "c_term_start",
|
|
|
+ TermEnd: "c_term_end",
|
|
|
+ CheckDate: "c_check_date",
|
|
|
+ UpdatedDate: "c_updated_date",
|
|
|
+ RegistCapi: "c_regist_capi",
|
|
|
+ RegisteredCapital: "c_registered_capital",
|
|
|
+ RegisteredCapitalUnit: "c_registered_capital_unit",
|
|
|
+ RegisteredCapitalCCY: "c_registered_capital_ccy",
|
|
|
+ RecCap: "c_rec_cap",
|
|
|
+ PaidUpCapital: "c_paid_up_capital",
|
|
|
+ PaidUpCapitalUnit: "c_paid_up_capital_unit",
|
|
|
+ PaidUpCapitalCCY: "c_paid_up_capital_ccy",
|
|
|
+ IsOnStock: "c_is_on_stock",
|
|
|
+ StockNumber: "c_stock_number",
|
|
|
+ StockType: "c_stock_type",
|
|
|
+ OrgNo: "c_org_no",
|
|
|
+ ImageUrl: "c_image_url",
|
|
|
+ Scope: "c_scope",
|
|
|
+ DesignatedRepresentativeList: "c_designated_representative_list",
|
|
|
+ OriginalName: "c_original_name",
|
|
|
+ RevokeInfo: "c_revoke_info",
|
|
|
+ Area: "c_area",
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 值是「数组/对象」的列:存 JSON 字符串(与 DMS 侧灌入的形态一致——那边是 jsonb 序列化过来的)。
|
|
|
+ * ⚠️ 比较时**不能按字符串比**:Python 的 `json.dumps` 带空格、jsonb 还会按字母序排键,
|
|
|
+ * 与 JS 的 `JSON.stringify` 输出不同,直接比会每轮都判成「有差异」而反复写。
|
|
|
+ */
|
|
|
+const JSON_COLUMNS = new Set([
|
|
|
+ "c_designated_representative_list",
|
|
|
+ "c_original_name",
|
|
|
+ "c_revoke_info",
|
|
|
+ "c_area",
|
|
|
+ "c_industry",
|
|
|
+]);
|
|
|
+
|
|
|
+/** 递归按键排序后序列化,用于 JSON 列的**语义比较**(与键序、空白无关) */
|
|
|
+export const stableJson = (value: unknown): string => {
|
|
|
+ const walk = (node: unknown): unknown => {
|
|
|
+ if (Array.isArray(node)) return node.map(walk);
|
|
|
+ if (node && typeof node === "object") {
|
|
|
+ const source = node as Record<string, unknown>;
|
|
|
+ return Object.keys(source)
|
|
|
+ .sort()
|
|
|
+ .reduce<Record<string, unknown>>((acc, key) => {
|
|
|
+ acc[key] = walk(source[key]);
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+ return node;
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ return JSON.stringify(walk(value));
|
|
|
+ } catch {
|
|
|
+ return String(value);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 单个值的归一化:空值一律返回 null(**不写**,也就不清存量) */
|
|
|
+const normalizeProfileValue = (raw: unknown): string | null => {
|
|
|
+ if (raw === null || raw === undefined) return null;
|
|
|
+ if (typeof raw === "string") return pickString(raw);
|
|
|
+ if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
|
+ if (Array.isArray(raw)) return raw.length ? JSON.stringify(raw) : null; // 空数组:不写
|
|
|
+ if (typeof raw === "object") {
|
|
|
+ return Object.keys(raw as Record<string, unknown>).length ? JSON.stringify(raw) : null;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+};
|
|
|
+
|
|
|
+/** 组装 1888 的工商信息字段(只含本轮有值的;空值不写) */
|
|
|
+export const buildProfileFields = (profileData: unknown): DmsRow => {
|
|
|
+ const data = asRecord(profileData);
|
|
|
+ const row: DmsRow = {};
|
|
|
+ if (!data) return row;
|
|
|
+ for (const [source, column] of Object.entries(PROFILE_FIELD_MAP)) {
|
|
|
+ const value = normalizeProfileValue(data[source]);
|
|
|
+ if (value !== null) row[column] = value;
|
|
|
+ }
|
|
|
+ return row;
|
|
|
+};
|
|
|
+
|
|
|
+/** 从 `company_info` 里取 `profile.data`(工商信息的来源;不是对象就返回 null) */
|
|
|
+export const readProfileDataFromCompanyInfo = (info: unknown): unknown =>
|
|
|
+ asRecord(asRecord(asRecord(info)?.profile)?.data) ?? null;
|
|
|
+
|
|
|
/* ------------------------------------------------------------------ *
|
|
|
* 1888 diff
|
|
|
* ------------------------------------------------------------------ */
|
|
|
@@ -259,11 +372,27 @@ export const buildEnterpriseRow = (
|
|
|
/** 标签字段集合(比较方式与身份字段不同:按集合比) */
|
|
|
const TAG_FIELDS = new Set<string>(Object.values(ANGLE_TO_FIELD));
|
|
|
|
|
|
+/** 字符串形式的 JSON 解析回值;空串当作「没有」,解析不了就当普通字符串 */
|
|
|
+const parseIfJson = (raw: unknown): unknown => {
|
|
|
+ if (typeof raw !== "string") return raw;
|
|
|
+ const trimmed = raw.trim();
|
|
|
+ if (!trimmed) return null;
|
|
|
+ try {
|
|
|
+ return JSON.parse(trimmed);
|
|
|
+ } catch {
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* 期望行 vs 现有行 → 需要写的字段(无变化返回 null,调用方据此跳过 update)。
|
|
|
*
|
|
|
- * - 标签字段:decode 后按**集合**比较,防 `"[]"` 与 `[]`、顺序不同这类假差异
|
|
|
- * - 身份字段:期望值非空且与现有值逐字不同才写;期望值为空一律不写(不清存量)
|
|
|
+ * 三类字段的比较方式不同:
|
|
|
+ * - **标签字段**(`c_tag_*`):decode 后按**集合**比较,防 `"[]"` 与 `[]`、顺序不同这类假差异
|
|
|
+ * - **JSON 列**(`c_area` / `c_original_name` 等):先解析再**按键排序**比较——
|
|
|
+ * DMS 里灌入的值是 Python/jsonb 序列化的(带空格、键按字母序),
|
|
|
+ * 与 JS 的 `JSON.stringify` 输出不同,直接比字符串会每轮都误判成「有差异」
|
|
|
+ * - **普通文本列**:期望值非空且与现有值(去空白后)不同才写;期望为空一律不写(**不清存量**)
|
|
|
*/
|
|
|
export const diffEnterpriseRow = (
|
|
|
existing: DmsRow | null,
|
|
|
@@ -277,6 +406,12 @@ export const diffEnterpriseRow = (
|
|
|
}
|
|
|
continue;
|
|
|
}
|
|
|
+ if (JSON_COLUMNS.has(field)) {
|
|
|
+ if (stableJson(parseIfJson(existing?.[field])) !== stableJson(parseIfJson(value))) {
|
|
|
+ patch[field] = value;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
const next = pickString(value);
|
|
|
if (!next) continue;
|
|
|
const current = pickString(existing?.[field]);
|