|
|
@@ -1,16 +1,19 @@
|
|
|
/**
|
|
|
* 打**真实 DMS**验证企业信息同步的落库行为(栏目 1888 企业基础信息 / 1886 企业荣誉信息)。
|
|
|
*
|
|
|
- * 为什么注入假分类响应:分类接口 `POST /api/company/classify` 目前对**任何带内容的
|
|
|
- * 请求体都返回 422**(2026-09-18 实测,连文档里的合成示例也被拒,属后端问题),
|
|
|
- * 所以这一段用 `applyCompanyClassification(分类结果)` 直接验证「拿到结果之后」的落库链路。
|
|
|
+ * 用**注入的假数据**(假的分类结果 + 假的荣誉列表)跑完整落库链路,因此:
|
|
|
+ * - 不消耗分类接口的 LLM 调用,可以反复跑
|
|
|
+ * - 覆盖边界(改标签、删荣誉、complete=false 不许删、缺信用代码跳过…)
|
|
|
*
|
|
|
* 验证什么:
|
|
|
- * ① 首写:1888 出现一行且六个标签字段 = 对应角度的 JSON 数组;1886 按标签各一行
|
|
|
- * ② 重跑同样的数据:**零新增**(幂等,不产生重复行)
|
|
|
- * ③ 改标签:1888 仍只有一行、字段已更新;1886 集合按增删收敛(旧行被销毁查不到)
|
|
|
- * ④ 资质荣誉 = 空集:删光「我方」行,**迁移数据行(c_source 非 company_info_sync)不动**
|
|
|
- * ⑤ 清理:按测试信用代码删掉本次产生的全部行,复查残留 0
|
|
|
+ * ① 1888 首写:一行,六个标签字段 = 对应角度的 JSON 数组
|
|
|
+ * ② 1886 首写:**一条荣誉一行**,title 与 c_tag_name 都是荣誉名,级别/来源/机构/日期/证书号各就各位
|
|
|
+ * ③ 幂等:同样数据再跑一次 → 零新增、行 uuid 不变
|
|
|
+ * ④ 改:1888 标签变化 → 仍是同一行且已更新;1886 少一条/多一条/改字段 →
|
|
|
+ * 删多余、补新增、更新差异,行数与后端列表一致
|
|
|
+ * ⑤ `honors.complete = false` → **只增改不删除**(记录集不完整时不许删)
|
|
|
+ * ⑥ 缺信用代码 → 整条跳过,不写任何行
|
|
|
+ * ⑦ 清理:按测试信用代码删掉本次产生的全部行,复查残留 0
|
|
|
*
|
|
|
* 怎么跑(先按 _entry-company-classify.ts 头部注释重新打包):
|
|
|
* 1) npm run dev # 代理负责注入 DMS token
|
|
|
@@ -27,11 +30,10 @@ globalThis.VITE_DMS_API = base;
|
|
|
|
|
|
const {
|
|
|
applyCompanyClassification,
|
|
|
- HONOR_SYNC_SOURCE,
|
|
|
+ applyCompanyHonors,
|
|
|
HONOR_TAG_FIELD,
|
|
|
ANGLE_TO_FIELD,
|
|
|
DMS_COLUMN_ENTERPRISE,
|
|
|
- DMS_MODEL_ENTERPRISE,
|
|
|
DMS_COLUMN_HONOR,
|
|
|
DMS_MODEL_HONOR,
|
|
|
addDmsContent,
|
|
|
@@ -53,33 +55,38 @@ const check = (name, ok, extra = '') => {
|
|
|
|
|
|
const TEST_CREDIT_CODE = `TEST_CC_${Date.now()}`;
|
|
|
const TEST_NAME = '分类同步测试公司';
|
|
|
+const IDENTITY = { name: TEST_NAME, creditCode: TEST_CREDIT_CODE, legalRep: '测试法人' };
|
|
|
|
|
|
-const findEnterprise = async () => {
|
|
|
- const rows = await searchDmsContents(DMS_COLUMN_ENTERPRISE, {
|
|
|
+/** 造一条企查查形态的荣誉记录(`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,
|
|
|
});
|
|
|
- return rows;
|
|
|
-};
|
|
|
-
|
|
|
-const findHonors = async () => {
|
|
|
- const rows = await searchDmsContents(DMS_COLUMN_HONOR, {
|
|
|
+const findHonors = () =>
|
|
|
+ searchDmsContents(DMS_COLUMN_HONOR, {
|
|
|
search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT_CODE } }],
|
|
|
page: 0,
|
|
|
pageSize: 200,
|
|
|
});
|
|
|
- return rows;
|
|
|
-};
|
|
|
+const honorNames = (rows) =>
|
|
|
+ rows
|
|
|
+ .map((r) => (typeof r[HONOR_TAG_FIELD] === 'string' && r[HONOR_TAG_FIELD] ? r[HONOR_TAG_FIELD] : r.title))
|
|
|
+ .sort();
|
|
|
|
|
|
-const classifyWith = (categories, extra = {}) => ({
|
|
|
+const classifyWith = (categories) => ({
|
|
|
company_name: TEST_NAME,
|
|
|
credit_code: TEST_CREDIT_CODE,
|
|
|
legal_representative: '测试法人',
|
|
|
categories,
|
|
|
status: 'completed',
|
|
|
errors: {},
|
|
|
- ...extra,
|
|
|
});
|
|
|
|
|
|
console.log(`测试信用代码:${TEST_CREDIT_CODE}\n`);
|
|
|
@@ -88,114 +95,128 @@ try {
|
|
|
/* ---------------------------------------------------------------- *
|
|
|
* ① 首写
|
|
|
* ---------------------------------------------------------------- */
|
|
|
- console.log('【1】首写:1888 一行 + 1886 按标签各一行');
|
|
|
- const first = classifyWith({
|
|
|
- 基本信息: ['小微企业'],
|
|
|
- 资质荣誉: ['高新技术企业', '专精特新'],
|
|
|
- 产业信息: ['第一产业', '现代农业'],
|
|
|
- 经营活动: ['对外投资'],
|
|
|
- 行业信息: ['涉农(广义)'],
|
|
|
- 许可认证: ['食品生产许可'],
|
|
|
- });
|
|
|
- await applyCompanyClassification(first);
|
|
|
+ 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('身份字段正确', 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['资质荣誉']]
|
|
|
- );
|
|
|
+ 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();
|
|
|
- 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));
|
|
|
+ check('1886 行数 = 荣誉记录数(2)', honors1.length === 2, honors1.length);
|
|
|
+ const h1 = honors1.find((r) => r[HONOR_TAG_FIELD] === '高新技术企业') || {};
|
|
|
+ check('1886 title 与 c_tag_name 都写荣誉名', h1.title === '高新技术企业' && h1[HONOR_TAG_FIELD] === '高新技术企业', { title: h1.title, tag: h1[HONOR_TAG_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(first);
|
|
|
+ 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();
|
|
|
- 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);
|
|
|
+ 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 增删收敛');
|
|
|
- const second = classifyWith({
|
|
|
- 基本信息: ['小微企业', '专精特新'],
|
|
|
- 资质荣誉: ['高新技术企业', '科技小巨人'], // 去掉「专精特新」,新增「科技小巨人」
|
|
|
- 产业信息: ['第一产业', '现代农业'],
|
|
|
- 经营活动: ['对外投资'],
|
|
|
- 行业信息: ['涉农(广义)'],
|
|
|
- 许可认证: ['食品生产许可'],
|
|
|
- });
|
|
|
- await applyCompanyClassification(second);
|
|
|
+ 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();
|
|
|
- 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);
|
|
|
+ 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_TAG_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【4】资质荣誉空集 → 删光我方行;迁移数据行不动');
|
|
|
- const foreignId = await addDmsContent(DMS_COLUMN_HONOR, DMS_MODEL_HONOR, {
|
|
|
+ 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', // 模拟迁移数据:不是我们写的
|
|
|
- [HONOR_TAG_FIELD]: '迁移来的荣誉',
|
|
|
+ c_source: 'migrate_test', // 模拟迁移数据
|
|
|
+ title: '迁移来的荣誉',
|
|
|
+ c_level: '国家级',
|
|
|
c_created_at: '2026-09-18 12:00:00',
|
|
|
});
|
|
|
- check('预置了 1 条迁移行(用于验证不被误删)', !!foreignId, foreignId);
|
|
|
+ check('预置 1 条迁移行', (await findHonors()).length === 4, (await findHonors()).length);
|
|
|
|
|
|
- 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));
|
|
|
+ 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【5】分类结果缺信用代码 → 整条跳过(不写任何行)');
|
|
|
- const noCredit = { company_name: '无信用代码的公司', credit_code: null, categories: { 基本信息: ['x'] }, status: 'completed' };
|
|
|
- await applyCompanyClassification(noCredit);
|
|
|
- check('1888 行数未变(仍是 1,且没有新建无名行)', (await findEnterprise()).length === 1);
|
|
|
+ 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);
|
|
|
} finally {
|
|
|
/* ---------------------------------------------------------------- *
|
|
|
* 清理
|
|
|
@@ -211,10 +232,8 @@ try {
|
|
|
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);
|
|
|
+ check('残留 0 行(1888)', (await findEnterprise()).length === 0);
|
|
|
+ check('残留 0 行(1886)', (await findHonors()).length === 0);
|
|
|
}
|
|
|
|
|
|
console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
|