ソースを参照

fix(company): 1888 补写系统字段 title = 企业名称

用户反馈「给企业基础信息传值时,将企业名称传给后端字段 title」——确实漏写了。
后果与 1886 同因:title 只在创建那一刻初始化、之后改 c_* 不带动它,不传就被 DMS
落成字符串 "null",列表里看不出是哪家企业。

- classification-sync-utils:新增纯函数 withSystemTitle(row),取 c_name 作 title;
  值相同原样返回(不产生无谓写入)、c_name 为空则不动(不清存量)
- company-info-sync:在 upsertEnterpriseRow 一处调用 —— 1888 的两条入口共用,
  因此工商信息与分类标签两条路径都覆盖;走 diff 的普通文本比较,更新路径也会
  给历史遗留的 title="null" 补上企业名称

验证:纯逻辑 84/84(+5)、真实 DMS 32/32(+2,含「旧行 title=null 被修正」)、
端到端 25/25(+1)、回归 10/10 与 12/12;build 与 validate-harness 通过。
另核查历史数据:扫 1888 前 6000 行,title 缺失/为 "null" 的行 = 0,无需补数据。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 3 時間 前
コミット
d3a3b4c682

+ 6 - 0
harness/docs/reference/DMS_COLUMNS.md

@@ -216,6 +216,12 @@
 |------|------|------|
 | `c_created_at` | 创建时间 | **timestamp** |
 
+> 📌 **系统字段 `title` 写企业名称**(不在 fieldList 里,是 DMS 的列表显示列)。
+> 前端同步会带上它(`withSystemTitle`,取 `c_name`)——原因见 1886 那段:
+> `title` 只在**创建那一刻**初始化,不传的话 DMS 落的是**字符串 `"null"`**,
+> 列表里根本看不出是哪家企业(2026-09-20 用户反馈后补上)。
+> 库里灌入的行本来就有 title(值相同不会产生写入);历史遗留的 `"null"` 会在下次同步被修正。
+
 ### 2.5 助手问答记录 `columnId=1889`
 
 | 字段 | 别名 | 类型 | 前端控件 | 必填 |

ファイルの差分が大きいため隠しています
+ 5 - 5
harness/feature_list.json


+ 37 - 0
harness/progress.md

@@ -2783,3 +2783,40 @@
     数值(部门列 340px 上限、名称列占满剩余)都留了注释,后续要微调改一处即可
 - **下一步最佳动作**:无(本轮已确认);功能清单里仍无 in_progress,候选方向见 Session 031
 
+## Session 066
+
+- **日期**:2026-09-20
+- **本轮目标**:用户反馈「给企业基础信息传值时,将企业名称传给后端字段 title」
+- **问题性质**:**我们漏写了 1888 的系统字段 `title`** —— 库里灌入的行本来就有
+  (09-17 那批),但**前端新建的行**会因为没传而被 DMS 落成**字符串 `"null"`**,
+  在 DMS 列表里看不出是哪家企业。这与 1886 当初踩的是同一个坑
+  (`title` 只在创建那一刻初始化,之后改 `c_*` 字段不带动它)
+- **改法**:
+  - `classification-sync-utils.ts` 新增纯函数 **`withSystemTitle(row)`**:取 `c_name` 作为 `title`;
+    值相同则**原样返回**(不产生无谓写入)、`c_name` 为空则**不动** `title`(不清存量)
+  - `company-info-sync.ts` 的 **`upsertEnterpriseRow` 一处**调用它 —— 1888 的两条入口
+    (工商信息 / 分类标签)共用这一处,所以两条路径都覆盖
+  - 因为是走 `diffEnterpriseRow` 的普通文本比较,**更新路径也会给历史遗留的
+    `title="null"` 补上企业名称**
+- **运行过的验证**:
+  - `npm run build` 通过
+  - **纯逻辑 84/84**(新增 5 项:取 c_name / 已相同不动 / `"null"` 被修正 / c_name 空不动 / diff 能捕获)
+  - **真实 DMS 32/32**(新增 2 项:新增行 title = 企业名称;**把已有行的 title 改成 `"null"` 后
+    再同步 → 被修正**)
+  - **端到端 25/25**(新增 1 项:真实载荷同步后 title = 企业名称)
+  - 回归:适配层直通 10/10、载荷字段 12/12 仍绿
+- **已记录证据**:本文件 Session 066;三处新增断言;
+  `DMS_COLUMNS.md` §2.4 的 title 说明;`feature_list.json` 的对应条目
+- **提交记录**:见下条提交(已按约定提交推送)
+- **更新过的文件或工件**:`src/network/api/dms/{classification-sync-utils,company-info-sync}.ts`、
+  `harness/tools/{_entry-company-classify.ts, verify-company-classify-sync.mjs,
+  verify-company-classify-dms.mjs, verify-company-classify-e2e.mjs}`、
+  `harness/docs/reference/DMS_COLUMNS.md`、`harness/feature_list.json`、本文件
+- **已知风险或未解决问题**:
+  - ⚠️ 未走浏览器(改动在后台同步逻辑,触发链路未变);用户可在 DMS 里查看 1888 的
+    「标题」列是否显示企业名称
+  - ✅ **历史数据已核查,无需补**:扫了 1888 前 6000 行(31 页 × 200),
+    **title 缺失或为 `"null"` 的行 = 0** —— 灌入行本来就有标题,前端此前也只建过测试行(已清理)。
+    也就是说这个坑**只在「前端新建企业行」时才会触发**,本轮补上后不会再产生
+- **下一步最佳动作**:无(本轮已闭环);若要继续,见 Session 031 的候选优先级
+

+ 1 - 0
harness/tools/_entry-company-classify.ts

@@ -31,6 +31,7 @@ export {
   resolveSyncedIdentity,
   sameTagSet,
   stableJson,
+  withSystemTitle,
 } from '../../src/network/api/dms/classification-sync-utils';
 
 /** 注入分类结果 / 荣誉列表 / 工商信息直接落库(验证脚本用它打真实 DMS,不依赖分类接口) */

+ 6 - 0
harness/tools/verify-company-classify-dms.mjs

@@ -249,6 +249,12 @@ try {
 
   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);

+ 5 - 0
harness/tools/verify-company-classify-e2e.mjs

@@ -244,6 +244,11 @@ check(
   })(),
   afterRow.c_area
 );
+check(
+  "系统字段 title = 企业名称(DMS 列表里显示的那一列)",
+  afterRow.title === (profileData.Name || identity.name),
+  afterRow.title
+);
 check(
   "**语义相同的 JSON 列被原样保留**(没有被我们重写成另一种格式)",
   JSON_COLS.every((c) => beforeJson[c] === undefined || afterRow[c] === beforeJson[c]),

+ 14 - 0
harness/tools/verify-company-classify-sync.mjs

@@ -39,6 +39,7 @@ import {
   resolveSyncedIdentity,
   sameTagSet,
   stableJson,
+  withSystemTitle,
 } from './_company-classify.mjs';
 
 let pass = 0;
@@ -206,6 +207,19 @@ console.log('\n【3b】工商信息字段映射与比较');
   check('存量是空串、本次有值 → 写', diffEnterpriseRow({ c_revoke_info: '' }, { c_revoke_info: '{"A":1}' })?.c_revoke_info === '{"A":1}');
   check('普通文本列语义同前(不同才写)', diffEnterpriseRow({ c_status: '存续' }, { c_status: '存续(在营、开业、在册)' })?.c_status === '存续(在营、开业、在册)' && diffEnterpriseRow({ c_status: '存续' }, { c_status: '存续' }) === null);
   check('stableJson 递归排序键', stableJson({ b: 1, a: [{ d: 2, c: 3 }] }) === '{"a":[{"c":3,"d":2}],"b":1}');
+
+  // 系统字段 title = 企业名称(不写的话 DMS 会落成字符串 "null",列表里看不出是哪家)
+  check('title 取 c_name', withSystemTitle({ c_name: '合成公司', c_status: '存续' }).title === '合成公司');
+  check('title 已等于 c_name → 原样返回(不产生无谓写入)', (() => {
+    const row = { c_name: '合成公司', title: '合成公司' };
+    return withSystemTitle(row) === row;
+  })());
+  check('title 是字符串 "null" → 修正为企业名称', withSystemTitle({ c_name: '合成公司', title: 'null' }).title === '合成公司');
+  check('c_name 为空 → 不动 title(不清存量)', (() => {
+    const row = { title: '原有标题', c_status: '存续' };
+    return withSystemTitle(row) === row;
+  })());
+  check('title 变化会被 diff 捕获(更新路径也能补上)', diffEnterpriseRow({ c_name: '合成公司', title: 'null' }, withSystemTitle({ c_name: '合成公司' }))?.title === '合成公司');
 }
 
 /* ---------------------------------------------------------------- *

+ 16 - 0
src/network/api/dms/classification-sync-utils.ts

@@ -365,6 +365,22 @@ export const buildProfileFields = (profileData: unknown): DmsRow => {
 export const readProfileDataFromCompanyInfo = (info: unknown): unknown =>
   asRecord(asRecord(asRecord(info)?.profile)?.data) ?? null;
 
+/**
+ * 补上 DMS 的**系统字段 `title`**(列表里显示的那一列)。
+ *
+ * 为什么要显式写(实测教训,与 1886 同因):
+ * - `title` 只在**创建那一刻**初始化,之后改 `c_*` 字段**不会带动它**;
+ * - 创建时不传,DMS 落的是**字符串 `"null"`**(1889 踩过,列表里看不出是哪一条)。
+ *
+ * 取值就用 `c_name`(企业名称);`c_name` 为空时不动 `title`(**不清存量**)。
+ * 库里灌入的行本来就有 title,值相同不会产生写入。
+ */
+export const withSystemTitle = (row: DmsRow): DmsRow => {
+  const name = pickString(row?.c_name);
+  if (!name || pickString(row?.title) === name) return row;
+  return { ...row, title: name };
+};
+
 /* ------------------------------------------------------------------ *
  * 1888 diff
  * ------------------------------------------------------------------ */

+ 5 - 2
src/network/api/dms/company-info-sync.ts

@@ -48,6 +48,7 @@ import {
   readHonorsFromCompanyInfo,
   readProfileDataFromCompanyInfo,
   resolveSyncedIdentity,
+  withSystemTitle,
   type CompanyClassification,
   type CompanyIdentity,
 } from "./classification-sync-utils";
@@ -217,11 +218,13 @@ export const applyCompanyHonors = async (
 
 /** 1888:没有就新增,有就按差异更新(无差异零写入) */
 async function upsertEnterpriseRow(creditCode: string, desired: DmsRow): Promise<void> {
+  // 系统字段 title = 企业名称(两条入口共用这一处,见 withSystemTitle 的说明)
+  const target = withSystemTitle(desired);
   const existing = await findDmsRowBy(DMS_COLUMN_ENTERPRISE, "c_credit_code", creditCode);
 
   if (!existing) {
     const content: DmsRow = {
-      ...desired,
+      ...target,
       // desired 里可能回退到了前端抽的 identity,这里兜底保证幂等键一定在
       c_credit_code: creditCode,
       c_created_at: nowDmsTimestamp(),
@@ -231,7 +234,7 @@ async function upsertEnterpriseRow(creditCode: string, desired: DmsRow): Promise
     return;
   }
 
-  const patch = diffEnterpriseRow(existing, desired);
+  const patch = diffEnterpriseRow(existing, target);
   if (!patch) return; // 无变化:不写
 
   const id = typeof existing.id === "string" ? existing.id : "";

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません