| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- /**
- * 打**真实 DMS**验证企业信息同步的落库行为(栏目 1888 企业基础信息 / 1886 企业荣誉信息)。
- *
- * 用**注入的假数据**(假的分类结果 + 假的荣誉列表)跑完整落库链路,因此:
- * - 不消耗分类接口的 LLM 调用,可以反复跑
- * - 覆盖边界(改标签、删荣誉、complete=false 不许删、缺信用代码跳过…)
- *
- * 验证什么:
- * ① 1888 首写:一行,六个标签字段 = 对应角度的 JSON 数组
- * ② 1886 首写:**一条荣誉一行**,title 与 c_honor 都是荣誉名,级别/来源/机构/日期/证书号各就各位
- * ③ 幂等:同样数据再跑一次 → 零新增、行 uuid 不变
- * ④ 改:1888 标签变化 → 仍是同一行且已更新;1886 少一条/多一条/改字段 →
- * 删多余、补新增、更新差异,行数与后端列表一致
- * ⑤ `honors.complete = false` → **只增改不删除**(记录集不完整时不许删)
- * ⑥ 缺信用代码 → 整条跳过,不写任何行
- * ⑦ 清理:按测试信用代码删掉本次产生的全部行,复查残留 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,
- applyCompanyHonors,
- applyCompanyProfile,
- HONOR_NAME_FIELD,
- ANGLE_TO_FIELD,
- DMS_COLUMN_ENTERPRISE,
- DMS_COLUMN_HONOR,
- DMS_MODEL_HONOR,
- DMS_MODEL_ENTERPRISE,
- addDmsContent,
- deleteDmsContent,
- searchDmsContents,
- updateDmsContent,
- } = 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 IDENTITY = { name: TEST_NAME, creditCode: TEST_CREDIT_CODE, legalRep: '测试法人' };
- /** 造一条企查查形态的荣誉记录(`records[].data`) */
- const rec = (Name, Level, Source, extra = {}) => ({
- data: { Name, Level, Source, ...extra },
- });
- const honorsNode = (list, complete = true) => ({ complete, records: list, sources: [], error: null });
- const findEnterprise = () =>
- searchDmsContents(DMS_COLUMN_ENTERPRISE, {
- search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
- page: 0,
- pageSize: 10,
- });
- const findHonors = () =>
- searchDmsContents(DMS_COLUMN_HONOR, {
- search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
- page: 0,
- pageSize: 200,
- });
- const honorNames = (rows) =>
- rows
- .map((r) => (typeof r[HONOR_NAME_FIELD] === 'string' && r[HONOR_NAME_FIELD] ? r[HONOR_NAME_FIELD] : r.title))
- .sort();
- const classifyWith = (categories) => ({
- company_name: TEST_NAME,
- credit_code: TEST_CREDIT_CODE,
- legal_representative: '测试法人',
- categories,
- status: 'completed',
- errors: {},
- });
- console.log(`测试信用代码:${TEST_CREDIT_CODE}\n`);
- try {
- /* ---------------------------------------------------------------- *
- * ① 首写
- * ---------------------------------------------------------------- */
- console.log('【1】首写:1888 一行 + 1886 一条荣誉一行');
- await applyCompanyClassification(
- classifyWith({
- 基本信息: ['小微企业'],
- 资质荣誉: ['高新技术企业'],
- 产业信息: ['第一产业', '现代农业'],
- 经营活动: ['对外投资'],
- 行业信息: ['涉农(广义)'],
- 许可认证: ['食品生产许可'],
- }),
- IDENTITY
- );
- await applyCompanyHonors(
- honorsNode([
- rec('高新技术企业', '国家级', '某认定公告', {
- PublishOffice: '科技部',
- PublishDate: '2025-01-02',
- CertificateCode: 'CERT-1',
- }),
- rec('独角兽企业', '省级', '2026 浙江独角兽', { PublishDate: '2026-04-23' }),
- ]),
- IDENTITY
- );
- const rows1 = await findEnterprise();
- check('1888 恰好一行', rows1.length === 1, rows1.length);
- const row1 = rows1[0] || {};
- check('1888 身份字段正确', row1.c_name === TEST_NAME && row1.c_oper_name === '测试法人', row1);
- check('1888 标签字段正确(非空角度)', row1[ANGLE_TO_FIELD['基本信息']] === '["小微企业"]' && row1[ANGLE_TO_FIELD['产业信息']] === '["第一产业","现代农业"]', row1);
- const honors1 = await findHonors();
- check('1886 行数 = 荣誉记录数(2)', honors1.length === 2, honors1.length);
- const h1 = honors1.find((r) => r[HONOR_NAME_FIELD] === '高新技术企业') || {};
- check('1886 title 与 c_honor 都写荣誉名', h1.title === '高新技术企业' && h1[HONOR_NAME_FIELD] === '高新技术企业', { title: h1.title, tag: h1[HONOR_NAME_FIELD] });
- 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);
- check('1886 归属正确', honors1.every((r) => r.c_credit_code === TEST_CREDIT_CODE && r.c_name === TEST_NAME));
- /* ---------------------------------------------------------------- *
- * ② 幂等
- * ---------------------------------------------------------------- */
- console.log('\n【2】同数据重跑 → 零新增');
- await applyCompanyClassification(
- classifyWith({ 基本信息: ['小微企业'], 资质荣誉: ['高新技术企业'], 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'] }),
- IDENTITY
- );
- await applyCompanyHonors(
- honorsNode([
- rec('高新技术企业', '国家级', '某认定公告', { PublishOffice: '科技部', PublishDate: '2025-01-02', CertificateCode: 'CERT-1' }),
- rec('独角兽企业', '省级', '2026 浙江独角兽', { PublishDate: '2026-04-23' }),
- ]),
- IDENTITY
- );
- const rows2 = await findEnterprise();
- check('1888 仍是一行、uuid 未变', rows2.length === 1 && rows2[0]?.id === row1.id, rows2.length);
- check('1886 仍 2 行', (await findHonors()).length === 2);
- /* ---------------------------------------------------------------- *
- * ③ 改:1888 标签 + 1886 增删改
- * ---------------------------------------------------------------- */
- console.log('\n【3】改:1888 更新、1886 增删改对齐');
- await applyCompanyClassification(
- classifyWith({ 基本信息: ['小微企业', '专精特新'], 资质荣誉: ['高新技术企业'], 产业信息: ['第一产业', '现代农业'], 经营活动: ['对外投资'], 行业信息: ['涉农(广义)'], 许可认证: ['食品生产许可'] }),
- IDENTITY
- );
- await applyCompanyHonors(
- honorsNode([
- // 同键但发布机构变了 → 更新
- rec('高新技术企业', '国家级', '某认定公告', { PublishOffice: '工信部', PublishDate: '2025-01-02', CertificateCode: 'CERT-1' }),
- // 新增一条
- rec('科技小巨人', '市级', '2026 市科委名单'),
- // 「独角兽企业」本条不出现 → 应被删除
- ]),
- IDENTITY
- );
- 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();
- check('1886 行数 = 新列表(2)', honors3.length === 2, honors3.length);
- check('1886 名字对齐(新增进、多余的删掉)', JSON.stringify(honorNames(honors3)) === JSON.stringify(['科技小巨人', '高新技术企业']), honorNames(honors3));
- const h3 = honors3.find((r) => r[HONOR_NAME_FIELD] === '高新技术企业') || {};
- check('1886 同键行的字段差异已更新', h3.c_publish_office === '工信部', h3.c_publish_office);
- /* ---------------------------------------------------------------- *
- * ④ complete=false:只增改、不删除
- * ---------------------------------------------------------------- */
- console.log('\n【4】honors.complete=false → 只增改不删除');
- await applyCompanyHonors(honorsNode([rec('新荣誉', '区级', '某区名单')], false), IDENTITY);
- const honors4 = await findHonors();
- check('新增的进来了', honorNames(honors4).includes('新荣誉'), honorNames(honors4));
- check('**原有的没被删**(3 行:高新技术、科技小巨人、新荣誉)', honors4.length === 3, honorNames(honors4));
- /* ---------------------------------------------------------------- *
- * ⑤ 迁移行也会被对齐(不再是「只碰自己写的行」)
- * ---------------------------------------------------------------- */
- console.log('\n【5】全量对齐:非本同步写入的同企业荣誉行同样参与');
- 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', // 模拟迁移数据
- title: '迁移来的荣誉',
- c_level: '国家级',
- c_created_at: '2026-09-18 12:00:00',
- });
- check('预置 1 条迁移行', (await findHonors()).length === 4, (await findHonors()).length);
- await applyCompanyHonors(honorsNode([rec('迁移来的荣誉', '国家级', 'migrate_test')]), IDENTITY);
- const honors5 = await findHonors();
- check('后端列表只有它 → 其余全删,只剩 1 行', honors5.length === 1, honorNames(honors5));
- check('留下的正是迁移行那一条(按「荣誉名+级别+来源」匹配到同一行)', honors5[0]?.title === '迁移来的荣誉', honors5[0]?.title);
- /* ---------------------------------------------------------------- *
- * ⑥ 缺信用代码 → 跳过
- * ---------------------------------------------------------------- */
- console.log('\n【6】缺信用代码 → 整条跳过');
- await applyCompanyHonors(honorsNode([rec('不该出现的荣誉', '国家级', 'X')]), { name: TEST_NAME, creditCode: null, legalRep: null });
- check('1886 行数未变', (await findHonors()).length === 1);
- await applyCompanyHonors(null, IDENTITY);
- check('honors 缺失/null → 跳过(不清库)', (await findHonors()).length === 1);
- /* ---------------------------------------------------------------- *
- * ⑦ 工商信息(profile.data → 1888)
- * ---------------------------------------------------------------- */
- console.log('\n【7】工商信息同步(不依赖分类接口)');
- const PROFILE = {
- Name: TEST_NAME,
- CreditCode: TEST_CREDIT_CODE,
- OperName: '测试法人',
- No: '310118000000001',
- BelongOrg: '青浦区市场监督管理局',
- Status: '存续(在营、开业、在册)',
- Province: 'SH',
- AreaCode: '310118',
- Address: '上海市青浦区测试路 1 号',
- RegistCapi: '1000万元',
- RegisteredCapital: '1000',
- IsOnStock: '0',
- Noop: '映射表里没有的字段',
- EndDate: '',
- RevokeInfo: null,
- DesignatedRepresentativeList: [],
- OriginalName: [{ Name: '测试曾用名', ChangeDate: '2024-07-22' }],
- Area: { Province: '上海市', City: '上海市', County: '青浦区' },
- };
- await applyCompanyProfile(PROFILE, IDENTITY);
- const p1 = (await findEnterprise())[0] || {};
- check('标量字段落位', p1.c_status === '存续(在营、开业、在册)' && p1.c_regist_capi === '1000万元' && p1.c_no === '310118000000001' && p1.c_area_code === '310118', p1);
- check('**系统字段 title = 企业名称**(不写的话 DMS 落成字符串 "null")', p1.title === TEST_NAME, p1.title);
- // 已有行 title 是字符串 "null"(旧版本写的 / 别的来源建的)→ 下次同步要补上
- await updateDmsContent(DMS_COLUMN_ENTERPRISE, DMS_MODEL_ENTERPRISE, p1.id, { title: 'null' });
- await applyCompanyProfile(PROFILE, IDENTITY);
- check('旧行的 title="null" 会被修正为企业名称', (await findEnterprise())[0]?.title === TEST_NAME, (await findEnterprise())[0]?.title);
- check('数组/对象落成 JSON 字符串', JSON.parse(p1.c_original_name || '[]')[0]?.Name === '测试曾用名' && JSON.parse(p1.c_area || '{}').County === '青浦区', { orig: p1.c_original_name, area: p1.c_area });
- check('空值不写(clearing 风险)', p1.c_end_date === undefined || p1.c_end_date === '', p1.c_end_date);
- check('映射表外的字段不写', p1.c_noop === undefined && p1.Noop === undefined);
- // ⚠️ 灌入行的 JSON 是 Python/jsonb 序列化的(带空格、键按字母序)——语义相同就不该重写
- const PY_STYLE_AREA = '{ "City": "上海市", "County": "青浦区", "Province": "上海市" }';
- const rowBefore = (await findEnterprise())[0];
- await updateDmsContent(DMS_COLUMN_ENTERPRISE, DMS_MODEL_ENTERPRISE, rowBefore.id, { c_area: PY_STYLE_AREA, c_status: '旧状态' });
- await applyCompanyProfile({ ...PROFILE, Area: { Province: '上海市', City: '上海市', County: '青浦区' }, Status: '存续(在营、开业、在册)' }, IDENTITY);
- const p2 = (await findEnterprise())[0] || {};
- check('**语义相同的 JSON 不被重写**(灌入形态原样保留)', p2.c_area === PY_STYLE_AREA, p2.c_area);
- check('真变化的文本列照常更新', p2.c_status === '存续(在营、开业、在册)', p2.c_status);
- await applyCompanyProfile(PROFILE, IDENTITY);
- check('再同步一次:JSON 列仍是灌入形态(不反复写)', (await findEnterprise())[0]?.c_area === PY_STYLE_AREA);
- } 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');
- check('残留 0 行(1888)', (await findEnterprise()).length === 0);
- check('残留 0 行(1886)', (await findHonors()).length === 0);
- }
- console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
- process.exit(fail ? 1 : 0);
|