/** * 打**真实 DMS**验证企业信息同步的落库行为(栏目 1888 企业基础信息 / 1886 企业荣誉信息)。 * * 为什么注入假分类响应:分类接口 `POST /api/company/classify` 目前对**任何带内容的 * 请求体都返回 422**(2026-09-18 实测,连文档里的合成示例也被拒,属后端问题), * 所以这一段用 `applyCompanyClassification(分类结果)` 直接验证「拿到结果之后」的落库链路。 * * 验证什么: * ① 首写:1888 出现一行且六个标签字段 = 对应角度的 JSON 数组;1886 按标签各一行 * ② 重跑同样的数据:**零新增**(幂等,不产生重复行) * ③ 改标签:1888 仍只有一行、字段已更新;1886 集合按增删收敛(旧行被销毁查不到) * ④ 资质荣誉 = 空集:删光「我方」行,**迁移数据行(c_source 非 company_info_sync)不动** * ⑤ 清理:按测试信用代码删掉本次产生的全部行,复查残留 0 * * 怎么跑(先按 _entry-company-classify.ts 头部注释重新打包): * 1) npm run dev # 代理负责注入 DMS token * 2) node harness/tools/verify-company-classify-dms.mjs https://localhost:8083/dms-api * * ⚠️ 脚本**不持有 token**(和浏览器一样只打代理地址);测试数据用可识别的信用代码前缀, * 结束时自行清理。 */ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const base = process.argv[2] || 'https://localhost:8083/dms-api'; globalThis.VITE_DMS_API = base; const { applyCompanyClassification, HONOR_SYNC_SOURCE, HONOR_TAG_FIELD, ANGLE_TO_FIELD, DMS_COLUMN_ENTERPRISE, DMS_MODEL_ENTERPRISE, DMS_COLUMN_HONOR, DMS_MODEL_HONOR, addDmsContent, deleteDmsContent, searchDmsContents, } = await import('./_company-classify.mjs'); let pass = 0; let fail = 0; const check = (name, ok, extra = '') => { if (ok) { pass++; console.log(` ok ${name}`); } else { fail++; console.log(` FAIL ${name}${extra === '' ? '' : ` → ${JSON.stringify(extra)}`}`); } }; const TEST_CREDIT_CODE = `TEST_CC_${Date.now()}`; const TEST_NAME = '分类同步测试公司'; const findEnterprise = async () => { const rows = await searchDmsContents(DMS_COLUMN_ENTERPRISE, { search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }], page: 0, pageSize: 10, }); return rows; }; const findHonors = async () => { const rows = await searchDmsContents(DMS_COLUMN_HONOR, { search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }], page: 0, pageSize: 200, }); return rows; }; const classifyWith = (categories, extra = {}) => ({ company_name: TEST_NAME, credit_code: TEST_CREDIT_CODE, legal_representative: '测试法人', categories, status: 'completed', errors: {}, ...extra, }); console.log(`测试信用代码:${TEST_CREDIT_CODE}\n`); try { /* ---------------------------------------------------------------- * * ① 首写 * ---------------------------------------------------------------- */ console.log('【1】首写:1888 一行 + 1886 按标签各一行'); const first = classifyWith({ 基本信息: ['小微企业'], 资质荣誉: ['高新技术企业', '专精特新'], 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'], }); await applyCompanyClassification(first); const rows1 = await findEnterprise(); check('1888 恰好一行', rows1.length === 1, rows1.length); const row1 = rows1[0] || {}; check('身份字段正确', row1.c_name === TEST_NAME && row1.c_credit_code === TEST_CREDIT_CODE && row1.c_oper_name === '测试法人', { c_name: row1.c_name, c_credit_code: row1.c_credit_code, c_oper_name: row1.c_oper_name, }); check('基本信息标签', row1[ANGLE_TO_FIELD['基本信息']] === '["小微企业"]', row1[ANGLE_TO_FIELD['基本信息']]); check('产业信息标签', row1[ANGLE_TO_FIELD['产业信息']] === '["第一产业","现代农业"]', row1[ANGLE_TO_FIELD['产业信息']]); check('许可认证标签', row1[ANGLE_TO_FIELD['许可认证']] === '["食品生产许可"]', row1[ANGLE_TO_FIELD['许可认证']]); // ⚠️ c_tag_honor 在 DMS 里的 frontType 是 content(富文本),其余是 text/varchar —— // 富文本字段有可能被清洗,所以单独断言它逐字往返 check( '资质荣誉标签(富文本字段)逐字往返', row1[ANGLE_TO_FIELD['资质荣誉']] === '["高新技术企业","专精特新"]', row1[ANGLE_TO_FIELD['资质荣誉']] ); const honors1 = await findHonors(); const owned1 = honors1.filter((r) => r.c_source === HONOR_SYNC_SOURCE); const tags1 = owned1.map((r) => r[HONOR_TAG_FIELD]).sort(); check('1886 我方行数 = 资质荣誉标签数', owned1.length === 2, owned1.length); check('1886 标签内容正确', JSON.stringify(tags1) === JSON.stringify(['专精特新', '高新技术企业']), tags1); check('1886 行带企业与归属', owned1.every((r) => r.c_credit_code === TEST_CREDIT_CODE && r.c_name === TEST_NAME)); /* ---------------------------------------------------------------- * * ② 幂等:同数据重跑 * ---------------------------------------------------------------- */ console.log('\n【2】同数据重跑 → 零新增'); await applyCompanyClassification(first); const rows2 = await findEnterprise(); const honors2 = await findHonors(); check('1888 仍是一行(没有重复)', rows2.length === 1, rows2.length); check('1888 记录 uuid 未变(走的是更新/零写入)', rows2[0]?.id === row1.id, { before: row1.id, after: rows2[0]?.id }); check('1886 仍是我方 2 行', honors2.filter((r) => r.c_source === HONOR_SYNC_SOURCE).length === 2, honors2.length); /* ---------------------------------------------------------------- * * ③ 改标签 * ---------------------------------------------------------------- */ console.log('\n【3】改标签 → 1888 更新、1886 增删收敛'); const second = classifyWith({ 基本信息: ['小微企业', '专精特新'], 资质荣誉: ['高新技术企业', '科技小巨人'], // 去掉「专精特新」,新增「科技小巨人」 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'], }); await applyCompanyClassification(second); const rows3 = await findEnterprise(); check('1888 仍是一行', rows3.length === 1, rows3.length); check('1888 基本信息已更新', rows3[0]?.[ANGLE_TO_FIELD['基本信息']] === '["小微企业","专精特新"]', rows3[0]?.[ANGLE_TO_FIELD['基本信息']]); const honors3 = await findHonors(); const tags3 = honors3.filter((r) => r.c_source === HONOR_SYNC_SOURCE).map((r) => r[HONOR_TAG_FIELD]).sort(); check('1886 收敛到新集合(2 行)', tags3.length === 2, tags3); check('被移除的标签已不在', !tags3.includes('专精特新'), tags3); check('新增的标签已在', tags3.includes('科技小巨人'), tags3); /* ---------------------------------------------------------------- * * ④ 空集 + 迁移行保护 * ---------------------------------------------------------------- */ console.log('\n【4】资质荣誉空集 → 删光我方行;迁移数据行不动'); const foreignId = await addDmsContent(DMS_COLUMN_HONOR, DMS_MODEL_HONOR, { c_id: 0, c_credit_code: TEST_CREDIT_CODE, c_name: TEST_NAME, c_source: 'migrate_test', // 模拟迁移数据:不是我们写的 [HONOR_TAG_FIELD]: '迁移来的荣誉', c_created_at: '2026-09-18 12:00:00', }); check('预置了 1 条迁移行(用于验证不被误删)', !!foreignId, foreignId); await applyCompanyClassification( classifyWith({ 基本信息: ['小微企业', '专精特新'], 资质荣誉: [], // completed + 空数组 = 确实没有荣誉 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'], }) ); const honors4 = await findHonors(); check('我方荣誉行已删光', honors4.filter((r) => r.c_source === HONOR_SYNC_SOURCE).length === 0, honors4.length); check('迁移行仍在(未被误删)', honors4.some((r) => r.id === foreignId && r[HONOR_TAG_FIELD] === '迁移来的荣誉'), honors4.map((r) => r.c_source)); /* ---------------------------------------------------------------- * * ⑤ 缺失信用代码 → 跳过 * ---------------------------------------------------------------- */ console.log('\n【5】分类结果缺信用代码 → 整条跳过(不写任何行)'); const noCredit = { company_name: '无信用代码的公司', credit_code: null, categories: { 基本信息: ['x'] }, status: 'completed' }; await applyCompanyClassification(noCredit); check('1888 行数未变(仍是 1,且没有新建无名行)', (await findEnterprise()).length === 1); } finally { /* ---------------------------------------------------------------- * * 清理 * ---------------------------------------------------------------- */ console.log('\n【清理】'); const cleanup = async (columnId, label) => { const rows = columnId === DMS_COLUMN_ENTERPRISE ? await findEnterprise() : await findHonors(); for (const row of rows) { if (typeof row.id === 'string') await deleteDmsContent(columnId, row.id); } console.log(` ${label} 清理 ${rows.length} 行`); }; await cleanup(DMS_COLUMN_ENTERPRISE, '1888'); await cleanup(DMS_COLUMN_HONOR, '1886'); const leftE = await findEnterprise(); const leftH = await findHonors(); check('残留 0 行(1888)', leftE.length === 0, leftE.length); check('残留 0 行(1886)', leftH.length === 0, leftH.length); } console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`); process.exit(fail ? 1 : 0);