audit-company-info-coverage.mjs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * 审计:`company_info` 里的字段,有多少真的落进了 DMS 的企业两栏目。
  3. *
  4. * 为什么要有这个脚本:`company_info`(企查查原始结构)字段很多,而 1888 的列
  5. * 是照着它建的(47 列里大部分是工商信息),**但我们的同步只写其中一小部分**。
  6. * 字段一多,靠记忆或靠文档核对必然漏;这个脚本把三边对齐:
  7. * ① 真实载荷里实际有哪些字段(`company / profile.data / profile.source / honors`)
  8. * ② DMS 模型里实际有哪些列(**实时** `getModelById`,不用记录值)
  9. * ③ 我们的代码实际写哪些列(在脚本里显式列出,与 src 对齐)
  10. *
  11. * 怎么跑(需要 dev server 起着,代理注入 DMS token):
  12. * node harness/tools/audit-company-info-coverage.mjs <company-info.json> [dms 代理地址]
  13. *
  14. * 载荷怎么来:把 `/api/chat` 的 `result` 事件里 `data.company_info` 对象存成 JSON 文件
  15. * (整个对象,不含外层 company_info 键)。注意用 node 写文件,别用 shell 里的 curl -d 传中文。
  16. */
  17. process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  18. import { readFileSync } from 'node:fs';
  19. const payloadPath = process.argv[2];
  20. const dmsBase = process.argv[3] || process.env.DMS_BASE || 'https://localhost:8083/dms-api';
  21. if (!payloadPath) {
  22. console.error('用法:node audit-company-info-coverage.mjs <company-info.json> [dms 代理地址]');
  23. process.exit(2);
  24. }
  25. globalThis.VITE_DMS_API = dmsBase;
  26. // ⚠️ 注意取的是 **modelId**(2036/2032),不是 columnId(1888/1886)——
  27. // 传错会把别的模型拉回来(踩过:1888 当 modelId 用,拉回来一个带 c_milvus_chunk_ids 的知识库模型)
  28. const {
  29. DMS_MODEL_ENTERPRISE,
  30. DMS_MODEL_HONOR,
  31. PROFILE_FIELD_MAP,
  32. } = await import('./_company-classify.mjs');
  33. /* ------------------------------------------------------------------ *
  34. * 我们的代码实际写入的列(与 src 对齐,改代码时记得同步这里)
  35. * 来源:src/network/api/dms/company-info-sync.ts 与 classification-sync-utils.ts
  36. * ------------------------------------------------------------------ */
  37. const WE_WRITE_1888 = [
  38. // 工商信息:**直接从代码的映射表派生**,避免这份清单与 src 漂移
  39. ...Object.values(PROFILE_FIELD_MAP),
  40. 'c_created_at', // 仅新增时
  41. // 六个分类角度标签(JSON 数组字符串)
  42. 'c_tag_basic',
  43. 'c_tag_honor',
  44. 'c_tag_sector',
  45. 'c_tag_operation',
  46. 'c_tag_industry',
  47. 'c_tag_license',
  48. ];
  49. const WE_WRITE_1886 = [
  50. 'c_id', // 只传 0(must 字段,DMS 自动填)
  51. 'c_credit_code',
  52. 'c_name',
  53. 'c_honor', // 荣誉名(并写系统字段 title)
  54. 'c_level',
  55. 'c_source',
  56. 'c_publish_office',
  57. 'c_publish_date',
  58. 'c_beging_date',
  59. 'c_dead_line',
  60. 'c_certificate_code',
  61. 'c_created_at',
  62. ];
  63. /** company_info 字段 → 1888 列(语义对应;null = DMS 没有这一列) */
  64. const MAP_PROFILE_DATA = {
  65. Name: 'c_name',
  66. CreditCode: 'c_credit_code',
  67. OperName: 'c_oper_name',
  68. BelongOrg: 'c_belong_org',
  69. OperId: 'c_oper_id',
  70. EntType: 'c_ent_type',
  71. EconKind: 'c_econ_kind',
  72. Status: 'c_status',
  73. Province: 'c_province',
  74. AreaCode: 'c_area_code',
  75. Address: 'c_address',
  76. StartDate: 'c_start_date',
  77. EndDate: 'c_end_date',
  78. TermStart: 'c_term_start',
  79. TermEnd: 'c_term_end',
  80. CheckDate: 'c_check_date',
  81. UpdatedDate: 'c_updated_date',
  82. RegistCapi: 'c_regist_capi',
  83. RegisteredCapital: 'c_registered_capital',
  84. RegisteredCapitalUnit: 'c_registered_capital_unit',
  85. RegisteredCapitalCCY: 'c_registered_capital_ccy',
  86. RecCap: 'c_rec_cap',
  87. PaidUpCapital: 'c_paid_up_capital',
  88. PaidUpCapitalUnit: 'c_paid_up_capital_unit',
  89. PaidUpCapitalCCY: 'c_paid_up_capital_ccy',
  90. IsOnStock: 'c_is_on_stock',
  91. StockNumber: 'c_stock_number',
  92. StockType: 'c_stock_type',
  93. OrgNo: 'c_org_no',
  94. No: 'c_no',
  95. ImageUrl: 'c_image_url',
  96. Scope: 'c_scope',
  97. DesignatedRepresentativeList: 'c_designated_representative_list',
  98. OriginalName: 'c_original_name',
  99. RevokeInfo: 'c_revoke_info',
  100. Area: 'c_area',
  101. };
  102. const MAP_COMPANY = {
  103. KeyNo: null,
  104. Name: 'c_name',
  105. CreditCode: 'c_credit_code',
  106. StartDate: 'c_start_date',
  107. OperName: 'c_oper_name',
  108. Status: 'c_status',
  109. No: 'c_no',
  110. Address: 'c_address',
  111. };
  112. const MAP_HONOR_RECORD = {
  113. Name: 'c_honor(并写系统 title)',
  114. Level: 'c_level',
  115. Source: 'c_source',
  116. PublishOffice: 'c_publish_office',
  117. PublishDate: 'c_publish_date',
  118. BegingDate: 'c_beging_date',
  119. DeadLine: 'c_dead_line',
  120. CertificateCode: 'c_certificate_code',
  121. };
  122. /* ------------------------------------------------------------------ *
  123. * 读输入
  124. * ------------------------------------------------------------------ */
  125. const payload = JSON.parse(readFileSync(payloadPath, 'utf8'));
  126. const fetchModelFields = async (modelId, expectAlias) => {
  127. const form = new URLSearchParams({ modelId: String(modelId) }).toString();
  128. const res = await fetch(`${dmsBase}/model/getModelById`, {
  129. method: 'POST',
  130. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  131. body: form,
  132. });
  133. const json = await res.json().catch(() => null);
  134. if (!json || json.code !== 200) {
  135. throw new Error(`取模型 ${modelId} 失败:${json ? `${json.code} ${json.message}` : res.status}`);
  136. }
  137. // 防呆:modelId 传错时 DMS 不会报错、只会返回另一个模型,所以校验别名
  138. if (json.content.modelAlias !== expectAlias) {
  139. throw new Error(
  140. `模型 ${modelId} 的别名是「${json.content.modelAlias}」,期望「${expectAlias}」——modelId 传错了吧?`
  141. );
  142. }
  143. const list = JSON.parse(json.content.fieldList);
  144. return Object.values(list)
  145. .filter((f) => f.name) // 个别条目 name 为空
  146. .sort((a, b) => a.sequence - b.sequence);
  147. };
  148. const fields1888 = await fetchModelFields(DMS_MODEL_ENTERPRISE, '企业基础信息');
  149. const fields1886 = await fetchModelFields(DMS_MODEL_HONOR, '企业荣誉信息');
  150. const cols1888 = fields1888.map((f) => f.name);
  151. const cols1886 = fields1886.map((f) => f.name);
  152. const out = [];
  153. const line = (s = '') => out.push(s);
  154. line(`载荷:${payloadPath}`);
  155. line(`DMS 模型:1888 企业基础信息 ${cols1888.length} 字段 / 1886 企业荣誉信息 ${cols1886.length} 字段`);
  156. line();
  157. line('===== 载荷结构 =====');
  158. line(`顶层键:${Object.keys(payload).join(', ')}`);
  159. line(`company:${Object.keys(payload.company || {}).join(', ')}`);
  160. const profileData = payload.profile?.data || {};
  161. line(`profile.data(${Object.keys(profileData).length}):${Object.keys(profileData).join(', ')}`);
  162. line(`profile.source:${Object.keys(payload.profile?.source || {}).join(', ')}`);
  163. const honorRecordData = payload.honors?.records?.[0]?.data || {};
  164. line(`honors:${Object.keys(payload.honors || {}).join(', ')}`);
  165. line(`honors.records[].data:${Object.keys(honorRecordData).join(', ')}`);
  166. line(`honors.sources[]:${Object.keys(payload.honors?.sources?.[0] || {}).join(', ')}`);
  167. line();
  168. line('===== ① profile.data → 1888:已写 / 未写 =====');
  169. const wrote = [];
  170. const notWrote = [];
  171. const noColumn = [];
  172. for (const key of Object.keys(profileData)) {
  173. const col = MAP_PROFILE_DATA[key];
  174. if (col === undefined) noColumn.push(key);
  175. else if (col === null) noColumn.push(key);
  176. else if (WE_WRITE_1888.includes(col)) wrote.push([key, col]);
  177. else notWrote.push([key, col]);
  178. }
  179. line(`【已写入 DMS】${wrote.length} 个:`);
  180. for (const [k, c] of wrote) line(` profile.data.${k.padEnd(30)} → ${c}`);
  181. line(`【未写入(DMS 里已有现成的列,只是同步没写)】${notWrote.length} 个:`);
  182. for (const [k, c] of notWrote) line(` profile.data.${k.padEnd(30)} → ${c}`);
  183. line(`【DMS 没有对应列】${noColumn.length} 个:${noColumn.join(', ') || '(无)'}`);
  184. line();
  185. line('===== ② company 段 =====');
  186. for (const key of Object.keys(payload.company || {})) {
  187. const col = MAP_COMPANY[key];
  188. const state = col === null || col === undefined ? '**DMS 无对应列**' : WE_WRITE_1888.includes(col) ? `已写(${col},走 profile 同名字段)` : `未写(${col})`;
  189. line(` company.${key.padEnd(16)} → ${state}`);
  190. }
  191. line();
  192. line('===== ③ honors → 1886 =====');
  193. line('honors.records[].data:');
  194. for (const [k, c] of Object.entries(MAP_HONOR_RECORD)) {
  195. const ok = c.startsWith('c_honor') ? WE_WRITE_1886.includes('c_honor') : WE_WRITE_1886.includes(c);
  196. line(` ${k.padEnd(16)} → ${c} ${ok ? '【已写】' : '【未写】'}`);
  197. }
  198. line(` honors.complete / error、honors.sources[](${Object.keys(payload.honors?.sources?.[0] || {}).join('/')}):**不写**(只用于判断能否删除)`);
  199. line();
  200. line('===== ④ profile.source =====');
  201. line(` ${Object.keys(payload.profile?.source || {}).join(', ')}`);
  202. line(' → **全部不写**:1888 没有对应列,属溯源元信息(荣誉那边的 c_source 是"来源通知名",不是这个)');
  203. line();
  204. line('===== ⑤ 汇总:1888 的 47 列里,我们在写哪些 =====');
  205. const unused1888 = cols1888.filter((c) => !WE_WRITE_1888.includes(c));
  206. line(` 在写 ${WE_WRITE_1888.length} 列`);
  207. line(` 没写 ${unused1888.length} 列:${unused1888.join(', ')}`);
  208. line();
  209. line('===== ⑥ 汇总:1886 的 13 列 =====');
  210. const unused1886 = cols1886.filter((c) => !WE_WRITE_1886.includes(c));
  211. line(` 在写 ${WE_WRITE_1886.length} 列`);
  212. line(` 没写 ${unused1886.length} 列:${unused1886.join(', ') || '(无)'}`);
  213. const report = out.join('\n');
  214. console.log(report);
  215. if (process.env.WRITE_REPORT) {
  216. const { writeFileSync } = await import('node:fs');
  217. writeFileSync(process.env.WRITE_REPORT, report, 'utf8');
  218. console.log(`\n(已写入 ${process.env.WRITE_REPORT})`);
  219. }