|
|
@@ -0,0 +1,238 @@
|
|
|
+/**
|
|
|
+ * 审计:`company_info` 里的字段,有多少真的落进了 DMS 的企业两栏目。
|
|
|
+ *
|
|
|
+ * 为什么要有这个脚本:`company_info`(企查查原始结构)字段很多,而 1888 的列
|
|
|
+ * 是照着它建的(47 列里大部分是工商信息),**但我们的同步只写其中一小部分**。
|
|
|
+ * 字段一多,靠记忆或靠文档核对必然漏;这个脚本把三边对齐:
|
|
|
+ * ① 真实载荷里实际有哪些字段(`company / profile.data / profile.source / honors`)
|
|
|
+ * ② DMS 模型里实际有哪些列(**实时** `getModelById`,不用记录值)
|
|
|
+ * ③ 我们的代码实际写哪些列(在脚本里显式列出,与 src 对齐)
|
|
|
+ *
|
|
|
+ * 怎么跑(需要 dev server 起着,代理注入 DMS token):
|
|
|
+ * node harness/tools/audit-company-info-coverage.mjs <company-info.json> [dms 代理地址]
|
|
|
+ *
|
|
|
+ * 载荷怎么来:把 `/api/chat` 的 `result` 事件里 `data.company_info` 对象存成 JSON 文件
|
|
|
+ * (整个对象,不含外层 company_info 键)。注意用 node 写文件,别用 shell 里的 curl -d 传中文。
|
|
|
+ */
|
|
|
+
|
|
|
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
|
+
|
|
|
+import { readFileSync } from 'node:fs';
|
|
|
+
|
|
|
+const payloadPath = process.argv[2];
|
|
|
+const dmsBase = process.argv[3] || process.env.DMS_BASE || 'https://localhost:8083/dms-api';
|
|
|
+if (!payloadPath) {
|
|
|
+ console.error('用法:node audit-company-info-coverage.mjs <company-info.json> [dms 代理地址]');
|
|
|
+ process.exit(2);
|
|
|
+}
|
|
|
+globalThis.VITE_DMS_API = dmsBase;
|
|
|
+
|
|
|
+// ⚠️ 注意取的是 **modelId**(2036/2032),不是 columnId(1888/1886)——
|
|
|
+// 传错会把别的模型拉回来(踩过:1888 当 modelId 用,拉回来一个带 c_milvus_chunk_ids 的知识库模型)
|
|
|
+const {
|
|
|
+ DMS_MODEL_ENTERPRISE,
|
|
|
+ DMS_MODEL_HONOR,
|
|
|
+} = await import('./_company-classify.mjs');
|
|
|
+
|
|
|
+/* ------------------------------------------------------------------ *
|
|
|
+ * 我们的代码实际写入的列(与 src 对齐,改代码时记得同步这里)
|
|
|
+ * 来源:src/network/api/dms/company-info-sync.ts 与 classification-sync-utils.ts
|
|
|
+ * ------------------------------------------------------------------ */
|
|
|
+const WE_WRITE_1888 = [
|
|
|
+ 'c_credit_code', // 幂等键,来自身份
|
|
|
+ 'c_name', // 身份(分类响应 → 回退 profile.data.Name → company.Name)
|
|
|
+ 'c_oper_name', // 法人
|
|
|
+ 'c_created_at', // 仅新增时
|
|
|
+ // 六个分类角度标签(JSON 数组字符串)
|
|
|
+ 'c_tag_basic',
|
|
|
+ 'c_tag_honor',
|
|
|
+ 'c_tag_sector',
|
|
|
+ 'c_tag_operation',
|
|
|
+ 'c_tag_industry',
|
|
|
+ 'c_tag_license',
|
|
|
+];
|
|
|
+const WE_WRITE_1886 = [
|
|
|
+ 'c_id', // 只传 0(must 字段,DMS 自动填)
|
|
|
+ 'c_credit_code',
|
|
|
+ 'c_name',
|
|
|
+ 'c_honor', // 荣誉名(并写系统字段 title)
|
|
|
+ 'c_level',
|
|
|
+ 'c_source',
|
|
|
+ 'c_publish_office',
|
|
|
+ 'c_publish_date',
|
|
|
+ 'c_beging_date',
|
|
|
+ 'c_dead_line',
|
|
|
+ 'c_certificate_code',
|
|
|
+ 'c_created_at',
|
|
|
+];
|
|
|
+
|
|
|
+/** company_info 字段 → 1888 列(语义对应;null = DMS 没有这一列) */
|
|
|
+const MAP_PROFILE_DATA = {
|
|
|
+ Name: 'c_name',
|
|
|
+ CreditCode: 'c_credit_code',
|
|
|
+ OperName: 'c_oper_name',
|
|
|
+ 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',
|
|
|
+ No: 'c_no',
|
|
|
+ ImageUrl: 'c_image_url',
|
|
|
+ Scope: 'c_scope',
|
|
|
+ DesignatedRepresentativeList: 'c_designated_representative_list',
|
|
|
+ OriginalName: 'c_original_name',
|
|
|
+ RevokeInfo: 'c_revoke_info',
|
|
|
+ Area: 'c_area',
|
|
|
+};
|
|
|
+const MAP_COMPANY = {
|
|
|
+ KeyNo: null,
|
|
|
+ Name: 'c_name',
|
|
|
+ CreditCode: 'c_credit_code',
|
|
|
+ StartDate: 'c_start_date',
|
|
|
+ OperName: 'c_oper_name',
|
|
|
+ Status: 'c_status',
|
|
|
+ No: 'c_no',
|
|
|
+ Address: 'c_address',
|
|
|
+};
|
|
|
+const MAP_HONOR_RECORD = {
|
|
|
+ Name: 'c_honor(并写系统 title)',
|
|
|
+ Level: 'c_level',
|
|
|
+ Source: 'c_source',
|
|
|
+ PublishOffice: 'c_publish_office',
|
|
|
+ PublishDate: 'c_publish_date',
|
|
|
+ BegingDate: 'c_beging_date',
|
|
|
+ DeadLine: 'c_dead_line',
|
|
|
+ CertificateCode: 'c_certificate_code',
|
|
|
+};
|
|
|
+
|
|
|
+/* ------------------------------------------------------------------ *
|
|
|
+ * 读输入
|
|
|
+ * ------------------------------------------------------------------ */
|
|
|
+const payload = JSON.parse(readFileSync(payloadPath, 'utf8'));
|
|
|
+
|
|
|
+const fetchModelFields = async (modelId, expectAlias) => {
|
|
|
+ const form = new URLSearchParams({ modelId: String(modelId) }).toString();
|
|
|
+ const res = await fetch(`${dmsBase}/model/getModelById`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
+ body: form,
|
|
|
+ });
|
|
|
+ const json = await res.json().catch(() => null);
|
|
|
+ if (!json || json.code !== 200) {
|
|
|
+ throw new Error(`取模型 ${modelId} 失败:${json ? `${json.code} ${json.message}` : res.status}`);
|
|
|
+ }
|
|
|
+ // 防呆:modelId 传错时 DMS 不会报错、只会返回另一个模型,所以校验别名
|
|
|
+ if (json.content.modelAlias !== expectAlias) {
|
|
|
+ throw new Error(
|
|
|
+ `模型 ${modelId} 的别名是「${json.content.modelAlias}」,期望「${expectAlias}」——modelId 传错了吧?`
|
|
|
+ );
|
|
|
+ }
|
|
|
+ const list = JSON.parse(json.content.fieldList);
|
|
|
+ return Object.values(list)
|
|
|
+ .filter((f) => f.name) // 个别条目 name 为空
|
|
|
+ .sort((a, b) => a.sequence - b.sequence);
|
|
|
+};
|
|
|
+
|
|
|
+const fields1888 = await fetchModelFields(DMS_MODEL_ENTERPRISE, '企业基础信息');
|
|
|
+const fields1886 = await fetchModelFields(DMS_MODEL_HONOR, '企业荣誉信息');
|
|
|
+const cols1888 = fields1888.map((f) => f.name);
|
|
|
+const cols1886 = fields1886.map((f) => f.name);
|
|
|
+
|
|
|
+const out = [];
|
|
|
+const line = (s = '') => out.push(s);
|
|
|
+
|
|
|
+line(`载荷:${payloadPath}`);
|
|
|
+line(`DMS 模型:1888 企业基础信息 ${cols1888.length} 字段 / 1886 企业荣誉信息 ${cols1886.length} 字段`);
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== 载荷结构 =====');
|
|
|
+line(`顶层键:${Object.keys(payload).join(', ')}`);
|
|
|
+line(`company:${Object.keys(payload.company || {}).join(', ')}`);
|
|
|
+const profileData = payload.profile?.data || {};
|
|
|
+line(`profile.data(${Object.keys(profileData).length}):${Object.keys(profileData).join(', ')}`);
|
|
|
+line(`profile.source:${Object.keys(payload.profile?.source || {}).join(', ')}`);
|
|
|
+const honorRecordData = payload.honors?.records?.[0]?.data || {};
|
|
|
+line(`honors:${Object.keys(payload.honors || {}).join(', ')}`);
|
|
|
+line(`honors.records[].data:${Object.keys(honorRecordData).join(', ')}`);
|
|
|
+line(`honors.sources[]:${Object.keys(payload.honors?.sources?.[0] || {}).join(', ')}`);
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ① profile.data → 1888:已写 / 未写 =====');
|
|
|
+const wrote = [];
|
|
|
+const notWrote = [];
|
|
|
+const noColumn = [];
|
|
|
+for (const key of Object.keys(profileData)) {
|
|
|
+ const col = MAP_PROFILE_DATA[key];
|
|
|
+ if (col === undefined) noColumn.push(key);
|
|
|
+ else if (col === null) noColumn.push(key);
|
|
|
+ else if (WE_WRITE_1888.includes(col)) wrote.push([key, col]);
|
|
|
+ else notWrote.push([key, col]);
|
|
|
+}
|
|
|
+line(`【已写入 DMS】${wrote.length} 个:`);
|
|
|
+for (const [k, c] of wrote) line(` profile.data.${k.padEnd(30)} → ${c}`);
|
|
|
+line(`【未写入(DMS 里已有现成的列,只是同步没写)】${notWrote.length} 个:`);
|
|
|
+for (const [k, c] of notWrote) line(` profile.data.${k.padEnd(30)} → ${c}`);
|
|
|
+line(`【DMS 没有对应列】${noColumn.length} 个:${noColumn.join(', ') || '(无)'}`);
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ② company 段 =====');
|
|
|
+for (const key of Object.keys(payload.company || {})) {
|
|
|
+ const col = MAP_COMPANY[key];
|
|
|
+ const state = col === null || col === undefined ? '**DMS 无对应列**' : WE_WRITE_1888.includes(col) ? `已写(${col},走 profile 同名字段)` : `未写(${col})`;
|
|
|
+ line(` company.${key.padEnd(16)} → ${state}`);
|
|
|
+}
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ③ honors → 1886 =====');
|
|
|
+line('honors.records[].data:');
|
|
|
+for (const [k, c] of Object.entries(MAP_HONOR_RECORD)) {
|
|
|
+ const ok = c.startsWith('c_honor') ? WE_WRITE_1886.includes('c_honor') : WE_WRITE_1886.includes(c);
|
|
|
+ line(` ${k.padEnd(16)} → ${c} ${ok ? '【已写】' : '【未写】'}`);
|
|
|
+}
|
|
|
+line(` honors.complete / error、honors.sources[](${Object.keys(payload.honors?.sources?.[0] || {}).join('/')}):**不写**(只用于判断能否删除)`);
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ④ profile.source =====');
|
|
|
+line(` ${Object.keys(payload.profile?.source || {}).join(', ')}`);
|
|
|
+line(' → **全部不写**:1888 没有对应列,属溯源元信息(荣誉那边的 c_source 是"来源通知名",不是这个)');
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ⑤ 汇总:1888 的 47 列里,我们在写哪些 =====');
|
|
|
+const unused1888 = cols1888.filter((c) => !WE_WRITE_1888.includes(c));
|
|
|
+line(` 在写 ${WE_WRITE_1888.length} 列`);
|
|
|
+line(` 没写 ${unused1888.length} 列:${unused1888.join(', ')}`);
|
|
|
+line();
|
|
|
+
|
|
|
+line('===== ⑥ 汇总:1886 的 13 列 =====');
|
|
|
+const unused1886 = cols1886.filter((c) => !WE_WRITE_1886.includes(c));
|
|
|
+line(` 在写 ${WE_WRITE_1886.length} 列`);
|
|
|
+line(` 没写 ${unused1886.length} 列:${unused1886.join(', ') || '(无)'}`);
|
|
|
+
|
|
|
+const report = out.join('\n');
|
|
|
+console.log(report);
|
|
|
+if (process.env.WRITE_REPORT) {
|
|
|
+ const { writeFileSync } = await import('node:fs');
|
|
|
+ writeFileSync(process.env.WRITE_REPORT, report, 'utf8');
|
|
|
+ console.log(`\n(已写入 ${process.env.WRITE_REPORT})`);
|
|
|
+}
|