verify-company-classify-dms.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /**
  2. * 打**真实 DMS**验证企业信息同步的落库行为(栏目 1888 企业基础信息 / 1886 企业荣誉信息)。
  3. *
  4. * 用**注入的假数据**(假的分类结果 + 假的荣誉列表)跑完整落库链路,因此:
  5. * - 不消耗分类接口的 LLM 调用,可以反复跑
  6. * - 覆盖边界(改标签、删荣誉、complete=false 不许删、缺信用代码跳过…)
  7. *
  8. * 验证什么:
  9. * ① 1888 首写:一行,六个标签字段 = 对应角度的 JSON 数组
  10. * ② 1886 首写:**一条荣誉一行**,title 与 c_honor 都是荣誉名,级别/来源/机构/日期/证书号各就各位
  11. * ③ 幂等:同样数据再跑一次 → 零新增、行 uuid 不变
  12. * ④ 改:1888 标签变化 → 仍是同一行且已更新;1886 少一条/多一条/改字段 →
  13. * 删多余、补新增、更新差异,行数与后端列表一致
  14. * ⑤ `honors.complete = false` → **只增改不删除**(记录集不完整时不许删)
  15. * ⑥ 缺信用代码 → 整条跳过,不写任何行
  16. * ⑦ 清理:按测试信用代码删掉本次产生的全部行,复查残留 0
  17. *
  18. * 怎么跑(先按 _entry-company-classify.ts 头部注释重新打包):
  19. * 1) npm run dev # 代理负责注入 DMS token
  20. * 2) node harness/tools/verify-company-classify-dms.mjs https://localhost:8083/dms-api
  21. *
  22. * ⚠️ 脚本**不持有 token**(和浏览器一样只打代理地址);测试数据用可识别的信用代码前缀,
  23. * 结束时自行清理。
  24. */
  25. process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  26. const base = process.argv[2] || 'https://localhost:8083/dms-api';
  27. globalThis.VITE_DMS_API = base;
  28. const {
  29. applyCompanyClassification,
  30. applyCompanyHonors,
  31. HONOR_NAME_FIELD,
  32. ANGLE_TO_FIELD,
  33. DMS_COLUMN_ENTERPRISE,
  34. DMS_COLUMN_HONOR,
  35. DMS_MODEL_HONOR,
  36. addDmsContent,
  37. deleteDmsContent,
  38. searchDmsContents,
  39. } = await import('./_company-classify.mjs');
  40. let pass = 0;
  41. let fail = 0;
  42. const check = (name, ok, extra = '') => {
  43. if (ok) {
  44. pass++;
  45. console.log(` ok ${name}`);
  46. } else {
  47. fail++;
  48. console.log(` FAIL ${name}${extra === '' ? '' : ` → ${JSON.stringify(extra)}`}`);
  49. }
  50. };
  51. const TEST_CREDIT_CODE = `TEST_CC_${Date.now()}`;
  52. const TEST_NAME = '分类同步测试公司';
  53. const IDENTITY = { name: TEST_NAME, creditCode: TEST_CREDIT_CODE, legalRep: '测试法人' };
  54. /** 造一条企查查形态的荣誉记录(`records[].data`) */
  55. const rec = (Name, Level, Source, extra = {}) => ({
  56. data: { Name, Level, Source, ...extra },
  57. });
  58. const honorsNode = (list, complete = true) => ({ complete, records: list, sources: [], error: null });
  59. const findEnterprise = () =>
  60. searchDmsContents(DMS_COLUMN_ENTERPRISE, {
  61. search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
  62. page: 0,
  63. pageSize: 10,
  64. });
  65. const findHonors = () =>
  66. searchDmsContents(DMS_COLUMN_HONOR, {
  67. search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
  68. page: 0,
  69. pageSize: 200,
  70. });
  71. const honorNames = (rows) =>
  72. rows
  73. .map((r) => (typeof r[HONOR_NAME_FIELD] === 'string' && r[HONOR_NAME_FIELD] ? r[HONOR_NAME_FIELD] : r.title))
  74. .sort();
  75. const classifyWith = (categories) => ({
  76. company_name: TEST_NAME,
  77. credit_code: TEST_CREDIT_CODE,
  78. legal_representative: '测试法人',
  79. categories,
  80. status: 'completed',
  81. errors: {},
  82. });
  83. console.log(`测试信用代码:${TEST_CREDIT_CODE}\n`);
  84. try {
  85. /* ---------------------------------------------------------------- *
  86. * ① 首写
  87. * ---------------------------------------------------------------- */
  88. console.log('【1】首写:1888 一行 + 1886 一条荣誉一行');
  89. await applyCompanyClassification(
  90. classifyWith({
  91. 基本信息: ['小微企业'],
  92. 资质荣誉: ['高新技术企业'],
  93. 产业信息: ['第一产业', '现代农业'],
  94. 经营活动: ['对外投资'],
  95. 行业信息: ['涉农(广义)'],
  96. 许可认证: ['食品生产许可'],
  97. }),
  98. IDENTITY
  99. );
  100. await applyCompanyHonors(
  101. honorsNode([
  102. rec('高新技术企业', '国家级', '某认定公告', {
  103. PublishOffice: '科技部',
  104. PublishDate: '2025-01-02',
  105. CertificateCode: 'CERT-1',
  106. }),
  107. rec('独角兽企业', '省级', '2026 浙江独角兽', { PublishDate: '2026-04-23' }),
  108. ]),
  109. IDENTITY
  110. );
  111. const rows1 = await findEnterprise();
  112. check('1888 恰好一行', rows1.length === 1, rows1.length);
  113. const row1 = rows1[0] || {};
  114. check('1888 身份字段正确', row1.c_name === TEST_NAME && row1.c_oper_name === '测试法人', row1);
  115. check('1888 标签字段正确(非空角度)', row1[ANGLE_TO_FIELD['基本信息']] === '["小微企业"]' && row1[ANGLE_TO_FIELD['产业信息']] === '["第一产业","现代农业"]', row1);
  116. const honors1 = await findHonors();
  117. check('1886 行数 = 荣誉记录数(2)', honors1.length === 2, honors1.length);
  118. const h1 = honors1.find((r) => r[HONOR_NAME_FIELD] === '高新技术企业') || {};
  119. check('1886 title 与 c_honor 都写荣誉名', h1.title === '高新技术企业' && h1[HONOR_NAME_FIELD] === '高新技术企业', { title: h1.title, tag: h1[HONOR_NAME_FIELD] });
  120. check('1886 级别/来源/机构/日期/证书号落位', h1.c_level === '国家级' && h1.c_source === '某认定公告' && h1.c_publish_office === '科技部' && h1.c_publish_date === '2025-01-02' && h1.c_certificate_code === 'CERT-1', h1);
  121. check('1886 归属正确', honors1.every((r) => r.c_credit_code === TEST_CREDIT_CODE && r.c_name === TEST_NAME));
  122. /* ---------------------------------------------------------------- *
  123. * ② 幂等
  124. * ---------------------------------------------------------------- */
  125. console.log('\n【2】同数据重跑 → 零新增');
  126. await applyCompanyClassification(
  127. classifyWith({ 基本信息: ['小微企业'], 资质荣誉: ['高新技术企业'], 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'] }),
  128. IDENTITY
  129. );
  130. await applyCompanyHonors(
  131. honorsNode([
  132. rec('高新技术企业', '国家级', '某认定公告', { PublishOffice: '科技部', PublishDate: '2025-01-02', CertificateCode: 'CERT-1' }),
  133. rec('独角兽企业', '省级', '2026 浙江独角兽', { PublishDate: '2026-04-23' }),
  134. ]),
  135. IDENTITY
  136. );
  137. const rows2 = await findEnterprise();
  138. check('1888 仍是一行、uuid 未变', rows2.length === 1 && rows2[0]?.id === row1.id, rows2.length);
  139. check('1886 仍 2 行', (await findHonors()).length === 2);
  140. /* ---------------------------------------------------------------- *
  141. * ③ 改:1888 标签 + 1886 增删改
  142. * ---------------------------------------------------------------- */
  143. console.log('\n【3】改:1888 更新、1886 增删改对齐');
  144. await applyCompanyClassification(
  145. classifyWith({ 基本信息: ['小微企业', '专精特新'], 资质荣誉: ['高新技术企业'], 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'] }),
  146. IDENTITY
  147. );
  148. await applyCompanyHonors(
  149. honorsNode([
  150. // 同键但发布机构变了 → 更新
  151. rec('高新技术企业', '国家级', '某认定公告', { PublishOffice: '工信部', PublishDate: '2025-01-02', CertificateCode: 'CERT-1' }),
  152. // 新增一条
  153. rec('科技小巨人', '市级', '2026 市科委名单'),
  154. // 「独角兽企业」本条不出现 → 应被删除
  155. ]),
  156. IDENTITY
  157. );
  158. const rows3 = await findEnterprise();
  159. check('1888 仍是一行', rows3.length === 1, rows3.length);
  160. check('1888 基本信息已更新', rows3[0]?.[ANGLE_TO_FIELD['基本信息']] === '["小微企业","专精特新"]', rows3[0]?.[ANGLE_TO_FIELD['基本信息']]);
  161. const honors3 = await findHonors();
  162. check('1886 行数 = 新列表(2)', honors3.length === 2, honors3.length);
  163. check('1886 名字对齐(新增进、多余的删掉)', JSON.stringify(honorNames(honors3)) === JSON.stringify(['科技小巨人', '高新技术企业']), honorNames(honors3));
  164. const h3 = honors3.find((r) => r[HONOR_NAME_FIELD] === '高新技术企业') || {};
  165. check('1886 同键行的字段差异已更新', h3.c_publish_office === '工信部', h3.c_publish_office);
  166. /* ---------------------------------------------------------------- *
  167. * ④ complete=false:只增改、不删除
  168. * ---------------------------------------------------------------- */
  169. console.log('\n【4】honors.complete=false → 只增改不删除');
  170. await applyCompanyHonors(honorsNode([rec('新荣誉', '区级', '某区名单')], false), IDENTITY);
  171. const honors4 = await findHonors();
  172. check('新增的进来了', honorNames(honors4).includes('新荣誉'), honorNames(honors4));
  173. check('**原有的没被删**(3 行:高新技术、科技小巨人、新荣誉)', honors4.length === 3, honorNames(honors4));
  174. /* ---------------------------------------------------------------- *
  175. * ⑤ 迁移行也会被对齐(不再是「只碰自己写的行」)
  176. * ---------------------------------------------------------------- */
  177. console.log('\n【5】全量对齐:非本同步写入的同企业荣誉行同样参与');
  178. await addDmsContent(DMS_COLUMN_HONOR, DMS_MODEL_HONOR, {
  179. c_id: 0,
  180. c_credit_code: TEST_CREDIT_CODE,
  181. c_name: TEST_NAME,
  182. c_source: 'migrate_test', // 模拟迁移数据
  183. title: '迁移来的荣誉',
  184. c_level: '国家级',
  185. c_created_at: '2026-09-18 12:00:00',
  186. });
  187. check('预置 1 条迁移行', (await findHonors()).length === 4, (await findHonors()).length);
  188. await applyCompanyHonors(honorsNode([rec('迁移来的荣誉', '国家级', 'migrate_test')]), IDENTITY);
  189. const honors5 = await findHonors();
  190. check('后端列表只有它 → 其余全删,只剩 1 行', honors5.length === 1, honorNames(honors5));
  191. check('留下的正是迁移行那一条(按「荣誉名+级别+来源」匹配到同一行)', honors5[0]?.title === '迁移来的荣誉', honors5[0]?.title);
  192. /* ---------------------------------------------------------------- *
  193. * ⑥ 缺信用代码 → 跳过
  194. * ---------------------------------------------------------------- */
  195. console.log('\n【6】缺信用代码 → 整条跳过');
  196. await applyCompanyHonors(honorsNode([rec('不该出现的荣誉', '国家级', 'X')]), { name: TEST_NAME, creditCode: null, legalRep: null });
  197. check('1886 行数未变', (await findHonors()).length === 1);
  198. await applyCompanyHonors(null, IDENTITY);
  199. check('honors 缺失/null → 跳过(不清库)', (await findHonors()).length === 1);
  200. } finally {
  201. /* ---------------------------------------------------------------- *
  202. * 清理
  203. * ---------------------------------------------------------------- */
  204. console.log('\n【清理】');
  205. const cleanup = async (columnId, label) => {
  206. const rows = columnId === DMS_COLUMN_ENTERPRISE ? await findEnterprise() : await findHonors();
  207. for (const row of rows) {
  208. if (typeof row.id === 'string') await deleteDmsContent(columnId, row.id);
  209. }
  210. console.log(` ${label} 清理 ${rows.length} 行`);
  211. };
  212. await cleanup(DMS_COLUMN_ENTERPRISE, '1888');
  213. await cleanup(DMS_COLUMN_HONOR, '1886');
  214. check('残留 0 行(1888)', (await findEnterprise()).length === 0);
  215. check('残留 0 行(1886)', (await findHonors()).length === 0);
  216. }
  217. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  218. process.exit(fail ? 1 : 0);