# 客户拉通大表 · 后端结构清单（CloudBase 七表）

- 版本：2026-10-10 v1（同日增补 §8「宽表（推移表）存储与渲染策略」）；2026-10-11 增补 §9「两层设计（第一层项目推移表 / 第二层活动录入）的数据流与落盘」、§10「经营仪表盘指标字典」、**§11「周会决议（三岗各填一栏）与经营仪表盘报告版版式规范」**、**§12「仪表盘交互（拖拽/缩放/真自适应/布局持久化）与第二层活动录入精简（模板化描述 + 段级校验）」**
- 落地环境：CloudBase env `bier-sales-d0gatbvlx288724e9`（`脚本/销售工作台服务.py:660` `CB_ENV`）
- 依据：`知识沉淀/客户拉通大表_可复用数据源盘点_20261010.md`（下称「盘点」）
- 口径：**只追加不覆盖（append-only）**；三类 ID 缺口（活动→客户 / 活动·订单→商机 / 商机→客户）**允许为 null**，先靠「客户名 + `aliases[]` 别名桥」连接；前端把「未关联 ID」的行标黄。
- 复用说明：**表 C `activities` 沿用现有集合**（`销售工作台服务.py:700 ACT_CB_COLLECTION='activities'`），其余 6 张为新建集合。

---

## §1 集合总览

| 表 | CloudBase 集合名 | 用途 | 主键 | 来源 | 备注 |
|---|---|---|---|---|---|
| A | `cust_master` | 客户主档 | `work_item_id` | 已有（飞书客户） | 入 `aliases[]` 别名桥 |
| B | `opportunities` | 商机 | `work_item_id` | 已有（飞书 story） | 状态机载体 |
| C | `activities` | 每日活动（入口） | `id`=`FS`+`work_item_id` | **已有集合** | 沿用现格式，新增 3 字段 |
| D | `support_todos` | 资源协助待办 | `todo_id` | 新建（工作台内部表） | 三类：研发立项/技术支持测试单/售后进度跟进 |
| E | `dept_feedback` | 部门反馈流水 | `feedback_id` | 新建 | 谁·何时·做了什么·附件·可见范围 |
| F | `orders` | 订单记录 | `doc_no`（单据编号，对接键） | 已有（飞书订单/ERP） | 测试单/销售单 |
| G | `thread_view` | 拉通视图（事件流） | `event_id` | 新建 | 按商机聚合，一行一事件，只追加 |

> **说明**：现有 4 集合 `activities`/`todos`/`user_roles`/`visit_cards`（盘点 §3.5）。本清单新增 D 用 `support_todos` 与通用 `todos` 区分（现有 `todos` 承载活动派生待办，D 专指「资源协助待办」三类）。若产品部决定合并，可把 `todos.type ∈ {研发立项,技术支持测试单,售后进度跟进}` 视作同表——结构字段完全一致。

---

## §2 逐表字段

图例：**必** = 必填（不允许空）；**可** = 可选；来源标注 `已有（出处）` 或 `新建`。

### 表 A · `cust_master`（客户主档）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建（CloudBase 自动） | `"a1b2c3"` |
| `work_item_id` | number | 必 | 已有（盘点 §2「客户·工作项 ID」/ `数据/mcp_customers.json`→`customers[].work_item_id`） | `6878118191` |
| `name` | string | 必 | 已有（`mcp_customers.json`→`name`） | `"上海纳全生物医学科技有限公司"` |
| `short` | string | 可 | 已有（`customer_360.json`→`customers[].short` / `mcp_customers.json`→`field_c3224b`） | `"圣湘生物科技股份"` |
| `group` | string | 可 | 已有（`customer_360.json`→`customers[].group`） | `"圣湘集团"` |
| `aliases` | array\<string\> | 可 | **新建**（别名桥，解决名称不一致） | `["圣湘","圣湘生物","圣湘生物科技"]` |
| `sales` | string | 可 | 已有（`数据/customer_sp_map.json`→`map` / `customer_360.json`→`customers[].sales`） | `"赵云浩"` |
| `dept` | string | 可 | 已有（飞书客户 `field_5ed7ab`，`脚本/patch_customer_scope.py:11`） | `"诊断原料销售拓展部"` |
| `region` | string | 可 | 已有（飞书客户 `field_5ed7ab`，同上） | `"广东"` |
| `tier` | string | 可 | 已有（飞书客户 `field_17186c`，`脚本/pull_customer_levels.py:41`） | `"A类"` |
| `level` | string | 可 | 已有（飞书客户 `field_c8e80d`，同上） | `"大客户"` |
| `contacts` | array\<string\> | 可 | 已有（`数据/customer_contacts.json`→`contacts{客户名:[人名]}`） | `["张工","李工"]` |
| `status` | string | 可 | 已有（`customer_360.json`→`latest_date`/`orders` 派生 / `数据/diag_customers.json`→`status`） | `"活跃"` |
| `updated_at` | number | 可 | 新建 | `1760000000000`（ms 时间戳） |

### 表 B · `opportunities`（商机）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建 | `"o1"` |
| `work_item_id` | number | 必 | 已有（盘点 §2「商机·工作项 ID」/ `销售工作台服务.py:2349` story） | `7038571941` |
| `name` | string | 必 | 已有（`数据/opps_by_customer.json`→`[].name` / `数据/ketizu_list.json`） | `"DNA单管"` |
| `customer_id` | number \| **null** | 可 | **缺口**（盘点 §4-4：商机→客户 ID 无落地） | `6878118191` 或 `null` |
| `customer_name` | string | 必 | 已有（`opps_by_customer.json` 按客户名分组） | `"圣湘生物科技股份有限公司"` |
| `status` | string | 必 | 已有（`opps_by_customer.json`→`status`，飞书 `work_item_status`） | `"实现销售"` |
| `stage` | number | 必 | **新建**（状态机序号 0–5，前端进度条用） | `4` |
| `owner` | string | 可 | 已有（`opps_by_customer.json`→`creator`；飞书 `owner`） | `"赵云浩"` |
| `expect_amount` | number \| null | 可 | 已有（`数据/opportunity_sandbox.json`，口径=沙盘；**示例值**） | `480000` |
| `ketizu_mode` | string | 可 | 已有（飞书商机 `课题组模式`=`field_d3c543`） | `"一类"` |
| `ketizu_l1` | string | 可 | 已有（飞书商机 `一级课题组`=`field_2c3bee`） | `"呼吸道检测课题组"` |
| `open_requests` | number | 可 | **新建**（派生：D 未闭环数） | `0` |
| `closed_requests` | number | 可 | **新建**（派生：D 已闭环数） | `3` |
| `can_close` | boolean | 可 | **新建**（派生：所有 D 闭环 → true） | `true` |
| `created_at` | number | 可 | 新建 | `1760000000000` |

### 表 C · `activities`（每日活动 · **沿用现有集合**）

> 现有记录结构：`id/created/submitted_by/sales/type/subtype/customer/contact/date/channel/order_no/sku_count/att_count/opp_count/block_count`（`销售工作台服务.py:1226-1228`）。下表 **新增 3 字段**（★）支持「勾选需要资源协助」。

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `id` | string | 必 | 已有（`脚本/pull_activities_mcp.py:181`，`'FS'+work_item_id`） | `"FS7132642710"` |
| `submitted_by` / `sales` | string | 必 | 已有（飞书活动 `创建者`→`sales`/`submitted_by`） | `"赵云浩"` |
| `type` | string | 可 | 已有（飞书活动 `名称`→`type`） | `"线下拜访"` |
| `subtype` | string | 可 | 已有（活动细分类型） | `"技术交流"` |
| `customer` | string | 可 | 已有（飞书活动 `活动关联客户`=`field_5f20fc`，读回仅 label） | `"圣湘生物科技股份有限公司"` |
| `customer_id` | number \| **null** | 可 | **缺口**（盘点 §4-2：活动→客户 ID） | `6878118191` 或 `null` |
| `contact` | string | 可 | 已有 | `"张工"` |
| `date` | string | 必 | 已有（飞书活动 `创建时间`/`field_a2b3f6`） | `"2026-08-05"` |
| `channel` | string | 可 | 已有（飞书活动 `活动类型`=`field_76654e`；offline/online/cold/joint） | `"offline"` |
| `dept_raw` | string | 可 | 已有（飞书活动 `跟进部门`=`field_5ed7ab`） | `"诊断原料销售拓展部"` |
| `text` | string | 可 | 已有（飞书活动 `活动描述`=`field_b99055`） | `"PI 提出冻干体系放大需求"` |
| `order_no` | string \| null | 可 | 已有 | `null` |
| `opp_id` | string \| **null** | 可 | **缺口**（盘点 §4-3：活动→商机） | `"7038571941"` 或 `null` |
| `need_support` ★ | boolean | 可 | **新建**（勾选「需要资源协助」） | `true` |
| `support_types` ★ | array\<string\> | 可 | **新建**（多选：研发立项/技术支持测试单/售后进度跟进） | `["研发立项","技术支持测试单"]` |
| `support_todo_ids` ★ | array\<string\> | 可 | **新建**（生成 D 的回指） | `["TD20260805001","TD20260805002"]` |
| `sku_count`/`att_count`/`opp_count`/`block_count` | number | 可 | 已有 | `3` |
| `created` | number | 可 | 已有 | `1760000000000` |

### 表 D · `support_todos`（资源协助待办 · **工作台内部表**）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建 | `"t1"` |
| `todo_id` | string | 必 | 新建（`TD`+日期+序号）/ 对齐 `数据/todos.json` | `"TD20260805001"` |
| `source_activity_id` | string | 必 | 已有（`数据/todos.json`→`source{kind:'activity',activity_id}`，`销售工作台服务.py:1160`） | `"FS7132642710"` |
| `customer_id` | number \| **null** | 可 | 兼容缺口 | `6878118191` |
| `customer_name` | string | 必 | 已有（`数据/todos.json`→`client`） | `"圣湘生物科技股份有限公司"` |
| `opp_id` | string \| **null** | 可 | **缺口**（活动→商机） | `"7038571941"` |
| `type` | string | 必 | 已有口径（`todos.json`→`type`/`cat`；`TD_SCOPE`，`销售工作台服务.py:1151`）：**研发立项 / 技术支持测试单 / 售后进度跟进** | `"研发立项"` |
| `text` | string | 必 | 已有（`todos.json`→`text`） | `"冻干保护剂体系立项评估"` |
| `owner_sales` | string | 可 | 已有（`todos.json`→`owner`） | `"赵云浩"` |
| `assignee_dept` | string | 可 | **新建**（承接部门） | `"研发部"` |
| `assignee` | string | 可 | **新建**（承接人） | `"王工"` |
| `sla_days` | number | 可 | **新建** | `10` |
| `due` | string | 可 | 已有（`todos.json`→`due`） | `"2026-08-15"` |
| `status` | string | 必 | 已有（`todos.json`→`done`/`closed` 归一）：待认领→已认领→处理中→待闭环→已闭环 / 已退回 | `"已闭环"` |
| `claimed_at` | number | 可 | **新建** | `1760000000000` |
| `conclusion` | string | 可 | 已有（`todos.json`→`conclusion`） | `"立项评估通过，进入配方开发"` |
| `closed_by` | string | 可 | 已有（`todos.json`→`closedBy`） | `"王工"` |
| `closed_at` | number | 可 | 已有（`todos.json`→`closed`） | `1760000000000` |
| `created` | number | 可 | 已有（`todos.json`→`created`） | `1760000000000` |

### 表 E · `dept_feedback`（部门反馈流水）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建 | `"f1"` |
| `feedback_id` | string | 必 | 新建 | `"FB20260807001"` |
| `todo_id` | string | 必 | **新建**（关联 D） | `"TD20260805001"` |
| `opp_id` | string \| null | 可 | **新建** | `"7038571941"` |
| `customer_id` | number \| **null** | 可 | 兼容缺口 | `6878118191` |
| `actor` | string | 必 | 已有（`数据/sr_review_kac.json`→`activity_owner`；`todos.json`→`closedBy`） | `"王工"` |
| `actor_dept` | string | 可 | **新建** | `"研发部"` |
| `action` | string | 必 | **新建**（做了什么） | `"提交立项评估结论"` |
| `content` | string | 必 | 已有（`数据/test_feedback_202608.json`→`raw`） | `"冻干保护剂体系可行性已验证"` |
| `attachments` | array\<string\> | 可 | 已有（活动附件/`数据/活动附件` 路径） | `["配方初稿.pdf"]` |
| `visibility` | array\<string\> | 可 | **新建**（可见范围：`sales`/`dept`/`tech`/`rd`/`all`） | `["sales","dept","tech","rd"]` |
| `created` | number | 必 | 新建（何时） | `1760000000000` |

### 表 F · `orders`（订单记录 · 测试单/销售单）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建 | `"od1"` |
| `doc_no` | string | 必 | 已有（飞书 `销售订单.name` = ERP `单据编号`，**两侧对接键**，盘点 §3.3） | `"ZHBR260907070"` |
| `work_item_id` | number \| null | 可 | 已有（`数据/mcp_orders_input.json`→`工作项id`；`数据/pull_orders.py:272`） | `7109150199` |
| `type` | string | 可 | 已有（飞书 `field_a6e501`；ERP `单据类型`）：标准销售订单/测试销售订单/预测订单 | `"标准销售订单"` |
| `customer_id` | number \| **null** | 可 | 兼容缺口 | `6878118191` |
| `customer_name` | string | 可 | 已有（飞书 `购货单位#`=`field_e1001d` / ERP `客户` 列） | `"圣湘生物科技股份有限公司"` |
| `opp_id` | string \| **null** | 可 | **缺口**（盘点 §4-3：订单→商机；生命科学有 `商机(飞书)#`，诊断原料无） | `"7038571941"` |
| `ketizu_l1` | string | 可 | 已有（ERP 生命科学 `商机(飞书)#`；`数据/order_ketizu_map.json` 覆盖 88.68%） | `"呼吸道检测课题组"` |
| `salesperson` | string | 可 | 已有（ERP `销售员` 列 / 飞书下单人角色） | `"赵云浩"` |
| `sku` | string | 可 | 已有（飞书 `field_8c4b7b` / ERP `货号#`） | `"MD2104-000804-1"` |
| `amount` | number | 可 | 已有（飞书 `金额`=`field_51d592` / ERP `价税合计`） | `52000` |
| `batch_no` | string | 可 | 已有（ERP `出货批号#`；飞书 `field_8d9153` 待确认） | `"20260417"` |
| `prep_dept` | string | 可 | 已有（ERP `备货部门`） | `"珠海备货仓"` |
| `product_type` | string | 可 | 已有（ERP `产品类型#`） | `"分子诊断原料"` |
| `product_category` | string | 可 | 已有（ERP `产品大类#`） | `"PCR原料"` |
| `biz_closed` | string | 可 | 已有（ERP `业务关闭` / `关闭状态`） | `"未关闭"` |
| `is_gift` | boolean | 可 | 已有（ERP `是否赠品`） | `false` |
| `delivery_date` | string | 可 | 已有（飞书 `field_98f9e8` / ERP `发货日期`） | `"2026-09-30"` |
| `tracking_no` | string | 可 | 已有（飞书 `field_96a245` 物流单号） | `"SF1234567890"` |
| `created` | number | 可 | 新建 | `1760000000000` |

### 表 G · `thread_view`（拉通视图 · append-only 事件流）

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `_id` | string | 必 | 新建 | `"e1"` |
| `event_id` | string | 必 | 新建 | `"EV20260805001"` |
| `opp_id` | string \| null | 可 | 聚合键（可空：未关联商机的活动/订单归入 `null` 桶） | `"7038571941"` |
| `customer_id` | number \| null | 可 | 兼容缺口 | `6878118191` |
| `event_type` | string | 必 | 新建（activity/todo_create/todo_claim/feedback/todo_close/order/opp_stage） | `"feedback"` |
| `source_collection` | string | 必 | 新建（activities/support_todos/dept_feedback/orders） | `"dept_feedback"` |
| `source_id` | string | 必 | 新建（源记录主键，回链） | `"FB20260807001"` |
| `ts` | number | 必 | 新建（**排序键**，事件时间 ms） | `1760000000000` |
| `title` | string | 必 | 新建 | `"研发部反馈 · 立项评估通过"` |
| `summary` | string | 可 | 新建 | `"王工：冻干保护剂体系可行性已验证"` |
| `actor` | string | 可 | 新建 | `"王工/研发部"` |
| `amount` | number \| null | 可 | 新建（订单事件用） | `8600` |
| `linked` | boolean | 必 | **新建**（是否已关联到 ID；false → 前端标黄「待关联」） | `true` |
| `payload` | object | 可 | 新建（源记录快照，防上游改动） | `{"status":"已闭环"}` |

---

## §3 关联键与状态机

### 3.1 关联键（外键）

| 关系 | 键 | 现状 | 处理 |
|---|---|---|---|
| A ↔ B 商机→客户 | `opportunities.customer_id` → `cust_master.work_item_id` | **缺口（null）** | 先用 `customer_name` + `cust_master.aliases[]` 归一匹配 |
| A ↔ C 活动→客户 | `activities.customer_id` → `cust_master.work_item_id` | **缺口（null）** | 同上；活动只读回客户名 label |
| C ↔ D 活动→待办 | `support_todos.source_activity_id` → `activities.id` | ✓ 现有（`todos.source.activity_id`） | 直接外键 |
| C/D/F ↔ B 商机 | `*.opp_id` → `opportunities.work_item_id` | **缺口（null）** | 前端未关联行标黄；后续按「客户+时间」近似或飞书补字段 |
| D ↔ E 待办→反馈 | `dept_feedback.todo_id` → `support_todos.todo_id` | 新建 | 直接外键 |
| F ↔ ERP 订单 | `orders.doc_no` = ERP `单据编号` | ✓ 现成 | 两侧完全一致 |
| G 聚合 | `thread_view.opp_id` → `opportunities.work_item_id` | 新建 | 事件流按 `opp_id` 聚合、按 `ts` 排序 |

**别名桥（解决名称不一致）**：`cust_master.aliases[]` 收集简称、集团名、历史名；入库前先按 `aliases` 命中 → 回填 `customer_id`；未命中 → `customer_id=null` + 行标黄。

### 3.2 状态机

**① 资源协助待办 D（support_todos.status）**
```
待认领 ──认领──▶ 已认领 ──开始处理──▶ 处理中 ──写结论──▶ 待闭环 ──承接人确认──▶ 已闭环（终态）
   │                    │
   └──── 退回 ◀─────────┘（部门认为不属于本部门 → 退回销售）
```
- 认领/处理/闭环**仅承接人**可操作；退回后回到发起销售的视野。

**② 商机 B（opportunities.status / stage）**
```
0 沟通需求确认 ─▶ 1 产品试用中 ─▶ 2 订单签订中 ─▶ 3 实现销售 ─▶ 4 成交 ─▶ 5 闭环
```
> 对齐飞书 `opps_by_customer.json.status`（沟通需求确认/产品试用中/订单签订中/实现销售）。

**③ 成交/闭环门禁（Gate）**
- B 只有在其名下**所有** `support_todos.status == 已闭环`（`open_requests == 0`）时，`can_close` 才为 `true`，才允许把 B 标为 **成交/闭环**。
- 校验时机：D 闭环写回时，重算 B 的 `open_requests`/`closed_requests`/`can_close`。

### 3.3 端到端工作流（写进页面的链路）
```
销售填每日活动(C)
  → 勾选「需要资源协助」+ 选类型
  → 系统按类型生成待办(D)  [研发立项/技术支持测试单/售后进度跟进]
  → 部门承接人认领(D.status=已认领)
  → 进展写反馈流水(E)
  → 销售/技术支持/研发 同一视图可见(G)
  → 承接人写闭环结论 → 回写 D.status=已闭环
  → 触发商机 B 推进(重算 open_requests)
  → 该商机下所有请求闭环 → B 才可标成交/闭环
  → 全过程 append-only 写 G(thread_view)，只追加不覆盖
```

---

## §4 权限矩阵（角色 × 字段 × 可读写）

角色：`sales` 销售员 / `dept` 部门承接人（研发·技术支持·售后）/ `support` 销售支持 / `admin` 管理员。
**管理员不受字段限制（read+write 全字段）。**

| 集合 | 字段分组 | sales（销售发起） | dept（部门承接人） | support（销售支持） | admin（管理员） |
|---|---|---|---|---|---|
| A `cust_master` | 主档字段 | R | R | R | RW |
| A | `aliases[]` / `sales` | — | — | R | RW |
| B `opportunities` | `status`/`stage` | **R**（成交需门禁） | R | **RW**（可标成交/闭环，须 `can_close`） | RW |
| B | 其余字段 | R | R | RW | RW |
| C `activities` | 全部 | RW（本人） | R | RW | RW |
| C | `need_support`/`support_types` | RW（本人） | — | RW | RW |
| D `support_todos` | `text`/`type`/`owner_sales`/`customer_*`/`opp_id` | **RW（发起时）** | R | RW | RW |
| D | `assignee_dept`/`assignee`/`sla_days`/`due` | R | **R** | **RW（指派）** | RW |
| D | `status`/`claimed_at`/`conclusion`/`closed_by`/`closed_at` | R | **RW（仅自己承接的）** | R | RW |
| E `dept_feedback` | `content`/`action`/`attachments` | R | **RW（自己承接的待办）** | RW | RW |
| E | `visibility` | R | R | RW | RW |
| F `orders` | 全部 | R | R | RW | RW |
| G `thread_view` | 全部 | R | R | **R（只读，append 由系统）** | RW |

**可读范围（行级）**
- `sales`：仅 `owner_sales==自己` 或 `customer.sales==自己` 的记录（对齐现有工作台的隔离口径）。
- `dept`：本部门认领的 D/E 可见；跨商机的同行记录只读。
- `support`/`admin`：全量。

**CloudBase 权限规则草案（安全规则 JSON）**
```json
{
  "read": "auth != null && (doc.sales == auth.uid || get_user_role(auth.uid) in ['support','admin'])",
  "write": "auth != null && (get_user_role(auth.uid) == 'admin' || (doc.owner_sales == auth.uid && doc._field_group == 'init'))"
}
```
> 说明：CloudBase 集合级 `read/write` 只做粗粒度网关；**字段级**限制（部门只改自己那栏）在**服务端云函数**里做二次校验（按角色白名单字段），前端字段 `disabled` 仅作提示、不作安全边界。`thread_view` 设为**客户端只读、服务端云函数写**（append-only）。

---

## §5 索引建议

| 集合 | 索引 | 用途 |
|---|---|---|
| `cust_master` | `work_item_id`(唯一)、`name`、`aliases` | 主键查 / 名称归一 |
| `opportunities` | `work_item_id`(唯一)、`customer_name`、`status` | 商机挂客户 / 筛选 |
| `activities` | `id`(唯一)、`customer_id`、`opp_id`、`date` | 时间线取数 |
| `support_todos` | `todo_id`(唯一)、`source_activity_id`、`opp_id`、`status` | 聚合、门禁重算 |
| `dept_feedback` | `feedback_id`(唯一)、`todo_id`、`opp_id`、`created` | 穿针 |
| `orders` | `doc_no`(唯一)、`opp_id`、`customer_id` | ERP 对接 / 聚合 |
| `thread_view` | `event_id`(唯一)、`opp_id`+`ts`(复合)、`event_type` | 商机时间线排序、来源筛选 |

---

## §6 字段级数据来源（链接盘点报告）

| 表 | 对应盘点章节 | 复用率 | 主要缺口 |
|---|---|---|---|
| A 客户主档 | §1 表A | ≈85% | 唯一编码（靠 work_item_id+名称归一） |
| B 商机 | §1 表B | ≈70% | 商机→客户显式外键；金额口径 |
| C 每日活动 | §1 表C | 字段≈95% / 关联≈30% | 活动→客户 ID、活动→商机 |
| D 资源协助待办 | §1 表D | ≈55% | 研发立项无实体；测试单库空 |
| E 部门反馈 | §1 表E | ≈45% | 跨部门派单/回复表未建（本清单新建） |
| F 订单 | §1 表F | ≈95% | 订单→商机外键（诊断原料无列） |
| G 拉通视图 | §1 表G | ≈40% | 统一视图 + 跨表外键（本清单新建） |

> 飞书/CloudBase 实体名逐字取自盘点 §2 与 §3.5，未改动。

---

## §7 待确认（沿用盘点 §5，影响结构）
1. 飞书 `create_workitem` 可用 `work_item_type`/`template_id`（现 `销售工作台服务.py:3392-3397` 用 `template_id:'700373'`）——影响 D 是否能回写飞书。
2. `field_5f20fc` 能否取客户 `work_item_id` 而非 label——影响 `activities.customer_id` 回填。
3. 商机「关联客户」字段名是否存在于 `story` 表——影响 `opportunities.customer_id`。
4. 课题组口径：`ketizu_list.json`(1744) vs ERP `商机(飞书)#`(88.68%)。
5. `field_8d9153` 是否 = 出货批号#。

---

## §8 宽表（项目推移表）的 CloudBase 存储与渲染策略

> 背景：DEMO《客户拉通大表》由「时间线卡片」改造为**推移表（宽表）**——行＝客户/商机、列＝阶段 + 资源协助 + 月份，**行向下、列向右均可无限延展**，对齐参照表《BRSW-营销中心-客户项目推移表V02.xlsx》（Sheet「进度清单（周更）」= Tab① 宽表；Sheet「沟通内容详单（日更）」= Tab② 流水表）。
> **§1–§7 七表结构不变**；本节是它们在宽表形态下的**投影与内嵌口径**，不改动、不删除原七表内容。

### 8.1 核心口径：行是文档，列是「定义」而非「字段」

关系型宽表「加一列 = 改表结构（DDL）+ 迁移全表」；文档型（CloudBase）里**列不是 schema，而是数据**：

- **一行一商机** → 一条 `wide_rows` 文档；
- **一列一阶段/月份** → 一条 `column_defs` 文档（**列定义表驱动，列不硬编码**）；
- 单元格的值存在行文档的**对象字段**里（`stages{}` / `months{}`），键 = `col_id`；
- 新增列 ⇒ **只插一条 `column_defs`**，不碰任何 row 文档；新增行 ⇒ 只插一条 `wide_rows`。

因此**加列/加行都只是 append 一条文档：零迁移、零停机、零前端改版**。

### 8.2 集合 A · `wide_rows`（宽表行 · 一行一商机）

| 字段名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| `_id` | string | 必 | CloudBase 自动 | `"wr1"` |
| `row_key` | string | 必 | 行键（= 商机 `work_item_id`，唯一） | `"7038571941"` |
| `customer_id` | number \| **null** | 可 | 缺口允许 null（盘点 §4） | `6878118191` |
| `customer_name` | string | 必 | 客户名（配 `cust_master.aliases[]` 归一） | `"圣湘生物科技股份有限公司"` |
| `region` / `tier` | string | 可 | 地区 / 等级 | `"湖南"` / `"A类"` |
| `sales` / `tech` | string | 可 | 宝锐销售 / 宝锐技术 | `"赵云浩"` / `"李工"` |
| `dept` | string | 可 | 部门（宽表筛选用，可不占列） | `"诊断原料销售拓展部"` |
| `status` | string | 可 | 商机状态（沟通需求确认/产品试用中/订单签订中/实现销售/成交） | `"实现销售"` |
| `opp_name` | string | 必 | 商机名称 | `"多重呼吸道病原体冻干体系开发"` |
| `solution` | string | 可 | 试剂匹配方案 | `"MD2104 冻干保护剂 + 一步法 RT-qPCR MIX"` |
| `stages` | object | 可 | **阶段 → 单元格**（键 = `column_defs.col_id`） | `{"st_制定解决方案":{"state":"已完成","date":"2026-05-12","owner":"赵云浩","evidence":["方案_v1.pptx"],"unlinked":false}}` |
| `months` | object | 可 | **月份 → 单元格**（键 = 月份列 `col_id`，如 `"2026-10"`） | `{"2026-10":{"state":"进行中","date":"2026-10-08","owner":"赵云浩"}}` |
| `support_requests` | array\<object\> | 可 | **资源协助**（研发立项/技术支持测试单/售后进度跟进） | 见 8.3 |
| `close` | object | 可 | 闭环单元格 | `{"state":"进行中","date":"2026-10-05","owner":"销售支持"}` |
| `unlinked` | boolean | 可 | 该行任一 ID 未回填 → 前端标黄「待关联」 | `true` |
| `events` | array\<object\> | 可 | **append-only 反馈流水**（单元格抽屉的数据源） | 见 8.4 |
| `updated_at` | number | 可 | 最后写入 ms | `1760000000000` |

**单元格（cell）统一形状**：`{state, date, owner, evidence[], unlinked}`，`state ∈ {未开始, 进行中, 已完成, 卡住}`（对应前端灰/蓝/绿/红四色块）。

### 8.3 `support_requests[]`（资源协助 · 内嵌数组）

> 对应 §1 表 D `support_todos` 的**内嵌口径**（宽表一行固定三列：研发立项 / 技术支持测试单 / 售后进度跟进）。

| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| `type` | string | 研发立项 / 技术支持测试单 / 售后进度跟进 | `"研发立项"` |
| `todo_id` | string | 回指表 D（若保留独立集合） | `"TD20260805001"` |
| `state` | string | 单元格状态 | `"已完成"` |
| `date` / `owner` | string | 日期 / 负责人（承接人） | `"2026-08-20"` / `"王工"` |
| `assignee_dept` | string | 承接部门 | `"研发部"` |
| `evidence` | array\<string\> | 附件名 | `["立项评估.pdf"]` |

### 8.4 `events[]`（append-only 反馈流水）

> 对应 §1 表 G `thread_view` 的**按行分片**：宽表每个单元格点开看到的即这些事件（谁 / 何时 / 做了什么 / 附件名）。

| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| `event_id` | string | 唯一 | `"EV20260805010"` |
| `col_id` | string | 归属列（阶段/月份/资源协助/闭环） | `"st_小试"` |
| `ts` | number | 事件时间 ms（**排序键**） | `1760000000000` |
| `actor` | string | 谁 | `"陈工/售后部"` |
| `action` | string | 做了什么 | `"客户现场小试通过"` |
| `attachments` | array\<string\> | 附件名 | `["小试报告_0702.pdf"]` |
| `linked` | boolean | 是否已关联 ID（false → 标黄） | `true` |

**只追加不覆盖**：单元格 `state/date` 是「最新快照」，`events[]` 是过程留痕；两者都写——前端表格读快照，抽屉读流水。

### 8.5 集合 B · `column_defs`（列定义 · 表驱动的灵魂）

| 字段名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| `_id` | string | 必 | 自动 | `"cd1"` |
| `col_id` | string | 必 | 列键（= row 文档里的对象键） | `"st_制定解决方案"` |
| `group` | string | 必 | `left` / `stage` / `support` / `month` / `close` | `"stage"` |
| `title` | string | 必 | 列头文字 | `"制定解决方案"` |
| `order` | number | 必 | 组内排序（渲染顺序） | `10` |
| `width` | number | 可 | 列宽 px | `112` |
| `sticky` | boolean | 可 | 是否冻结（`left` 组 = true） | `false` |
| `active` | boolean | 可 | 软删（下线列而不删数据） | `true` |
| `created_at` | number | 可 | 建列时间 | `1760000000000` |

**初始列**（对齐参照表）：`left` 8 列（客户编码 / 客户 / 地区 / 等级 / 宝锐销售 / 宝锐技术 / 商机名称 / 试剂匹配方案）、`stage` 9 列（制定解决方案 → 1次送样 → 2次送样 → 小试 → 中试 → 临床试验 → 临床资料申报 → 现场审核 → 获批量产）、`support` 3 列、`month` 当月 1 列、`close` 1 列。

### 8.6 新增列 / 新增行的存储策略

**① 新增列（对应 DEMO 的「＋新增阶段列」/「＋新增月份列」）**
1. 写一条 `column_defs`：`{col_id:'st_新增阶段10', group:'stage', title:'新增阶段 10', order:100, width:112, active:true}`（月份列则 `group:'month', col_id:'2026-11'`）。
2. **不修改任何 `wide_rows` 文档** —— 旧行的 `stages{}` / `months{}` 里没有该键。
3. 读取时缺键 ⇒ 渲染「—」（未填写）。

**② 新增行（对应「＋新增商机」）**
1. 写一条 `wide_rows`，`stages:{}` / `months:{}` / `support_requests:[]` 可为空对象/空数组。
2. 空对象 ⇒ 全部单元格渲染「—」。

**③ 列下线**：置 `active:false`（软删），数据仍留在 row 文档里，可随时恢复；**不物理删除**。

### 8.7 前端渲染策略（列定义表驱动 · 列不硬编码）

```
拉取 column_defs（按 group + order 排序）
  → 生成 COLS（col_id / title / width / sticky / group）
  → 生成 <colgroup>（列宽） + 两行表头（组表头 + 列表头）
  → 拉取 wide_rows
  → 每行按 COLS 逐列取值：
        group==='stage'   → row.stages[col_id]
        group==='month'   → row.months[col_id]
        group==='support' → row.support_requests.find(r => r.type === c.title)
        group==='close'   → row.close
        取不到            → 渲染「—」
  → 左侧 sticky 列按「累计列宽」算 left 偏移；表格显式宽度 = Σ 列宽
```

要点：
- **列不硬编码**：新增/下线列只改 `column_defs`，前端**零改动**、**零重新部署**（DEMO 正是这样：点按钮 → 列数组 push → 重渲染，无写死列）。
- **表宽 = Σ 列宽**：`table-layout:fixed` 必须配**显式表宽**，否则列宽塌缩、冻结列 `left` 偏移与实际列位错位（DEMO 已踩此坑并修正）。
- **冻结**：`left` 组 `position:sticky`；横向滚动只作用于 `stage/support/month/close` 组。
- **KPI**（进度异常 / 正常 / 重点关注）：由行内单元格 `state==='卡住'` 与 `unlinked` **派生**，不落库（需要可缓存）。

### 8.8 与 §1 七表的关系（两种落地口径）

| 宽表元素 | 七表来源 | 内嵌口径（本 DEMO / 轻量） | 独立集合口径（重协作） |
|---|---|---|---|
| 一行 | 表 B `opportunities` | `wide_rows` 文档 | 直接读 `opportunities` |
| `stages{}` | 新建派生 | 内嵌对象 | 可作 `stage_snapshots` 集合 |
| `support_requests[]` | 表 D `support_todos` | 内嵌数组 | 保留独立集合，前端 join |
| `events[]` | 表 G `thread_view` | 内嵌数组（按行分片） | 独立集合，`opp_id`+`ts` 复合索引 |
| 反馈附件 | 表 E `dept_feedback` | 折进 `events[].attachments` | 保留独立集合 |
| 列头 / 列宽 / 顺序 | 无（关系型里是 schema） | **`column_defs` 集合** | 同左 |

> 取舍：**内嵌**读一次文档即得整行宽表（适合 DEMO / 单商机视图）；**独立集合**便于跨行聚合与权限隔离（表 D/E/G 已定义）。二者可并存：`wide_rows` 只存**快照**，权威数据仍在七表，`thread_view` 事件按需回填。

### 8.9 索引建议（补充 §5）

| 集合 | 索引 | 用途 |
|---|---|---|
| `wide_rows` | `row_key`(唯一)、`customer_id`、`sales`、`dept`、`status` | 行定位 / 筛选取数 |
| `column_defs` | `col_id`(唯一)、`group`+`order` | 列定义排序渲染 |

### 8.10 待关联与 append-only 口径（沿用 §3.1）

- `customer_id` / `opp_id` 允许为 `null`；未回填 ⇒ 行或单元格 `unlinked:true`，前端标黄「待关联」（DEMO 保留 v1 此设计）。
- 宽表**只追加**：新行 = 新文档；新列 = 新 `column_defs`；单元格更新 = 写最新快照 + 追加一条 `events[]`；**不物理删除**（列下线用 `active:false`）。

---

## §9 两层设计（第一层项目推移表 / 第二层活动录入）的数据流与落盘

> 依据：用户 2026-10-11 给出的《客户拉通大表第一层及第二层demo》xlsx。**§1–§8 不动**；本节描述两层视图的数据流与存储，是 §8 宽表策略在「两层交互」形态下的落地口径。
> 对应 DEMO：`工具/客户拉通大表_demo.html`（生成器 `脚本/build_customer_thread_demo.py`；验收 `脚本/verify_customer_thread_demo.py`）。
> 旧版保留：`工具/客户拉通大表_demo_v1_时间线版.html`（v1）、`工具/客户拉通大表_demo_v2_单层宽表.html`（v2）、`工具/客户拉通大表_demo_v3_自动映射版.html`（v3，2026-10-11 改手动多选前的存档）。

### 9.1 两层的数据流：第二层（活动）→ 抓取 → 第一层（项目）

```
第二层 活动录入表（一行一活动）
   ├─ 组1 活动基本信息：部门 / 客户编码 / 客户名称 / 商机名称 / 竞品信息 / 销售员 / 技术支持 / 商机规模
   ├─ 组2 活动信息更新（可持续向右延展）：日期 / 拜访对象·联系人 / 关联订单 / 货号 / 活动类型
   └─ 组3 待办事项：事项 / 负责人 / 时间节点
        ↓ ① 抓取（按「客户名称 + 商机名称」匹配；客户名配 cust_master.aliases[] 归一）
        ↓ ② 回写（活动 → 项目）
第一层 项目推移表（一行一商机）
   ├─ 组1 项目基本信息：部门 / 客户编码 / 客户名称 / 商机名称 / 销售员 / 商机规模   ← 由活动基本信息抓取
   ├─ 组2 项目关键节点（可向右延展）：陌生开发 → 方案匹配 → 中试 → 临床试验 →
   │        已确认被用于注册 → 已转产 → 客户已获证 → 新证迭代                ← 手动维护 + 活动推进回写
   └─ 组3 资源协助（可向右延展）：技术支持测试项目单N / 研发项目单N / 售后N / 其他N  ← 由销售在第二层「手动多选」触发，选中即加列
```

- **抓取键**：`cust_master.name`（+`aliases[]`）与 `opportunities.name`；未匹配 ⇒ 该行/格 `unlinked:true`，前端黄色「待关联」（§3.1 / §8.10 口径不变）。
- **一行对应**：第二层「一行一活动」⇒ 表 C `activities` 一条；第一层「一行一商机」⇒ 表 B `opportunities` 一条（内嵌口径 = §8.2 `wide_rows`）。第一层行按 4 个部门分组：大客户部 / 销售拓展部 /  / 生物制药销售部 / 国际业务部。
- 用户四条逻辑：① 由活动的**基本信息、关联商机**抓取第一层项目基本信息；② 项目基本信息中的**竞对信息可手动维护**；③ **项目关键节点可手动维护**；④ **资源协作由销售在第二层手动多选触发**（技术支持测试项目单 / 研发项目单 / 售后 / 其他；选中即落表 D `support_todos` 并在第一层「资源协助」组加一列，见 9.5 / 9.8）。

### 9.2 三轨落盘：本地记录 / CloudBase / 飞书描述

提交一条活动，**同时写三处**（口径见 §1 表 C/D/E）：

| 轨 | 载体 | 主键 / 形态 | 说明 |
|---|---|---|---|
| ① 本地记录 | 本地台账（浏览器演示） | **活动编号 `ACT-YYYYMMDD-NNN`** | 追加一条活动对象；活动编号 = 当日序号，作两轨外键 |
| ② CloudBase 数据库 | 集合 `activities` + `support_todos` + `dept_feedback` | `activities.id="FS-"+act_id`；`support_todos.todo_id="TD-"+act_id`；`dept_feedback.feedback_id="FB-"+act_id` | 三集合用 `act_id` 串联写入 |
| ③ 飞书描述 | 文本（**一键复制**） | 固定模板（见下） | 供粘贴到飞书活动/群 |

- `activities`（表 C）：沿用现格式，**新增 3 字段** —— `structured_6d{}`（见 9.4）、`node_push`（本次推进的节点名，用于回写第一层）、`support_types[]`（本次手动多选的资源协作类型中文列表；服务端据此生成 D，见 9.8）。
- `support_todos`（表 D）：`type` 由**销售在第二层手动多选**的资源协作生成 —— **不再由活动类型自动映射**。`type` 枚举 `tech_test / rnd_project / after_sales / other`，`type_label` 存中文标签，`note` 存「其他」的说明；每选中一类生成一条待办文档（`todo_id = TD-<act_id>-NN`，`state` 初始 `待承接`），`todo / owner / due` 取自第二层「待办事项」。
- `dept_feedback`（表 E）：`actor`=销售员，`action`=`活动类型 · 待办事项`，`visible`=本部门 + 技术支持。

**飞书描述模板**（「一键复制」的实际文本）：

```
【活动录入】ACT-YYYYMMDD-NNN
部门：… ｜ 销售员：… ｜ 技术支持：…
客户：…（客户编码 …）
商机：… ｜ 商机规模：…
竞品信息：…
日期：… ｜ 拜访对象/联系人：… ｜ 关联订单：… ｜ 货号：…
活动类型：…
—— 本次触发的资源协作（销售手动多选）——
  1. 技术支持测试项目单 → 承接待办 TD-…（type=tech_test，state=待承接，负责人 …，时间节点 …）
  2. 其他（说明：…） → 承接待办 TD-…（type=other，state=待承接，负责人 …，时间节点 …）
  （未触发时写：  （本次未触发资源协作））
—— 6 维结构化 ——
① 客户需求（分点）：…
② 技术参数（货号+量化）：…
③ 测试单结果跟进：…
④ 问题现象（含客户原话）：…
⑤ 客户反馈：…
⑥ 竞对信息（品牌+量化）：…
—— 待办事项 ——
事项：…
负责人：… ｜ 时间节点：…
```

### 9.3 列定义表驱动：新增列不迁移

沿用 §8.5 `column_defs` 集合，**列不是 schema、而是数据**：

| 层 / 组 | 列集合（col_id 前缀） | 可向右延展 |
|---|---|---|
| 第一层 组1 | `dept / cid / cust / opp / sales / scale` | 否 |
| 第一层 组2 | `st:<节点名>`（陌生开发 / 方案匹配 / 中试 / 临床试验 / 已确认被用于注册 / 已转产 / 客户已获证 / 新证迭代） | **是**（＋新增节点列） |
| 第一层 组3 | `rs:<资源名>`（技术支持测试项目单N / 研发项目单N / 售后N / 其他N，N 为该组内该类已有列数+1） | **是**（＋新增资源协助列 / 销售手动多选一类自动加一列） |
| 第二层 组1 | 8 列基本信息 | 否 |
| 第二层 组2 | `date / contact / order / sku / type` | **是** |
| 第二层 组3 | `todo / owner / due` | 否 |

- **新增列 ⇒ 只写一条 `column_defs`**（`{col_id, group, title, order, width, active:true}`），**不修改任何行文档**；读取缺键渲染「—」。
- 单元格以 `col_id` 为键：第一层 `nodes{'st:中试':{…}}`、`res{'rs:技术支持测试项目单2':{…}}`；第二层每行 = 一条活动文档。
- 因此「＋新增节点列 / ＋新增资源协助列」与「销售手动多选触发的资源协助加列」都是 **append 操作：零迁移、零停机、前端零改版**。

### 9.4 6 维结构化字段如何存（`structured_6d{}`）

活动类型 =「**测试结果跟进**」时 **6 维缺一不可**（前端强制校验：缺项高亮、禁止提交并提示缺哪项）。存为活动文档内的**对象字段**：

| 键 | 维度 | 要求 |
|---|---|---|
| `d1` | ① 客户需求 | **分点**列述 |
| `d2` | ② 技术参数 | 含**货号 + 量化指标** |
| `d3` | ③ 测试单结果跟进 | 测试单编号 + 结果 |
| `d4` | ④ 问题现象 | 含**客户原话** |
| `d5` | ⑤ 客户反馈 | 客户结论 / 诉求 |
| `d6` | ⑥ 竞对信息 | **品牌 + 量化** |

```json
{ "act_id":"ACT-20261011-003", "type":"测试结果跟进",
  "structured_6d": { "d1":"…","d2":"…","d3":"…","d4":"…","d5":"…","d6":"…" },
  "node_push":"已转产" }
```

- 另外两种活动类型（采购跟进 / 客户档案完善）`structured_6d` **可空但建键齐全**（六键允许为空串，统一 schema 便于下游检索）。
- 检索：可按 `structured_6d.d6` 抽竞对、按 `structured_6d.d2` 抽货号/量化做台账。

### 9.5 两层联动的回写规则（活动 → 第一层）

一次活动提交，对第一层**对应商机行**（按 商机名称 匹配，回退 客户名称）执行：

| 联动 | 触发 | 回写内容 |
|---|---|---|
| **项目关键节点推进** | 表单选定「本次推进节点」≠ 不推进 | 该节点格 `{state:'已完成', date, owner:销售员, act:活动编号}` |
| **资源协助加列（每选一类 +1 列）** | 第二层「触发的资源协作」手动多选（见 9.8） | 每选中一类 → 追加一列 `技术支持测试项目单N` / `研发项目单N` / `售后N` / `其他N`（N = 该资源协助组内该类已有列数+1），并在该行该列写 `{state:'待承接', date, owner:发起销售员, act:活动编号}` |

- 未匹配到第一层行 ⇒ **不回写**、提示「未匹配商机行」，活动仍三轨落盘（`unlinked` 口径）。
- 回写只写**最新快照**（§8.4），过程留痕仍追加 `events[]`。

### 9.6 与 §1 七表 / §8 宽表的关系

| 两层元素 | 七表来源 | 备注 |
|---|---|---|
| 第一层 一行商机 | 表 B `opportunities`（内嵌 §8.2 `wide_rows`） | 组1 = §8 的 `left` 列（用户版列集） |
| 第一层 组2 节点 | §8 派生（与 `stages{}` 同构） | 用户版节点名替换参照表阶段名 |
| 第一层 组3 资源协助 | 表 D `support_todos`（内嵌 `support_requests[]`） | 活动资源单 +1 ⇒ 追加列 |
| 第二层 一行活动 | 表 C `activities` | 新增 `structured_6d{}` / `node_push` |
| 待办事项 | 表 D `support_todos` | `todo / owner / due / state` |
| 反馈流水 | 表 E `dept_feedback` · 表 G `thread_view` | append-only |
| 列头 / 列宽 / 顺序 | `column_defs` 集合 | 表驱动，加列不迁移 |

> §8 的 `wide_rows` / `column_defs` / `support_requests[]` / `events[]` 与本节完全兼容：本节把「宽表 + 流水表」两个 Tab 升级为「**第一层项目推移表 + 第二层活动录入**」两层交互，**数据模型不变**。

### 9.7 索引建议（补充 §5）

| 集合 | 索引 | 用途 |
|---|---|---|
| `activities` | `act_id`(唯一)、`customer_id`+`date`、`type` | 活动检索 / 两层抓取键 |
| `support_todos` | `todo_id`(唯一)、`act_id`、`opportunity` | 资源协助聚合 |
| `dept_feedback` | `feedback_id`(唯一)、`act_id` | 反馈流水 |

### 9.8 资源协作：销售手动多选（四类）· type 枚举与加列规则

> **变更（2026-10-11）**：作废「活动类型 → 资源」自动映射（原 `测试结果跟进→技术支持测试项目单` / `采购跟进→售后单` / `客户档案完善→不生成`）。资源协作改由**销售在第二层手动多选**触发。

第二层新增区块「**触发的资源协作（多选）**」，四类选项**逐字**取自用户口径，可多选、可一个都不选：

| 选项（逐字） | 服务端 `support_todos.type` | 建议承接部门 | 说明 |
|---|---|---|---|
| 技术支持测试项目单 | `tech_test` | 技术支持部 | — |
| 研发项目单 | `rnd_project` | 研发部 | — |
| 售后 | `after_sales` | 售后部 | — |
| 其他 | `other` | 待指派 | **必填说明**（存 `note`）；未填 ⇒ 前端拦截提交并提示 |

**提交规则（每选中一类）**：
1. **生成一条 `support_todos`**：`todo_id = TD-<act_id>-NN`，`act_id` 串联，`type` / `type_label`（中文）/ `note`（「其他」说明），`state=待承接`，`todo / owner / due` 取自第二层「待办事项」。
2. **第一层对应商机行「资源协助」组新增一列**：列名 = 选项名 + N，**N = 该资源协助组内该类已有列数 + 1**（列**不硬编码**，由所选类型动态生成，避免重名）；`col_id = rs:<列名>`，与 §8.5 `column_defs` 一致。
3. **该单元格写** `{state:'待承接', date, owner:发起销售员, act:活动编号}`。

- **不选任何一类** ⇒ 不生成 `support_todos`、**不加列**（第一层列数不变）。
- 三轨同步：本地记录 `supportTypes[]`、CloudBase `activities.support_types[]` + `support_todos.docs[]`、飞书描述**「本次触发的资源协作」**段（类型 + 说明 + 承接待办），可直接粘贴给对应部门。

---

## §10 经营仪表盘指标字典

> 对应页面 `工具/客户拉通大表_仪表盘.html`（生成器 `脚本/build_customer_thread_dashboard.py`；验收 `脚本/verify_customer_thread_dashboard.py`）。
> 数据与定稿基线同一批：`脚本/ct_data.py`（单一数据源）；DEMO 为**零外链、数据内嵌**的静态页。
> 刷新频率（上线后）：所有指标随数据管道刷新 —— 每日活动上传 → CloudBase 写入 `activities` / `support_todos` / `dept_feedback` → 聚合计入仪表盘；**DEMO 为内嵌示例值，不联网**。
> 汇率口径：国际业务部以 `US$` 计的商机规模按 **`1 USD = 7.2 CNY`（示例汇率）** 折算，与 `ct_data.USD_CNY` 一致。

### 10.1 顶部总览 KPI（5 项）

| KPI | 口径定义 | 计算公式 | 数据来源字段 | 刷新频率 |
|---|---|---|---|---|
| 商机总数 | 第一层「一行一商机」的总行数 | `COUNT(DISTINCT row_key)` | `opportunities.work_item_id`（内嵌口径 = §8.2 `wide_rows.row_key`） | 实时（随商机增删） |
| 客户数 | 去重客户数（按公司名 / 归一后客户 ID） | `COUNT(DISTINCT cust_master.work_item_id)` | `cust_master.work_item_id`（+ `name` 配 `aliases[]` 归一） | 实时 |
| 商机总额 | 全部商机金额合计（外币折算人民币） | `Σ amount`（`US$` 项 × 7.2） | `orders.amount`（ERP `单据编号`=`doc_no` 对接）/ `opportunities.expect_amount` | 每日（随订单 / 商机金额变动） |
| 卡住商机数 | 当前所处节点状态为「卡住」的商机数 | `COUNT(opps where state(stages[cur])='卡住')` | `opportunities.stages{}.state` | 每日 |
| **数据待关联率** | 客户编码 / 商机 ID 未回填（null）的行占比（阈值 > 30% → 橙框） | `COUNT(customer_id 或 opp_id 为 null) / 商机总数 × 100%` | `activities.customer_id` / `*.opp_id`（null ⇒ 标黄「待关联」） | 实时 |

> 2026-10-11 变更：原挂在「客户数」卡上的「待关联占比 > 30% 橙框」**移出**，改为独立的**第 5 张 KPI 卡「数据待关联率」**（示例数据 30.8%，橙框 `flag-amber`）；验收脚本 `kpi_count` 同步改 5。

### 10.2 11 块指标卡

| # | 指标卡 | 口径定义 | 计算公式 | 数据来源字段 | 刷新频率 |
|---|---|---|---|---|---|
| 1 | 阶段分布漏斗 | 8 个关键节点各有多少商机**已到达**（节点格 state ∈ 已完成 / 进行中 / 卡住 即视为已到达） | 对每个节点 `COUNT(opps where stages[node].state ∈ {已完成,进行中,卡住})` | `opportunities.stages{}`（节点格 state） | 随节点推进（每日） |
| 2 | 商机状态分布 | 按「**商机 × 关键节点**」单元格状态统计格子数（每商机 8 格） | `COUNT(cells)` 按 state 分组 → 未开始 / 进行中 / 已完成 / 卡住 | `opportunities.stages{}.state` | 每日 |
| 3 | 商机销售额 | 商机总额 + 按部门拆分 + 按（当前所处）阶段拆分 | 总额 `Σ amount`；按部门 `Σ_{owner∈dept}`；按阶段 `Σ_{cur(node)=stage}` | `orders.amount` / `opportunities.expect_amount`；部门/阶段取 `opportunities.owner`·`dept` + `stages{}` | 每日 |
| 4 | 客户数量 | 去重客户数 + 按部门分布 | `COUNT(DISTINCT cust)`；按部门 `COUNT(DISTINCT cust by dept)` | `cust_master.work_item_id`（+ `aliases[]`） | 实时 |
| 5 | 阶段停留时长 / 卡点 Top | 每个项目在每个节点的**停留天数** | `天数 = stages[node].end − stages[node].start`；**未收口（end 为空）按「今天」− start**；列出停最久 Top 5 | `opportunities.stages{}.start / .end`（**日期相减得天数**） | 每日 |
| 6 | 阶段转化率 + 平均推进周期 | 进入某节点 → 进入下一节点的比例与平均天数 | 转化率 = `下游到达数 / 上游到达数`；平均周期 = `AVG(下游.start − 上游.start)`（对两节点皆有 start 的商机取均值） | `opportunities.stages{}`（到达数 + 日期差） | 每日 |
| 7 | 资源消耗与闭环率 | 技术支持测试项目单 / 研发项目单 / 售后 / 其他 各触发次数 + 已闭环 / 未闭环 | 触发次数 = 按 `lane` 统计事件数（≈ `support_todos` 条数）；**闭环率分母 = 商机总数**，分子 = 含「闭环」事件的商机数（`已闭环商机 / 商机总数`） | 触发 → `support_todos`（`type`）；闭环 → `thread_view.event_type = todo_close` / 事件 `k='闭环'` | 每日 |
| 8 | 沉默商机预警 | 最后跟进距今 > 30 / 60 / 90 天的商机数与金额（三档） | **沉默天数 = 今天 − MAX(该商机的事件日期)**；分档 `30<d≤60` / `60<d≤90` / `d>90`，各档汇总商机数与金额 | 最后跟进 = 该商机最新事件日期 → `activities.date` / `thread_view.ts`（**按事件时间字段相减**） | 每日 |
| 9 | 销售员维度 | 人均商机数 / 金额 / 卡住数（按销售员聚合） | 按 `owner` 分组：`COUNT(opps)`、`Σ amount`、`COUNT(state='卡住')` | `opportunities.owner` / `wide_rows.sales`（行级隔离口径同工作台） | 每日 |
| 10 | 商机规模分档 | 10 万以下 / 10–50 万 / 50 万以上 三档商机数与金额 | 按金额分档：`<10万` / `10–50万` / `>50万`，各档 `COUNT` + `Σ` | `orders.amount` / `opportunities.expect_amount` | 每日 |
| 11 | 数据治理 · 待关联 ID 占比 | 客户编码 / 商机 ID 未回填（null）的行占比 | `待关联占比 = COUNT(customer_id 或 opp_id 为 null) / 商机总数` | `activities.customer_id` / `*.opp_id`（null ⇒ 前端标黄「待关联」，append-only 允许 null） | 实时 |

### 10.3 关键口径补充（回答“按什么字段算”）

- **「阶段停留天数」按什么日期相减**：取该商机在该节点的 `stages[node].start` 与 `stages[node].end`（均为 `YYYY-MM-DD`）；**未收口的节点（`end` 为空，即进行中 / 卡住）以「今天」代替 end** 计算。
- **「沉默」按什么时间字段**：取该商机**最新一条事件的时间**（`activities.date` 或 `thread_view.ts`），与「今天」相减得天；三档阈值为 >30 / >60 / >90 天。
- **闭环率的分母**：**商机总数**（= 第一层行数）；分子为**至少含一条「闭环」事件的商机数**（对应 `thread_view.event_type = todo_close`，或 §3.2 状态机中商机 `can_close=true` 且 `status=闭环`）。
- **四态色**：已完成=绿 `#16A34A` / 进行中=蓝 `#1852B6` / 卡住=红 `#C0392B` / 未开始=灰 `#B9C2CE`（与基线轨道、状态标签一致）。
- **外币折算**：`US$` 商机规模按 `× 7.2` 折算人民币后计入总额与分档（示例汇率；上线后可替换为当日汇率）。

### 10.4 表现形态与交付物（2026-10-11 更新）

- 定稿基线 `工具/客户拉通大表_demo.html`（生成器 `脚本/build_customer_thread_demo.py`）：**默认 = 第一层「形态1 里程碑轨道」**（一行一商机、8 节点圆点串、4 部门分组、行可点）；**点行 → 展开区 = 第二层「形态B 资源泳道矩阵」**（行=资源类型 5 条泳道、列=月份、事件向右延展）。保留「表格视图」切换（回三层表头宽表）及全部原功能（三层表头 / 资源协作手动多选 / 6 维校验 / 三轨落盘 / 待关联黄标 / 加列加行）。
- 备份链：`客户拉通大表_demo_v1_时间线版.html`（v1）→ `_v2_单层宽表.html`（v2）→ `_v3_自动映射版.html`（v3）→ `_v4_表格版.html`（v4，本次改动前的两层表格式存档）。
- 形态对比页（保留）：`工具/客户拉通大表_表现形式demo.html`（生成器 `脚本/build_customer_thread_views.py`）。
- 新增交付物：`工具/客户拉通大表_仪表盘.html`（本节 11 指标卡 + 5 KPI；**2026-10-11 升级为「报告版」**，版式规范见 §11.4；**同日再升级为「可拖拽/可缩放/图形真自适应/布局持久化」交互版**，前端规范见 §12.1）。
- 数据单一来源：`脚本/ct_data.py`（基线页与仪表盘共用，保证“同一批示例数据”）。

---

## §11 周会决议（三岗各填一栏）与经营仪表盘报告版版式规范

> 本节为 2026-10-11 两项改善的规格：
> ① 在**第二层（活动录入表）**新增列组「**周会决议**」，三岗各填一栏，提交后落盘待办库 `support_todos`；
> ② 经营仪表盘升级为**面向总经理的报告版**（栅格 / 字号 / 颜色语义 / 版式顺序）。
> 对应 DEMO：`工具/客户拉通大表_demo.html`（生成器 `脚本/build_customer_thread_demo.py`；验收 `脚本/verify_customer_thread_demo.py`）。
> 对应报告版：`工具/客户拉通大表_仪表盘.html`（生成器 `脚本/build_customer_thread_dashboard.py`；验收 `脚本/verify_customer_thread_dashboard.py`）。

### 11.1 周会决议字段结构（`weekly_resolutions`）

第二层新增区块「**周会决议（三岗各填一栏 · 销售 / 技术支持 / 研发）**」。三栏**顺序固定**：`sales`（销售）→ `tech`（技术支持）→ `rnd`（研发）。每栏是一个**本岗填写位**，填写格式：

```
决议事项 ｜ 负责人 ｜ 时间节点          （一行一条，可多行）
```

字段结构（活动文档内的内嵌对象，按岗位分桶）：

```json
"weekly_resolutions": {
  "sales": [ { "role_key":"sales", "role":"销售",
               "item":"补做C组配方并复测", "owner":"王工", "due":"2026-10-20",
               "opportunity":"多重呼吸道病原体冻干体系开发",
               "act_id":"WR-20261011-01", "source":"周会决议" } ],
  "tech":  [ { "role_key":"tech", "role":"技术支持", "item":"…", "owner":"…", "due":"…", "opportunity":"…" } ],
  "rnd":   [ { "role_key":"rnd",  "role":"研发",     "item":"…", "owner":"…", "due":"…", "opportunity":"…" } ]
}
```

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `role_key` | string | 必 | `sales` / `tech` / `rnd`（固定顺序） |
| `role` | string | 必 | 岗位中文标签：销售 / 技术支持 / 研发 |
| `item` | string | 必 | 决议事项（一行一条） |
| `owner` | string | 可 | 负责人；缺省填 `（待定）` |
| `due` | string | 可 | 时间节点，优先识别 `YYYY-MM-DD`；缺省填 `（待定）` |
| `opportunity` | string | 可 | 关联商机名称；未选择填 `（未选择）` |
| `act_id` | string | 必 | 周会决议批次号 `WR-YYYYMMDD-NN` |
| `source` | string | 必 | 固定 `周会决议` |

- **解析**：每行按 `｜ / | / ； / ;` 分隔为「事项 ｜ 负责人 ｜ 时间节点」；行内出现 `YYYY-MM-DD` 则作为时间节点。
- **一条决议 ⇒ 一条待办**（一栏多行 ⇒ 多条待办）。

### 11.2 落盘映射（`weekly_resolutions` → `support_todos`）

提交周会决议后，**每条决议生成一条 `support_todos`**（集合 D，与 §9.8 的资源协作待办同表）：

| `support_todos` 字段 | 取值 | 来源 |
|---|---|---|
| `todo_id` | `WR-YYYYMMDD-NN-<role_key>-NN` | **主键 / 去重键** |
| `act_id` | `WR-YYYYMMDD-NN` | 周会决议批次号 |
| `role` / `role_key` | 销售/技术支持/研发 ｜ sales/tech/rnd | 岗位 |
| `owner` | 负责人 | 决议行 |
| `due` | 时间节点 | 决议行 |
| `opportunity` | 关联商机 | 该栏「关联商机」选择 |
| `source` | `周会决议` | 来源标识 |
| `item` | 决议事项 | 决议行 |
| `state` | `待承接` | 初始状态 |

- **去重键 = `todo_id`**（形态 `WR-<批次>-<role_key>-<序号>`）；同一 `(act_id, role_key, 序号)` 唯一，重复提交同批次不产生重复文档（append-only，幂等由 `todo_id` 保证）。
- 资源协作待办（§9.8）与周会决议待办共用 `support_todos`，靠 `source` 区分（`资源协作` / `周会决议`），`todo_id` 前缀区分（`TD-` / `WR-`）。
- 页面在提交后展示**结果面板「本次落盘 N 条待办」**（与三轨落盘面板风格一致）：列出每条待办（岗位/负责人/时间节点/关联商机/待办编号）+ `support_todos` 写入 JSON + **可一键复制的飞书描述**。

### 11.3 权限（三岗位各写各栏 · 管理员不受限）

| 角色 | 可编辑范围 | 说明 |
|---|---|---|
| 销售（`sales`） | 仅「销售」栏 | 点「以本岗身份填写」后本栏解锁（textarea 可编辑），其余两栏保持锁定 |
| 技术支持（`tech`） | 仅「技术支持」栏 | 同上 |
| 研发（`rnd`） | 仅「研发」栏 | 同上 |
| 管理员 / 主管 | **三栏不受限** | 勾选「管理员模式」→ 三栏全部解锁（取消则收回） |

- **默认锁定**：三栏 textarea 初始 `disabled`，状态角标 `🔒 本岗未解锁`；点「以本岗身份填写」→ `🔓 本岗填写中`（仅解锁本栏，模拟「按角色分字段」）。
- **未填栏不得静默空白**：表单三栏与表格列格在未提交时一律显示「**本岗未提交**」；已提交显示「已提交 N 条」。
- **上线口径**：前端仅作权限演示；实际写权限按 §4 权限矩阵 —— 岗位角色（`sales`/`tech`/`rnd`）只能写本岗栏，`manager`/`admin` 不受限（字段级写权限，按 `role_key` 分栏）。
- 表格侧：`#l2Table` 新增组「**周会决议（三岗各填一栏）**」，三列 = 销售 / 技术支持 / 研发；按「关联商机」匹配到该行的决议文本，未匹配显示「本岗未提交」。

### 11.4 经营仪表盘「报告版」版式规范

| 维度 | 规范 |
|---|---|
| 版式 | **单页 1440px 宽**（`--wrap:1440px`）；背景纯白（`--bg:#ffffff`）、**无阴影干扰**；可投屏也可直接打印（`@media print` 白底、卡片不跨页截断） |
| 统一栅格 | 卡宽 / 间距 / 内边距同一套变量：`--gap:14px`、`--pad:16px`、`--rad:12px`、`--lbl:96px`（条形图标签列宽）；三列 `grid`（`grid-template-columns:repeat(3,minmax(0,1fr))`），**同行卡片等高**（`align-items:stretch`；风险区三卡高度差 ≤ 6px）；图表左对齐基线一致（全卡同一 `.bar` 栅格） |
| 字号（**只留 4 级**） | `--fs-num:32px`（大数字 · 仅 4 张 KPI）/ `--fs-title:15px`（卡标题 · 子指标 · 环形中心）/ `--fs-body:12.5px`（正文）/ `--fs-note:10.5px`（注释 · 数据来源）；页面计算字号 **≤ 4 种**（含内嵌 SVG 文本） |
| 颜色语义 | 品牌蓝 `#1852B6`（主）/ `#0d3a85`（深）；状态四色：已完成=绿 `#16A34A` / 进行中=蓝 `#1852B6` / 卡住=红 `#C0392B` / 未开始=灰 `#B9C2CE`；预警橙 `#F59E0B`（`--warn-bd`）；**阈值框**：卡住商机数 > 0 → 红框（`flag-red`）、**第 5 张「数据待关联率」> 30% → 橙框（`flag-amber`，已从「客户数」卡移出）** |
| 版式顺序 | ① **结论摘要**（一句话，由数据自动生成）→ ② **5 张 KPI 大卡**（商机总数/客户数/商机总额/卡住商机数/数据待关联率，数字放大加粗、带阈值色）→ ③ **风险预警区**（单独成块 · 警示色 · 提前）：卡点 Top / 沉默商机三档 / 资源未闭环 → ④ **业务明细 · 11 块指标卡** |
| 弱化技术细节 | 每卡的「数据来源字段」从正文改为**卡片右下角小字**（`--fs-note`、右对齐、低对比），保留可追溯但不抢眼 |

- **结论摘要口径**（示例）：`在跟商机 {商机总数} 个，总额 ¥{商机总额}万；{卡住数} 个卡住（{停留最久客户}停留 {最长天数} 天最长），{沉默>90天数} 个沉默超 90 天；资源闭环率 {闭环率}%。`
- **不新增指标**：报告版只对 §10 已有的 4 KPI + 11 指标卡做**重排 / 对齐 / 强化层级**；所有数字与 `脚本/ct_data.py` 一致（验收脚本从 `ct_data.py` 独立复算 4 个关键数字并逐一对齐）。
- 备份链：仪表盘 `客户拉通大表_仪表盘_v1.html`（报告版改动前存档）；基线 `客户拉通大表_demo_v5.html`（周会决议改动前存档）。



---

## §12 仪表盘交互（拖拽 / 缩放 / 真自适应 / 布局持久化）与第二层活动录入精简（模板化描述 + 段级校验）

> 本节为 2026-10-11 两项改善的规格；**§1–§11 不动**，本节只描述新增/变更的前端行为与字段口径。
> ① 对应报告版 `工具/客户拉通大表_仪表盘.html`（生成器 `脚本/build_customer_thread_dashboard.py`；验收 `脚本/verify_customer_thread_dashboard.py`）。
> ② 对应基线 `工具/客户拉通大表_demo.html`（生成器 `脚本/build_customer_thread_demo.py`；验收 `脚本/verify_customer_thread_demo.py`）。
> 备份链：仪表盘 `_v1.html`（报告版前）→ `_v2.html`（本次交互前）；基线 `_v1`…`_v5` → `_v6.html`（本次精简前）。

### 12.1 仪表盘前端规范（可拖拽 / 可缩放 / 图形真自适应 / 布局持久化 / 打印回默认）

**卡片对象与默认顺序**
- `#grid` 内 = 11 张 `.card`（`data-card="1".."11"`）+ 2 个区块标题 `.sec`（`sec-risk` / `sec-detail`）。
- 默认子节点顺序 `DEF_CHILD_ORDER = [sec:risk, c5, c8, c7, sec:detail, c3, c6, c9, c1, c4, c11, c2, c10]`（**风险区前置**；`.sec` 横跨整行）。
- 卡片栅格 `grid-template-columns:repeat(3,minmax(0,1fr))` + `grid-auto-flow:row dense`（换位/跨列后自动补位）。

**① 拖拽换位（原生 HTML5 drag&drop，零外部库 · 范围 = 同一区块内）**
- 每张 `.card` `draggable`；`#grid` 上做事件委托：`dragstart → dragover → drop → dragend`。
- **拖拽范围 = 同一区块内**（2026-10-11 收紧）：卡片所属区块 = 该卡之前的最近一个 `.sec` 兄弟（`secOf()`）。`dragover` 时若目标卡与被拖卡**不在同一区块** → **拒绝放置**：不重排 DOM（= 卡片回弹原位）、`dataTransfer.dropEffect='none'`（禁止光标）、被拖卡加 `.dragging-cross`（红色虚线描边），工具条 `#dragHint` 变红提示「跨区放置已被拒绝」；落点 **在同一区块内** 才按目标卡**中线**判定，把被拖卡 `insertBefore` 到目标卡前/后 —— **DOM 真实重排**（不是视觉位移），其余卡自动让位。
- **区块顺序与区块标题绝对固定**：`.sec` 永不参与换位（所有插入均相对 `.card` 进行）；跨区拖拽被拒 ⇒ 任一 `.sec` 的**文本与位置在操作前后均不变**（验收探针 `__secInfo()` 逐次比对）。
- 工具条常驻提示 `#dragHint`：「同区块内拖拽可换位（区块顺序固定）」（`flex-basis:100%` 独占一行 + `nowrap`，文案变化不改变工具条高度 ⇒ 不引起区块标题位移）。
- 拖拽期间 `.dragging` 半透明描边；drop 后 `syncOrder()` 记录新顺序并持久化（落点结果 `window.__lastDrop = 'accepted' | 'rejected'`）。
- 备份链（本次）：`工具/客户拉通大表_仪表盘_v3.html` = 收紧拖拽范围前的存档。

**② 缩放四档（右下角 `.handle`）**
- 档位 `tier ∈ {0 小, 1 中, 2 大, 3 宽}` → `grid-column: span {1,1,2,3}`。
- 拖动手柄：按累计 `dx / (colW×0.55)` 换档（`colW = grid.clientWidth/3`）；**单击手柄 = 循环四档**（触屏/无拖拽环境兜底）。
- tier0（小）额外加 `.t-min`（`.body{max-height:130px;overflow:hidden}`）。

**③ 图形真自适应（按测量宽度重绘，不是 CSS 缩放）**
- `redrawAll()`：对每张卡量测 `.body` 宽度 `w` 与档位 → 调 `cardBody(i, w, compact)` 重画卡体。
- `compact = (tier===0) || (w < 300)`：
  - **卡1 阶段分布漏斗** = 内联 SVG，`<svg width=w viewBox="0 0 w H">`，条宽 `= cnt/max × (w − 标签列 − 尾距)` —— **卡变宽，条宽跟着变**（验收从 DOM 读 `svg[width]` 前后值）；
  - **卡2 / 卡7 / 卡11 环形 → 紧凑堆叠条**（`.sbar` + `.sbar-lgd`），隐藏次要图例；**卡6** 紧凑时隐藏 `.barnote`；条形图标签列 `--lbl` 由 96px → 60px（`.card.compact`）；
  - 图表可用宽 < 70px → 卡体渲染 **「内容溢出」提示** `.ovf`（不再叠字）；tier0 溢出时卡内显示 `.overflow-hint`（`.has-overflow`）。
- 断点：`<=1180px` 两列、`<=760px` 单列（原有响应式保留）。

**④ 布局持久化（localStorage · 每人一份）**
- 键名 `ct_dash_layout__<用户标识>`（占位 `demo-user`，读取 `window.__DASH_USER`）；值 `{v:1, order:[...], tier:{1..11}}`。
- 写入时机 = 拖拽 `drop` / 缩放 `mouseup`；`loadLayout()` 校验 `order` 长度 = 13 后应用；刷新即恢复顺序与档位。
- 顶部两个按钮：**「恢复默认布局」**（复位顺序与档位 + 清除本地键）、**「保存为我的布局」**（显式落本地并提示）。
- 页面明示：**「布局存本地（DEMO 为本地存储，仅本机浏览器生效）」**。

**⑤ 打印（忽略拖拽布局 · 回默认顺序）**
- `@media print`：隐藏 `.dash-bar` / `.handle` / `.overflow-hint`，`grid-auto-flow:row`（回默认流）。
- `beforeprint`/`window.__preparePrint()`：把 DOM 子节点顺序**复位为 `DEF_CHILD_ORDER`**（打印用默认顺序，不呈现用户拖拽后的次序）；`afterprint`/`__endPrint()` 还原用户布局。

**⑥ KPI（5 张）**
- 4 张原有 + 新增第 5 张 **「数据待关联率」**（> 30% → 橙框 `flag-amber`）；**原挂「客户数」的待关联橙框已移出**。

**验收探针（页面暴露）**：`__cardOrder()` / `__childOrder()` / `__cardW(i)` / `__svgW(i)` / `__cardTier(i)` / `__resetLayout()` / `__saveLayout()` / `__layoutKey` / `__preparePrint()` / `__endPrint()` / `__handleDisplay()` / `__lsValue()` / **`__secInfo()`（区块标题文本+坐标快照）** / **`__secOfCard(i)`（卡片所属区块）** / **`__dragHintText()` / `__dragHintDeny()`**。

### 12.2 第二层活动录入：字段删除 / 合并 / 只读继承（精简后 = 4 块 + 1 独立模块）

| 旧录入位 | 处置 | 新位置 / 说明 |
|---|---|---|
| 竞品信息（`#f_comp`） | **删除** | 并入活动描述模板（【宝锐方案】· 竞对情况 / 【竞对报价与差距】） |
| 货号（`#f_sku`） | **删除** | 并入描述模板（【宝锐方案】· 货号+名称） |
| 关联订单 / 货号 独立列 | **合并为单字段** | ③ 关联订单/测试单（`#f_order`，可选）；未填 → 表格标「待关联」 |
| 拜访对象 / 联系人 | 缩为小字段 | 顶部「继承条」普通输入（`#f_contact`） |
| 商机规模 / 部门 / 客户编码 | **只读继承** | `#f_scale_ro` / `#f_dept_ro` / `#f_cid_ro`（从第一层带出，不占录入位） |
| 销售员 | 只读继承 | `#f_sales_ro` |
| 技术支持 | 只读继承 + **待办默认协同人** | `#f_tech_ro`（新增待办行负责人默认 = 技术支持） |
| 6 维结构化（`#st1..#st6` / `#sixdim`） | **删除** | 改为 ② 活动描述 `#f_desc`（模板化多行 + 「插入模板」） |
| 待办事项 单条（`#f_todo/#f_owner/#f_due`） | **改为多条** | ④ 待办事项 `#todoRows`（事项 / 负责人 / 时间节点 ×N，「＋ 添加一条待办」） |
| 资源协作 多选（`#resPick`） | **保留** | ④ 内子块「资源协作（多选）」（口径同 §9.8，不变） |
| 周会决议 三岗 | **保留不变** | 见 §11 |

**精简后录入区** = ① 活动类型（`#f_type`）+ ② 活动描述（`#f_desc`）+ ③ 关联订单/测试单（`#f_order`）+ ④ 待办事项（`#todoRows`）**+ 独立模块 周会决议**。顶置「关联对象与继承字段（只读继承自第一层 · 不占录入位）」条。
L2 表格列（`#l2Table`）：活动编号 / 客户名称 / 商机名称 / 活动类型 / 关联订单·测试单 / **活动描述（模板化）** / 事项·负责人·时间节点 / 周会决议三列；**已删除 竞品信息 / 货号 列**。

### 12.3 模板内容与段级校验规则

**描述模板（按活动类型切换，`#f_type → #f_desc`；右上角「插入模板」可重插）**，原文存 `脚本/ct_data.py` 的 `DESC_TEMPLATES`：

- **测试结果跟进**（逐字照用户口径）：
  ```
  【客户需求】
  · 应用场景：           · 技术平台：qPCR/测序/冻干/其他
  · 项目状态：立项/研发/注册/量产   · 时间节点：
  · 参数要求：（量化，如 CV≤5%、Ct 漂移≤0.5）
  · 竞对情况：品牌+型号+量化对比（无则写"无"）
  【宝锐方案】
  · 货号+名称：          · 关联订单/测试单编号：
  · 测试条件：体系/仪器/程序
  【测试结果】（逐项：指标 → 实测值 → 判定）
  · 1.             · 2.
  · 结论：通过 / 超阈（超阈必写偏差方向与幅度）
  【问题现象】含客户原话（加引号）
  【客户反馈 / 下一步诉求】
  【待办事项】做什么 / 怎么做 / 做到什么程度 / 关联资源 / 负责人 / 日期
  ```
- **采购跟进**：采购阶段 / 预算与数量 / 决策链与关键人 / 竞对报价与差距 / 下一步动作与时间。
- **客户档案完善**：本次补齐了哪些字段（联系人 / 组织架构 / 设备 / 项目）。

**段级校验（改口径：按「段」校验，不再按 6 个独立字段）**
- 触发：活动类型 = `测试结果跟进`；必填段 `REQ_SEGS = [客户需求, 宝锐方案, 测试结果, 问题现象, 客户反馈, 待办事项]`（与 `ct_data.REQ_SEGS` 同源）。
- 解析：`parseDesc()` 按 `【…】` 头切段，段名 contains 匹配（兼容带后缀的段名，如【测试结果】（逐项…））。
- 缺段 → `#formErr` 提示「提交被拦截：…缺少必填段 —— 【测试结果】」、`#f_desc` 加 `.bad` 高亮、**不落盘**；补全后可提交。
- 另两类活动无强制段（按模板填写即可）。实时状态由 `#segStat` 展示（● 已填 / ○ 缺，`window.__segMissing()`）。

### 12.4 与 CloudBase 集合的映射变更（`activities` 写入载荷）

**删除字段**：`competitor`、`sku`、`structured_6d`（`d1..d6`）。
**新增字段**：`desc_template`（= 活动类型，模板键）、`desc_content`（= 用户填写后的活动描述全文）。
**保留**：`node_push`、`support_types`、`order_no`（单字段，可空 → `unlinked` 标黄「待关联」）。

```json
{ "collection": "activities",
  "doc": { "id":"FS-<actId>", "act_id":"<actId>", "submitted_by":"<销售员>", "sales":"<销售员>",
           "dept":"<部门>", "customer":"<客户名>", "customer_id":"<客户编码|null>", "opportunity":"<商机名>",
           "desc_template":"测试结果跟进", "desc_content":"【客户需求】…（填写后全文）",
           "date":"2026-10-11", "contact":"张工", "order_no":"SO-…|null", "type":"测试结果跟进",
           "node_push":"临床资料申报", "support_types":["研发项目单"], "created_at":"…" } }
```

- 待办事项：由 `m.todos[]`（**多条**：事项/负责人/时间节点）承载；资源协作仍按 §9.8 生成 `support_todos.docs[]`（`type` 枚举 `tech_test/rnd_project/after_sales/other`）。
- 飞书描述：改为「活动描述（全文）」段 + 「待办事项（可多条）」段；关联订单/测试单单列。

> 验收脚本同步更新：仪表盘 `kpi_count == 5`（+ 拖拽/缩放/localStorage/恢复默认/打印/自适应 断言）；基线断言「录入区只剩 4 块、旧栏目已删除、模板自动填入、段级校验拦截与补全、周会决议与轨道/泳道/表格视图未破、v1–v6 备份」。

---

## §13 商机性质标记（机会性 / 持续性）与「客诉→替换→采购恢复」周期

**增补日期**：2026-10-11 ｜ **依据**：用户实际发生过的两个案例——
① 天隆集团 `PFM4211`（HPV 海外）：2025 机会性商机 ¥109.8 万，2026 **未延续**（客户市场开拓原因）→ 典型「机会性商机无预警断尾」；
② 天隆集团 `M5244-ZJ4`：**稳定性客诉** → 2026-07 起采购受影响 → 2026-08 月底用 `MD2104-003006` 完成替换 → 直接影响 Q3 采购，实测**替换周期约 2 个月（≈60 天）**。
用户口径原话：**「让下季度能自动预警」**。本 §13 把这两条能力写成**可执行规则**（不是形容词），并在基线 DEMO 里做成**可见、可筛、可预警**的元素。

### 13.1 商机性质标记（机会性 / 持续性）

**A. 字段增补（`opportunities` 表 · 4 列）**

| 字段名 | 类型 | 必填 | 来源 | 示例值 |
|---|---|---|---|---|
| `nature` | enum(`opportunistic` \| `continuous` \| `unknown`) | 必 | **新建**（判定结果；`opportunistic`=机会性 / `continuous`=持续性 / `unknown`=待判） | `"opportunistic"` |
| `nature_basis` | string | 可 | **新建**（判定依据备注：命中哪一条 / 数据缺口） | `"单一替代项目、预计金额占客户年采购 >40%；无历史复购、无框架协议"` |
| `nature_confirmed_by` | string | 可 | **新建**（人工复核人；系统判定填「系统判定」） | `"系统判定"` |
| `nature_confirmed_at` | string | 可 | **新建**（判定/复核日期） | `"2026-10-11"` |

**B. 判定规则（可执行 · 输入 = 客户近 12 个月订单 + 商机框架字段 + 金额占比）**

满足**任一**条件 → `opportunistic`（机会性）：
1. **无历史复购**：该客户近 12 个月该品类订单 **≤1 单**；
2. **无框架 / 年度协议字段**（`opportunities.framework` 缺失或为空）；
3. **预计金额占客户年采购 >40%**，且该金额**集中在单一项目**（非多项分散）。

三条**均不满足** → `continuous`（持续性）。三项数据（复购 / 框架 / 金额占比）**任一缺失到无法判定** → `unknown`（待判，转人工）。

```
nature = 'unknown'
if 复购数(该客户, 该品类, 近12月) <= 1 or 无框架协议 or 金额占比>40%且单项目集中:
    nature = 'opportunistic'
elif 复购数 > 1 and 有框架协议 and 金额占比 <= 40%:
    nature = 'continuous'
# 阈值常量：复购阈值 = 1 单；金额占比阈值 = 40%；窗口 = 近 12 个月
```

**C. 预警规则（三条 · 默认阈值）**

| # | 条件 | 等级 | 文案 / 动作 |
|---|---|---|---|
| ① | 机会性商机**连续 N 个月（默认 3）无活动 / 无订单** | 黄 | 「机会性商机**潜在断尾风险**」→ 提醒补跟进 |
| ② | 上一年度机会性商机在本年度**已过预计成交窗口 60 天仍未成交** | 红 | 「上一周期机会性商机未延续」→ 复盘归因 |
| ③ | 机会性商机占**本季度预计收入 >30%** | 组合风险提示 | 「机会性收入占比过高，组合抗风险弱」 |

**D. 展示**

- **第一层商机行带 chip**：机会性 = 橙 / 持续性 = 绿 / 待判 = 灰；工具条提供 `全部 / 机会性 / 持续性 / 待判` **筛选**（点选即过滤第一层行，可复位）。
- 经营仪表盘**风险区**加一块 **「机会性商机流失预警」**：逐条列出预警项（客户 / 商机 / 金额 / 理由 / 等级），**可点击展开依据字段**；底部给规则①/②/③说明与**组合风险占比**（含阈值对比）。

**E. 落盘映射（`opportunities` 写入载荷 · 增补片段）**

```json
{ "collection": "opportunities",
  "doc": { "work_item_id": 7038571941, "name": "HPV 海外检测原料供给（PFM4211）", "customer_name": "天隆集团",
           "nature": "opportunistic", "nature_basis": "无历史复购 + 无框架协议 + 金额占比 >40%",
           "nature_confirmed_by": "系统判定", "nature_confirmed_at": "2026-10-11" } }
```

### 13.2 客诉 → 替换 → 采购恢复 周期

**A. 实体 `replacement_cases`（新建集合 · 把一次「客诉→替换」事件建成一条）**

| 字段名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| `case_id` | string | 必 | 案例编号 | `"RC-202607-001"` |
| `customer` | string | 必 | 客户（集团/抬头） | `"天隆集团（西安天隆科技有限公司）"` |
| `ticket` | string | 可 | 客诉单 ID（关联售后单） | `"AS-20260701-012"` |
| `orig_sku` | string | 必 | 原货号 | `"M5244-ZJ4"` |
| `complaint_date` | string | 必 | 客诉发生日 | `"2026-07-01"` |
| `affected_month` | string | 可 | 受影响起始月（采购受影响） | `"2026-07"` |
| `repl_sku` | string | 可 | 替品货号 | `"MD2104-003006"` |
| `replace_done_date` | string \| null | 可 | 替换完成日 | `"2026-08-30"` |
| `purchase_recover_date` | string \| null | 可 | 采购恢复确认日 | `"2026-09-15"` |
| `cycle_days` | number | 可 | 替换周期天数（派生，见下） | `60` |
| `stage` | string | 可 | 当前阶段（状态机取值） | `"采购恢复确认"` |
| `status` | string | 必 | 状态（进行中 / 闭环） | `"闭环（采购已恢复）"` |
| `stages[]` | array | 可 | 阶段明细：`[{name, in, out}]`，每阶段记**入/出时间** | 见下 |

**B. 阶段状态机（6 阶段 · 逐阶段记入/出时间）**

```
客诉受理 → 定位中 → 替品确认 → 送样验证 → 替换完成 → 采购恢复确认
   in/out     in/out    in/out      in/out      in/out        in/out
```

- 未闭环案例：末阶段只有 `in`、无 `out` → 前端按「今天」计算**该阶段已耗时长**，作为告警判据。
- 落地建议（`support_todos` 联动）：进入「替换完成」阶段 → 生成售后进度跟进待办；进入「采购恢复确认」→ 生成销售回访待办。

**C. 核心指标（给公式）**

| 指标 | 公式 | 案例基线 |
|---|---|---|
| **替换周期天数** | `替换完成日 − 客诉发生日` | 天隆 `M5244-ZJ4` 实测 **≈60 天**（2026-07-01 → 2026-08-30 = 60） |
| **采购恢复周期天数** | `采购恢复确认日 − 客诉发生日` | 天隆 76 天（2026-07-01 → 2026-09-15） |
| **响应及时率** | `定位中耗时 ≤ 7 天的案例数 / 总案例数` | 基线 `2 / 3 = 66.7%` |

**D. 预警规则**

1. 任一案例**停留某阶段 > 30 天 → 黄**；**> 60 天（超过替换周期基线）→ 红**。（判据：该案例当前所处阶段 `今天 − in` 的天数）
2. 客户侧**连续 2 个月采购额同比降 >20%**，且存在**未闭环**的替换案例 → **红**。

**E. 展示**

- 第一层**风险区**加一张 **「客诉 → 替换 → 采购恢复周期」卡**：逐案**列表 + 6 段阶段进度条 + 已耗时 + 告警色**（绿=已闭环 / 黄 / 红），**可点击展开依据字段与阶段时间**；卡底给三指标公式与「响应及时率」实测值。
- 真实案例（示例行）：天隆 `M5244-ZJ4` → `MD2104-003006`，客诉 2026-07-01、替换完成 2026-08-30、**替换周期 60 天**、采购恢复 2026-09-15（76 天），状态：闭环（采购已恢复）。

**F. 落地映射（`replacement_cases` 写入载荷 · 天隆案例）**

```json
{ "collection": "replacement_cases",
  "doc": { "case_id": "RC-202607-001", "customer": "天隆集团（西安天隆科技有限公司）",
           "ticket": "AS-20260701-012", "orig_sku": "M5244-ZJ4", "repl_sku": "MD2104-003006",
           "complaint_date": "2026-07-01", "affected_month": "2026-07", "replace_done_date": "2026-08-30",
           "purchase_recover_date": "2026-09-15", "cycle_days": 60, "stage": "采购恢复确认",
           "status": "闭环（采购已恢复）",
           "stages": [ {"name":"客诉受理","in":"2026-07-01","out":"2026-07-03"},
                       {"name":"定位中","in":"2026-07-03","out":"2026-07-08"},
                       {"name":"替品确认","in":"2026-07-08","out":"2026-07-15"},
                       {"name":"送样验证","in":"2026-07-15","out":"2026-08-30"},
                       {"name":"替换完成","in":"2026-08-30","out":"2026-09-15"},
                       {"name":"采购恢复确认","in":"2026-09-15","out":"2026-09-15"} ] } }
```

### 13.3 验收口径（与基线 DEMO 对齐）

- **基线断言（原文 49 项）全绿**：资源协作多选 → 第一层资源协助列 +1；第二层「测试结果跟进」6 段级校验（缺段被拦 / 补全可提交）；周会决议三岗各填一栏 → 落盘待办库 +3；里程碑轨道 13 行 + 资源泳道矩阵 5 泳道；新增节点列 / 资源协助列 / 商机「列行各 +1」。
- **§13 新增断言（9 项）**：筛选按钮 4 个；商机性质 chip ≥4 且含机会性 + 持续性两类；筛选生效（机会性 → 4 行且全为机会性）与可复位；风险区两块存在；流失预警含红/黄等级；替换卡 3 案 × 6 阶段 = 18 段进度条；天隆案例卡文本含 `M5244-ZJ4` / `MD2104-003006` / `60`；预警项可点击展开依据字段。
- **外部请求 = 0、无 console error**；**v1–v7 备份均存在**；`工具/index.html`、`工具/app.html`、`工具/客户拉通大表_仪表盘.html`、`工具/天隆_2025vs2026_趋势看板.html` **mtime 未变**。
- DEMO 关键选择器：`#natfBar .natf`（筛选）、`.natchip` / `.natchip-svg`（商机性质 chip）、`#cardOppLoss`（机会性商机流失预警）、`#cardReplace`（客诉→替换→采购恢复周期）。

---

## §14 v9「打表闭环」层（2026-10-30）

> **§1–§13 不动。** 本节是打表流程优化版 DEMO 的规格与口径。
> 对应产物：`工具/客户拉通大表_demo_v9_打表闭环.html`
> 生成器 `脚本/build_customer_thread_v9.py`｜模板 `脚本/_ct_v9_tpl.html`｜数据层 `脚本/ct_v9_data.py`｜验收 `脚本/verify_customer_thread_v9.py`（64 项）
> **v8 基线 `工具/客户拉通大表_demo.html` 未改动。**
>
> 与 §9 的关系：§9 定义两层结构与三轨落盘；§14 补三件事 ——
> ① **活动类型收口为 7 类模板 + 按类必填维度**（§9 的 `structured_6d` 固定 6 维改为按类型动态维度集）
> ② **第一层新增 4 组列**（客诉 / 竞对 / 客户公司经营 / 年度目标）
> ③ **总览页三视角**（周会 / 月会 / 年度目标，同一份数据换一组指标卡）

### 14.1 活动类型收口：7 类模板 + 动态维度集

真实活动流水实测 `type` 字段出现 **2,345 种写法**（最大一类「活动跟进」4,053 条，其余全是长尾）。
本版用**两段判定**（`type` 字段优先、`text` 描述兜底）收敛到 7 类；每类配自己的维度集，**维度是数据不是 schema**（同 `column_defs` 思路）。

| 类 | 模板名 | 维度数（必填） | 实测命中 | 判别关键词（type 优先 → text 兜底） |
|---|---|---|---|---|
| `routine` | 常规跟进 | 3（1） | 5,601 / 74.5% | 回款·催款·维护·拜访·客情·活动跟进 |
| `demand` | 需求挖掘 | 6（4） | 1,004 / 13.3% | 需求·商机·挖掘·项目跟进 |
| `test` | 测试结果跟进 | 6（5） | 684 / 9.1% | 测试·试用·送样·样品 |
| `milestone` | 项目里程碑推进 | 6（4） | 93 / 1.2% | 中试·临床·注册·转产·获证·立项·里程碑·节点 |
| `complaint` | 客诉处理 | 6（5） | 69 / 0.9% | 客诉·投诉·售后·退货·质量事故 |
| `compete` | 竞对情报 | 6（5） | 52 / 0.7% | 竞对·竞争对手·竞品·友商·替代·价格战 |
| `biz` | 客户经营动态 | 6（4） | 19 / 0.3% | 融资·上市·扩产·招标·集采·挂网·经营动态 |

- **质检检查单 = 该类型的必填维度清单**，缺项高亮并**阻止提交**（浏览器实测：空表单提交被拦截，报「缺 4 个必填维度」）。
- 维度字段名沿用 `d1..d6` 键（`structured{}` 内），键集齐全、值可空，便于下游按 `d6` 抽竞对、按 `d2` 抽货号/量化。
- **【推断】** 收口 + 必填维度是让看板能自动聚合的前提；不收口，总览只能靠人工二次归类。
  强制度范围（全类型强制 vs 仅测试/客诉/竞对强制）属管理决策，**由业务方定**。

### 14.2 第一层新增 4 组列（表格视图）

| 组 | 列 | 来源 | 口径 |
|---|---|---|---|
| 客诉 | 在途条数 · 高危条数 · 已闭环数 | `数据/mcp_complaints.json`（373 条，在途 176） | 客户级聚合；`urgency ∈ {critical,high}` 记高危 |
| 竞对 | 品牌（多选）· 命中次数 | 活动文本正则抽取 | **口径 = 「活动里被提到」≠「该客户正在用」**；外部信源见 §14.4 |
| 客户公司经营 | risks 标签（今年无采购 / 采购大幅下滑 / 客诉进行中）· 上市状态 | `customer_360.risks`（523 家）+ `market_intel/客户上市状态`（20 家） | 标签原文直引，不做二次解释 |
| 年度目标 | 目标（**占位口径 · 口径待定**）· y2024 · y2025 · 缺口 | `opportunity_sandbox.json`（有效 306 条） | 目标取沙盘 `target`；**不含 2026 实际**（需 ERP `doc_no` 回连，未做）；实际金额只认 ERP（P1） |

### 14.3 总览页三视角

| 视角 | 使用场景 | 主要指标卡 |
|---|---|---|
| 周会 | 销售 / 技术支持 / 研发 同屏 | 本周活动 · 本周有动作的商机 · 未闭环客诉 · 待办库在办；活动分类构成（含 74.5% 无结构化告警）· 按部门分布 · 未闭环客诉 Top10 · **商机外活动桶** |
| 月会 | 管理层 | 商机总数 · 目标合计 · 有活动商机 · 沉默 >90 天；**线别覆盖表** · 阶段漏斗 · 竞对动向 · 市场情报 · 部门对比 · 沉默三档 |
| 年度目标 | 看客户年度目标及达成 | 部门/个人 ERP 口径达成 · 商机级占位目标表 · **口径缺口卡**（三处不一致） |

- **商机外活动桶必须保留**：仅按商机行看板会丢掉 5,479 条（72.8%）活动（科研客户 + 479 条无客户线索），否则「活动量下降」会被误读成「销售没干活」。
- **风险卡降噪**：红 197 / 黄 33 / 灰 145 → 每档封顶 12 / 10 / 8，其余折叠。

### 14.4 自动管线（S2）现状与缺口

| 自动源 | 状态 | 依据 |
|---|---|---|
| 竞对/客户情报 | 已有可跑通道 | 自建 skill `molecular-dx-daily-intel`（15 信源 + 客户关联 + 信源健康度）；`数据/market_intel/latest.md` 第 43 周 |
| 活动 → 商机抓取 | 已实现（客户名 + `aliases[]` 归一） | 活动归一率 43.2%；落在商机客户的 2,043 条 |
| 商机 → 客户 ID | 仍缺 | 沙盘与飞书商机名对不上（精确命中 35/339），现按客户名归一 |
| 技术支持自动带出 | **0/565** | `tech_mapping` 18 条销售员映射**全是生命科学**，与 565 个诊断原料商机不相交 |
| 客户经营自动刷新 | 部分 | `customer_360.risks` 静态标签 + 上市状态一次性；未接定时刷新 |

### 14.5 里程碑与商务状态的关系（【推断】，口径待确认）

飞书「商务状态」4 档（沟通需求确认 201 / 产品试用中 47 / 订单签订中 26 / 实现销售 20）与 8 个「客户里程碑」
**不是同一条轴**。本版落点映射（**推断**）：沟通需求确认→陌生开发 · 产品试用中→方案匹配 · 订单签订中→中试 · 实现销售→临床试验。

- 阶段来源分三档并逐行标注：**自身飞书状态 294** / **客户级代理 65**（该商机本身无状态，取同客户飞书商机最靠后档）/ **阶段未知 206**（灰显，不臆断）。
- **8 节点后 4 个**（已确认被用于注册 / 已转产 / 客户已获证 / 新证迭代）**无自动来源**，需人工维护或另找数据源。
- 商机行中 **33 条占位行**（客户名 `nan` / `XX…`）已剔除，携带目标 50.6 万**未计入**合计。

### 14.6 本轮待业务方拍板的口径（页面第 ④ Tab 同表列出）

商机级年度目标最终口径 ｜ 生命科学商机台账是否单建 ｜ 销售员→部门映射（113 个商机 / 1,628.2 万目标挂在名单外销售员名下）
｜ 活动结构化强制度范围 ｜ 技术支持挂商机的口径（客户级/区域级/项目单级）｜ 后 4 个里程碑谁维护 ｜ 风险卡降噪阈值
｜ 飞书写入链打通（需装 `lark-cli` + 用户 OAuth；本机无 node/npm、Connector 目录无飞书）

### 14.7 验收

`python3 脚本/verify_customer_thread_v9.py` → **64 项全绿**（结构 20 / payload 解析 1 / 源文件独立对账 12 / 内嵌自洽 10 / 口径标注 5 / 元素计数 9 等）。
浏览器实测：4 个 Tab 逐个渲染、**console 0 error 0 warning**、第二层空表单提交被质检拦截、填齐 4 个必填维度后三轨落盘（本地记录 / CloudBase JSON / 飞书描述）输出完整。

**数据刷新复验（2026-10-31）**：源数据在首次构建后再次刷新（`activities_feishu.json` 12:04、`customer_360.json` / `mcp_complaints.json` 20:23），
已按新源重建 → 验收仍 64/64 全绿，页面 4 个 Tab 复测渲染正常、console 0 error。
口径变动：活动 7,491 → **7,522**；无结构化 5,573 → **5,601（74.5%）**；生命科学 3,120 → **3,142（41.8%）**；商机外 5,450 → **5,479**；
活动归一率 43.3% → **43.2%**；`type` 写法 2,319 → **2,345 种**。商机 565 / 客诉 373 / 客户 1,275 / 目标 3,874.7 万不变。

