verify-company-classify-dms.mjs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /**
  2. * 打**真实 DMS**验证企业信息同步的落库行为(栏目 1888 企业基础信息 / 1886 企业荣誉信息)。
  3. *
  4. * 为什么注入假分类响应:分类接口 `POST /api/company/classify` 目前对**任何带内容的
  5. * 请求体都返回 422**(2026-09-18 实测,连文档里的合成示例也被拒,属后端问题),
  6. * 所以这一段用 `applyCompanyClassification(分类结果)` 直接验证「拿到结果之后」的落库链路。
  7. *
  8. * 验证什么:
  9. * ① 首写:1888 出现一行且六个标签字段 = 对应角度的 JSON 数组;1886 按标签各一行
  10. * ② 重跑同样的数据:**零新增**(幂等,不产生重复行)
  11. * ③ 改标签:1888 仍只有一行、字段已更新;1886 集合按增删收敛(旧行被销毁查不到)
  12. * ④ 资质荣誉 = 空集:删光「我方」行,**迁移数据行(c_source 非 company_info_sync)不动**
  13. * ⑤ 清理:按测试信用代码删掉本次产生的全部行,复查残留 0
  14. *
  15. * 怎么跑(先按 _entry-company-classify.ts 头部注释重新打包):
  16. * 1) npm run dev # 代理负责注入 DMS token
  17. * 2) node harness/tools/verify-company-classify-dms.mjs https://localhost:8083/dms-api
  18. *
  19. * ⚠️ 脚本**不持有 token**(和浏览器一样只打代理地址);测试数据用可识别的信用代码前缀,
  20. * 结束时自行清理。
  21. */
  22. process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  23. const base = process.argv[2] || 'https://localhost:8083/dms-api';
  24. globalThis.VITE_DMS_API = base;
  25. const {
  26. applyCompanyClassification,
  27. HONOR_SYNC_SOURCE,
  28. HONOR_TAG_FIELD,
  29. ANGLE_TO_FIELD,
  30. DMS_COLUMN_ENTERPRISE,
  31. DMS_MODEL_ENTERPRISE,
  32. DMS_COLUMN_HONOR,
  33. DMS_MODEL_HONOR,
  34. addDmsContent,
  35. deleteDmsContent,
  36. searchDmsContents,
  37. } = await import('./_company-classify.mjs');
  38. let pass = 0;
  39. let fail = 0;
  40. const check = (name, ok, extra = '') => {
  41. if (ok) {
  42. pass++;
  43. console.log(` ok ${name}`);
  44. } else {
  45. fail++;
  46. console.log(` FAIL ${name}${extra === '' ? '' : ` → ${JSON.stringify(extra)}`}`);
  47. }
  48. };
  49. const TEST_CREDIT_CODE = `TEST_CC_${Date.now()}`;
  50. const TEST_NAME = '分类同步测试公司';
  51. const findEnterprise = async () => {
  52. const rows = await searchDmsContents(DMS_COLUMN_ENTERPRISE, {
  53. search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
  54. page: 0,
  55. pageSize: 10,
  56. });
  57. return rows;
  58. };
  59. const findHonors = async () => {
  60. const rows = await searchDmsContents(DMS_COLUMN_HONOR, {
  61. search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
  62. page: 0,
  63. pageSize: 200,
  64. });
  65. return rows;
  66. };
  67. const classifyWith = (categories, extra = {}) => ({
  68. company_name: TEST_NAME,
  69. credit_code: TEST_CREDIT_CODE,
  70. legal_representative: '测试法人',
  71. categories,
  72. status: 'completed',
  73. errors: {},
  74. ...extra,
  75. });
  76. console.log(`测试信用代码:${TEST_CREDIT_CODE}\n`);
  77. try {
  78. /* ---------------------------------------------------------------- *
  79. * ① 首写
  80. * ---------------------------------------------------------------- */
  81. console.log('【1】首写:1888 一行 + 1886 按标签各一行');
  82. const first = classifyWith({
  83. 基本信息: ['小微企业'],
  84. 资质荣誉: ['高新技术企业', '专精特新'],
  85. 产业信息: ['第一产业', '现代农业'],
  86. 经营活动: ['对外投资'],
  87. 行业信息: ['涉农(广义)'],
  88. 许可认证: ['食品生产许可'],
  89. });
  90. await applyCompanyClassification(first);
  91. const rows1 = await findEnterprise();
  92. check('1888 恰好一行', rows1.length === 1, rows1.length);
  93. const row1 = rows1[0] || {};
  94. check('身份字段正确', row1.c_name === TEST_NAME && row1.c_credit_code === TEST_CREDIT_CODE && row1.c_oper_name === '测试法人', {
  95. c_name: row1.c_name,
  96. c_credit_code: row1.c_credit_code,
  97. c_oper_name: row1.c_oper_name,
  98. });
  99. check('基本信息标签', row1[ANGLE_TO_FIELD['基本信息']] === '["小微企业"]', row1[ANGLE_TO_FIELD['基本信息']]);
  100. check('产业信息标签', row1[ANGLE_TO_FIELD['产业信息']] === '["第一产业","现代农业"]', row1[ANGLE_TO_FIELD['产业信息']]);
  101. check('许可认证标签', row1[ANGLE_TO_FIELD['许可认证']] === '["食品生产许可"]', row1[ANGLE_TO_FIELD['许可认证']]);
  102. // ⚠️ c_tag_honor 在 DMS 里的 frontType 是 content(富文本),其余是 text/varchar ——
  103. // 富文本字段有可能被清洗,所以单独断言它逐字往返
  104. check(
  105. '资质荣誉标签(富文本字段)逐字往返',
  106. row1[ANGLE_TO_FIELD['资质荣誉']] === '["高新技术企业","专精特新"]',
  107. row1[ANGLE_TO_FIELD['资质荣誉']]
  108. );
  109. const honors1 = await findHonors();
  110. const owned1 = honors1.filter((r) => r.c_source === HONOR_SYNC_SOURCE);
  111. const tags1 = owned1.map((r) => r[HONOR_TAG_FIELD]).sort();
  112. check('1886 我方行数 = 资质荣誉标签数', owned1.length === 2, owned1.length);
  113. check('1886 标签内容正确', JSON.stringify(tags1) === JSON.stringify(['专精特新', '高新技术企业']), tags1);
  114. check('1886 行带企业与归属', owned1.every((r) => r.c_credit_code === TEST_CREDIT_CODE && r.c_name === TEST_NAME));
  115. /* ---------------------------------------------------------------- *
  116. * ② 幂等:同数据重跑
  117. * ---------------------------------------------------------------- */
  118. console.log('\n【2】同数据重跑 → 零新增');
  119. await applyCompanyClassification(first);
  120. const rows2 = await findEnterprise();
  121. const honors2 = await findHonors();
  122. check('1888 仍是一行(没有重复)', rows2.length === 1, rows2.length);
  123. check('1888 记录 uuid 未变(走的是更新/零写入)', rows2[0]?.id === row1.id, { before: row1.id, after: rows2[0]?.id });
  124. check('1886 仍是我方 2 行', honors2.filter((r) => r.c_source === HONOR_SYNC_SOURCE).length === 2, honors2.length);
  125. /* ---------------------------------------------------------------- *
  126. * ③ 改标签
  127. * ---------------------------------------------------------------- */
  128. console.log('\n【3】改标签 → 1888 更新、1886 增删收敛');
  129. const second = classifyWith({
  130. 基本信息: ['小微企业', '专精特新'],
  131. 资质荣誉: ['高新技术企业', '科技小巨人'], // 去掉「专精特新」,新增「科技小巨人」
  132. 产业信息: ['第一产业', '现代农业'],
  133. 经营活动: ['对外投资'],
  134. 行业信息: ['涉农(广义)'],
  135. 许可认证: ['食品生产许可'],
  136. });
  137. await applyCompanyClassification(second);
  138. const rows3 = await findEnterprise();
  139. check('1888 仍是一行', rows3.length === 1, rows3.length);
  140. check('1888 基本信息已更新', rows3[0]?.[ANGLE_TO_FIELD['基本信息']] === '["小微企业","专精特新"]', rows3[0]?.[ANGLE_TO_FIELD['基本信息']]);
  141. const honors3 = await findHonors();
  142. const tags3 = honors3.filter((r) => r.c_source === HONOR_SYNC_SOURCE).map((r) => r[HONOR_TAG_FIELD]).sort();
  143. check('1886 收敛到新集合(2 行)', tags3.length === 2, tags3);
  144. check('被移除的标签已不在', !tags3.includes('专精特新'), tags3);
  145. check('新增的标签已在', tags3.includes('科技小巨人'), tags3);
  146. /* ---------------------------------------------------------------- *
  147. * ④ 空集 + 迁移行保护
  148. * ---------------------------------------------------------------- */
  149. console.log('\n【4】资质荣誉空集 → 删光我方行;迁移数据行不动');
  150. const foreignId = await addDmsContent(DMS_COLUMN_HONOR, DMS_MODEL_HONOR, {
  151. c_id: 0,
  152. c_credit_code: TEST_CREDIT_CODE,
  153. c_name: TEST_NAME,
  154. c_source: 'migrate_test', // 模拟迁移数据:不是我们写的
  155. [HONOR_TAG_FIELD]: '迁移来的荣誉',
  156. c_created_at: '2026-09-18 12:00:00',
  157. });
  158. check('预置了 1 条迁移行(用于验证不被误删)', !!foreignId, foreignId);
  159. await applyCompanyClassification(
  160. classifyWith({
  161. 基本信息: ['小微企业', '专精特新'],
  162. 资质荣誉: [], // completed + 空数组 = 确实没有荣誉
  163. 产业信息: ['第一产业', '现代农业'],
  164. 经营活动: ['对外投资'],
  165. 行业信息: ['涉农(广义)'],
  166. 许可认证: ['食品生产许可'],
  167. })
  168. );
  169. const honors4 = await findHonors();
  170. check('我方荣誉行已删光', honors4.filter((r) => r.c_source === HONOR_SYNC_SOURCE).length === 0, honors4.length);
  171. check('迁移行仍在(未被误删)', honors4.some((r) => r.id === foreignId && r[HONOR_TAG_FIELD] === '迁移来的荣誉'), honors4.map((r) => r.c_source));
  172. /* ---------------------------------------------------------------- *
  173. * ⑤ 缺失信用代码 → 跳过
  174. * ---------------------------------------------------------------- */
  175. console.log('\n【5】分类结果缺信用代码 → 整条跳过(不写任何行)');
  176. const noCredit = { company_name: '无信用代码的公司', credit_code: null, categories: { 基本信息: ['x'] }, status: 'completed' };
  177. await applyCompanyClassification(noCredit);
  178. check('1888 行数未变(仍是 1,且没有新建无名行)', (await findEnterprise()).length === 1);
  179. } finally {
  180. /* ---------------------------------------------------------------- *
  181. * 清理
  182. * ---------------------------------------------------------------- */
  183. console.log('\n【清理】');
  184. const cleanup = async (columnId, label) => {
  185. const rows = columnId === DMS_COLUMN_ENTERPRISE ? await findEnterprise() : await findHonors();
  186. for (const row of rows) {
  187. if (typeof row.id === 'string') await deleteDmsContent(columnId, row.id);
  188. }
  189. console.log(` ${label} 清理 ${rows.length} 行`);
  190. };
  191. await cleanup(DMS_COLUMN_ENTERPRISE, '1888');
  192. await cleanup(DMS_COLUMN_HONOR, '1886');
  193. const leftE = await findEnterprise();
  194. const leftH = await findHonors();
  195. check('残留 0 行(1888)', leftE.length === 0, leftE.length);
  196. check('残留 0 行(1886)', leftH.length === 0, leftH.length);
  197. }
  198. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  199. process.exit(fail ? 1 : 0);