---
name: baoyu-workbench-data-pipeline
description: |
  宝锐销售工作台数据管道 — 每日上传 → 自动处理 → JSON → 前端渲染。
  处理飞书活动导出、ERP销售订单、销售目标文件，生成工作台所需JSON数据。
  支持新旧两种CRM/ERP格式的自动检测。
triggers:
  - 用户上传了活动导出/销售订单/销售目标文件
  - 用户说"更新工作台数据"
  - 工作台数据显示不正确或过期
---


# 销售工作台数据管道

## 架构

```
数据/每日上传/           ← 用户每日拖入文件
    ├── 活动导出*.xlsx    → daily_activities.json
    ├── 销售订单导出*.xlsx → daily_tests.json + daily_sales.json
    └── 销售目标更新*/     → kpi_dashboard.json

regenerate_jsons.py       ← 处理脚本（自动检测格式版本）
process_daily.sh          ← 一键运行
cron: 0 8,18 * * *       ← 自动更新
```

## refresh_erp.py 文件选择陷阱（2026-07-20）

**⚠️ 必须按 mtime 排序，不能按文件名排序。**

`refresh_erp.py` 使用 `os.listdir` + `sorted()` 选择最新导出的文件。如果按文件名排序，`销售订单_2026年`（下划线）会排在 `销售订单导出-销售管理`（汉字）之前（ASCII 下划线 95 < 汉字），导致选取的是旧文件而非真正最新的文件。症状：运行脚本后数据不变，日志显示处理了旧文件。

**修复**（已应用到 `refresh_erp.py`）：
```python
# ❌ 错误：按文件名排序（_ 比汉字先，导致旧文件被选中）
files = sorted([f for f in os.listdir(SRC) if f.startswith('销售订单') and f.endswith('.xlsx')])

# ✅ 正确：按文件修改时间排序
files = [os.path.join(SRC,f) for f in os.listdir(SRC) if f.startswith('销售订单') and f.endswith('.xlsx')]
files.sort(key=lambda f: os.path.getmtime(f))
```

## CRM 活动 Excel → MCP JSON 合并（2026-07-20）

当用户上传飞书活动 CRM 导出 Excel 但仪表盘依赖 MCP-generated JSON 时，需要将 CRM Excel 转换为 JSON 格式并合并到现有的 `mcp_activities.json`。

**触发场景**：用户说"更新工作台数据"或上传活动导出文件，且 `mcp_activities.json` 的 `by_month` 中缺失当月数据时。

**转换脚本**（内联 Python，直接在 terminal 中执行）：
```python
import json, pandas as pd
from datetime import datetime

# 加载现有 + 新 CRM
with open('数据/mcp_activities.json') as f: old = json.load(f)
df = pd.read_excel('活动导出-xxx.xlsx', dtype=str)
df['date'] = df['创建时间'].str.strip().str[:10]

# 合并 by_month（格式：{date: {dept: count}}）
by_month = dict(old.get('by_month', {}))
for date_key, grp in df.groupby('date'):
    if date_key not in by_month: by_month[date_key] = {}
    by_month[date_key]['诊断原料销售部'] = by_month[date_key].get('诊断原料销售部', 0) + len(grp)

# 合并 by_creator
by_creator = dict(old.get('by_creator', {}))
for name, cnt in df['创建者'].value_counts().items():
    by_creator[name] = by_creator.get(name, 0) + int(cnt)

# 更新
old['updated'] = datetime.now().strftime('%Y-%m-%d %H:%M')
old['total'] = sum(sum(v.values()) for v in by_month.values())
old['by_month'] = by_month; old['by_creator'] = by_creator
with open('数据/mcp_activities.json', 'w') as f: json.dump(old, f, ensure_ascii=False, indent=2)
```

**关键注意事项**：
- CRM 导出无 `跟进部门` 列，默认归类到 `诊断原料销售部`
- `by_creator` 值是 int（不是 dict），直接加计数
- `by_month` 值是 `{dept: count}` 的 dict，不是 int
- `highlights` 数组取最近 50-100 条即可
- **⚠️ 合并后必须重算 `by_dept`**：合并 CRM 数据时 `by_dept` 不会被自动更新。需从 `by_month` 重新计算：
```python
by_dept = {}
for depts in by_month.values():
    for d, c in depts.items():
        by_dept[d] = by_dept.get(d, 0) + c
old['by_dept'] = by_dept
```
否则前端部门筛选下拉无数据（面板 will show empty filter）。

## ⚠️ ERP 全年文件处理 + 正确口径（2026-08-21 血案：单月逻辑算错全年）

**触发**：用户上传全年 ERP 文件（如 `销售订单_2026年1月1日～8月20日订单数据.xlsx`，21338 行），要求"更新所有销售额数据"。

### 正确口径（铁律）

```python
标准销售订单 + 已审核 + 是否赠品!='是' + 剔除宝锐系
# 宝锐系 = 客户名含 '宝锐|宝泰|横琴'（珠海宝锐/横琴宝锐/成都宝泰万润，约3.6万）
```
- 赠品行（是否赠品=是）价税合计**全为0**，排除与否不影响总额，但保持排除更严谨
- "基础物料数据（勿选）"**不是**免费配套，含真实销售金额（1005.9万），**不能排除**
- 测试单金额=0；预测订单（FO开头）金额33万不算销售

### KPI 达成率核算口径（问「算XX部门所有人的KPI达成率」时）

权威三件套：
- **目标**：`kpi_dashboard.json` 的 `people[].target`（手动维护，是权威目标）。⚠️ `my_kpis.json` 里的 `annual_target` 是旧值（陈溪子900万→现670万、黄文才500万→现300万），以 kpi_dashboard.json 为准。
- **实际**：`erp_sales_core.aggregate()` 返回的 `monthly_person` 按月求和（= 每人 `h1_total` + `h2_actual`）。直接 `python3 脚本/erp_sales_core.py` 看最新文件和月度汇总。
- **达成率 = 累计实际(1-当月) / 全年 target**（不是 H1 达成率，不是半年目标）。部门达成率 = `monthly_dept` 部门实际 / 部门目标（大客户2650万/拓展3400万/生命科学900万）。

**前端 `getKPI()` 个人达成率口径（2026-09 修复）**：`工具/index.html` 和 `工具/app.html` 的 `getKPI()` 已改为读 `dashPerson`（`kpi_dashboard.people` 的 `target` + `h1_total + h2_actual` 全年数据），**不再优先 `myPerson`**（`my_kpis.json` 只到 6 月、且旧目标 900万/500万）。若前端达成率又显示旧值或出现「入职后达成率」拆分标签，检查这两个文件是否被改回：`JOIN_SPLIT` 应为空对象、`ytd` 应为 `h1_total + h2_actual`。改完两个文件都要 `node --check` 提取的内联 JS 校验 + 部署（index.html→`/workbench/index.html`，app.html→`/工具/app.html`，landing 在 `/工具/` 用 `./app.html` 相对跳转）。

核算时必须核对/标注的已知数据问题（勿直接照搬数字）：
1. **任鑫 `target=4.0` 是脏数据**：应为 430万（这样拓展部12人目标总和才=3400万）。任鑫已离职，1-6月有33.2万、7月后归零；离职后这430万目标无人承接。
2. **大客户部目标缺口 549万**：赵云浩1350万 + 刘子研751万 = 2101万 ≠ 部门2650万。黄明月（原大客户1岗、1300万目标）5月离职后，刘子研只承接751万，剩余549万无人扛。
3. **刘子研口径特殊**：其实际含前手黄明月/张立娅 + 刘新元代管客户（23家，`_key_customers.json`）的1-6月订单（`map_person` 归并），是「大客户1岗」业绩而非个人从零做起。**2026-09 用户最终定调**：刘子研个人达成率 = 全年 751万（`target`）口径，**不拆分、不区分入职前后**；「大客户1岗」1300万口径（约43.7%）只作岗位参考视角。前端 `JOIN_SPLIT` 入职拆分逻辑已停用（清空为 `{}`），勿再恢复 1300万拆分。
4. **当月初数据缺失**：ERP 最新文件名只到 `_20260901`（9月1日），9月2日等当天订单在 `mcp_orders_input.json`（按创建时间）但金额口径与 ERP 不同，P1 铁律禁止混入——达成率只认 ERP，若用户问「截止今天」要说明数据实际截止到 ERP 最新文件日期。

### H1 口径（审计 vs ERP）

H1（1-6月）标记"已审计"（2164万，来自 h1_monthly.json/月度报告）。ERP 全年文件最新 H1=2268.9万（差104.9万，6月差最大+73.7万，是7-8月补录6月订单）。**默认保持审计值，如需 ERP 覆盖需用户确认**。

## 上传文件夹

路径：`~/Desktop/Hermes输出-工作类/数据/每日上传/`

支持的文件名模式：
- `活动导出*.xlsx` 或 `CRM活动导出*.xlsx`
- `销售订单导出*.xlsx` 或 `ERP销售订单*.xlsx`
- `销售目标更新*` 目录（含三部门月度数据文件）

## ⚠️ 收工前必查：有两个 ERP 派生物不在自动管道内（2026-09-23）

ERP 增量入库后，**除 5 个主 JSON 外还须手动补这两个**，否则前端会静默落后一整版：

| 派生物 | 生成脚本 | 谁在用 | 表现 |
|---|---|---|---|
| `数据/price_index.json` | `脚本/build_price_index.py` | `脚本/price_api.py`（客户 Tab 价格查询） | CDN 上那份曾落后一整版：本地 6.47MB vs 线上 6.04MB（meta.sources[0].file 指向旧件） |
| `数据/order_sku_index.json`、`customer_price_history.json`、`customer_order_context.json` | `脚本/build_order_data.py`（下单页三合一底座） | `/api/order/*` 接口 | 货号索引 / 历史成交价 / 账期台账停在旧件日期。**改了 `NORM`（如新增「物料编码」）也要重跑，并把 `工具/index.html` 里 `工具/下单.html?v=` 缓存号递增**，否则销售看到的还是旧子页 |
| `数据/orders_detail_2026.json` | `脚本/gen_orders_detail.py`（订单管理「完整明细」数据层） | `工具/index.html` 订单管理 Tab | 订单展开后的 品名/货号/规格/数量/单价/金额 停在旧件 |
| `数据/product_bom_map.json` | `脚本/build_product_mapping.py`（产品目录 BOM 索引） | `脚本/order_api.py` 的 `bom_info()` → 历史成交价查询的「套件构成」 | 产品目录换新版后不重跑 → 套件构成/物料编码是旧的；同次还会重出 `数据分析/宝锐分子诊断产品清单_父项子项映射_*.xlsx` |

### ③ 「订单类型」全是「未知」+ 物流异常口径错

- **订单类型**：MCP 订单**没有单据类型字段**，旧代码读 `o.orderType` 永远空 → 全显示「未知」。修复：`build_erp_ship_dates.py` 索引加 `ty`（单据类型），`attach_erp_ship_dates` 写入 `o['orderType']`。实测分布：测试 750 / 标准 582 / 预测 52 / 样机 1（剩下「未知」= ERP 未含的当天新单）。
- **物流异常**：旧条件 `s !== 'delivered' && s !== 'in_transit' && s !== 'no_tracking'` 把 **派送中/已揽收也算异常**。改为只算 `no_data` / `error` / `unknown`（在途是正常状态）。

### ERP 入库标准流程（五步，一步都不能省）

**拍基线 → 核验增量结构 → 合并 → 逐月回归校验 → 跑管道并三层验证**。完整命令、判读口径与常见误判见 `references/erp-incremental-intake.md`；配套脚本 `脚本/erp_preflight.py`（体检）、`脚本/erp_baseline.py`（拍基线）、`脚本/erp_inspect_increment.py`（核验结构）、`脚本/erp_merge_increment.py`（合并+回归校验，末尾打印 `MERGE_OK=1/0`）。

**🔴 铁律一：合并前必须拍基线快照，合并后必须逐月回归校验历史月。** 用 `aggregate()` 把合并前的逐月总额（部门/个人维度）落盘为 `数据/_erp_baseline_snapshot.json`；合并后重算并断言「增量区间之前的历史月一字不变」（差异 < 0.05万），只有最新月允许变化。只核总行数或全年总额会漏掉「9 月对了但 7 月被吃掉」这类静默损坏 —— 历史月一旦变动，立即停下排查切分点与列对齐，不准部署。

**🔴 铁律二：任何脚本都不准硬编码 ERP 文件名**（`销售订单_20260101-YYYYMMDD.xlsx` 每次导出都变）。一律 `sys.path.insert(0,'脚本'); import erp_sales_core; erp_sales_core.find_latest_erp()`。加新分析脚本前先 `grep -rnE "销售订单_20260101-[0-9]{8}" 脚本/*.py 数据/*.py | grep -v YYYYMMDD` 查残留 —— **必须带具体日期再筛**，裸串会命中几百条 JSON 的 `source` 字段和合并件的输出名（那些正常，不要动）。（已修过 `product_alias.py`、`客户分年对比分析.py`、`gen_ka_split.py`）

**🔴 铁律三：原始增量文件归档到 `月度销售额数据/_增量归档/`，不要丢进 `数据/每日上传/`。** `find_latest_erp()` 对两个源目录都是**非递归** `listdir`，增量文件若 mtime 比合并件新就会被选中 → 历史金额全丢。放子目录天然不被扫到。

**🔴 铁律四：增量件可能是现用件的「超集」，合并前先按现用 schema 对齐列。** 2026-09-20 实测增量 31 列 vs 现用 27 列（多出 `备注(内部）#` / `备注(客户）#` / `备注` / `收款条件备注#`，前 27 列同名同序）。`erp_merge_increment.py` 已内置对齐逻辑：列不一致时打印「多余列/缺失列」，`new = new[list(old.columns)]` 丢弃多余列，**缺列则 assert 中止**。丢弃备注列不影响金额链路（客户特殊要求走 `销售订单导出-销售管理.xlsx` 另一路）。遇到列差异先看清是多列还是缺列，缺列绝不能硬合。

**🔴 铁律五：合并/核验脚本不再硬编码文件名 —— 一律用 `ERP_INCREMENT` 环境变量。**（旧脚本里的 `NEW = 'doc_xxx_销售订单_...xlsx'` 会指向上一轮的增量件，是最隐蔽的错源。）

```bash
ARCH="$HOME/Desktop/hermas输入-工作台数据库/月度销售额数据/_增量归档"
cp -p ~/.hermes/cache/documents/doc_<hash>_销售订单_20260901-0920.xlsx "$ARCH/销售订单_20260901-0920.xlsx"   # ① 先归档（缓存会被清）
ERP_INCREMENT="$ARCH/销售订单_20260901-0920.xlsx" /usr/bin/python3 脚本/erp_inspect_increment.py   # ② 核验
ERP_INCREMENT="$ARCH/销售订单_20260901-0920.xlsx" /usr/bin/python3 脚本/erp_merge_increment.py    # ③ 合并，末尾必须 MERGE_OK=1
```

拍基线前先把上一份快照另存 `数据/_baseline_history/_erp_baseline_<旧截止日>.json`（`erp_baseline.py` 会原地覆盖，不留档就没法回溯）。

**已知坑（2026-09-21 实测）**：① `erp_preflight.py` 曾缺 `~/Desktop` 前缀 → 报「目录不存在」并显示错误的最新文件（已修，新增路径后须复核它打印的「现用最新 ERP 文件」是否等于 `erp_sales_core.find_latest_erp()`）；② 三层验证读 JSON 的键名：`kpi_dashboard` 用 **`depts`**（不是 `departments`）、`three_year_monthly['2026']` 月份键是**不补零**字符串 `'1'..'9'` 且未到月份是 `None`（求和前必须 `f = x if isinstance(x,(int,float)) else 0` 兜底）、`workbench_data` 内嵌键是 `kpiProgress` / `kpiDashboard`；③ **前端实读**：本地 8765 打开 `工具/index.html` 后，`state.data` 有 15 个模块但 `<main>` 为空 ≠ 数据问题（只是没走登录流程）。要出图必须补齐登录态：`loginOverlay.style.display='none'` + `app.style.display='block'` + **`state.user={name:'刘新元',role:'admin',dept:'销售管理',line:'da'}`** + `showApp()` + `loadData()`，等约 10s 再 `switchTab('dashboard'); render()`；**不设 `state.user` 时 `#main` 永远为空**（渲染分支依赖登录态）。读 `document.getElementById('main').innerText`，断言出现「当月进展 <数字>万」/「累计达成」；顺带扫一遍有没有 `/* … */` 类模板残留标记被渲染成可见文字。

**`aggregate(fp)` 返回 `(monthly_total, monthly_person, monthly_dept, source)`**，三个字典都是「先月后人/部门」的嵌套（`monthly_person['2026-09']['刘子研']`），不是按人嵌套。它只收 2026 年、且 `map_person` 归不了部门的行会被丢弃 —— 用它出的数才是权威口径。

**合并后必须重跑的管道（缺一步前端就不一致）**：`脚本/refresh_erp.py`（kpi_progress 当月）→ `数据/mcp_kpi_pipeline.py`（kpi_dashboard 的 H1/H2 + three_year_monthly 同步）→ `脚本/refresh_erp_amount.py`（erp_amount 按天/按客户）→ `脚本/merge_workbench_data.py`（前端 14→1 合并包）→ 部署 5 个 JSON（kpi_progress / kpi_dashboard / three_year_monthly / erp_amount / workbench_data）→ 断言本地 == CDN。

**⚠️ ser2date 截断时间（2026-09-07）**：新 ERP 导出的 `合同日期` 可能是字符串 `"2026-09-07 00:00:00"`（带时间），旧导出是 Excel 日期序列号（纯数字）。`ser2date()` 里 `float(v)` 对带时间字符串会抛异常走 `str(v)` 原样返回 → `_date` 带时间 → `aggregate_daily()` 的 daily key 变成 `"2026-09-07 00:00:00"`，前端 `_d >= periodStart` 字符串比较对边界日期（periodEnd 当天）会误排除。**修复**（已应用到 `erp_sales_core.ser2date`）：`str(v)` 后 `if ' ' in s: s = s.split(' ')[0]` 截断时间 + `s.replace('/','-')` 兼容斜杠。处理后 daily key 必须全部是纯 `YYYY-MM-DD`（验证 `sum(1 for k in keys if ' ' in k) == 0`）。

### 页面显示口径（planShipCell）

| 状态 | 显示 |
|---|---|
| 已发货 | 灰色日期（title 带 ERP 累计出库量） |
| **未发货 + 已过计划日** | **红色 `日期 ⚠超N天`** |
| 未发货 + 未到期 | 蓝色日期 |
| `FO` 开头（预测订单，不发货） | 浅灰日期，**绝不标红** |
| ERP 里查不到（当天新单未导出） | `—` |

筛选栏「📍 状态」加了伪状态 **`not_shipped`（⚪ 未发货(含超期)）**，命中条件 `o.shipped === false && !orderId.startsWith('FO')`（实测 52 单）。

**🔴 铁律：统计「超期未发」必须排除 FO 预测单。** 不排除时 104 单「未发货」里 52 单是 FO，超期数会从 **29 单虚报到 76 单**（实测）。FO 单本来就不发货，标红只会让人白追。

**坑：`dtype=str` 读 Excel 的空单元格是字符串 `'nan'`**，`float('nan')` 不抛异常 → 「未发货(out==0)」统计恒为 0。`num()` 里必须 `math.isfinite()` 兜底（本次踩过）。

### JSON 格式规范

每个 MCP JSON 文件包含两層架构：**汇总层**（KPI卡片用）+ **明细层**（表格渲染用）。

```json
{
  "updated": "2026-07-10",
  "total": 2817,
  "by_month": {"2026-04": {"诊断原料销售部": 669, ...}, ...},
  "by_dept": {"诊断原料销售部": 2190, ...},
  "by_creator": {"林潇": 290, ...},
  "highlights": [{"date": "...", "name": "...", "creator": "...", "dept": "..."}]
}
```

**关键原则**：
- 汇总字段与明细字段分层，前端 KPI 卡片读汇总层无需遍历全量
- 部门、创建者等分类维度用 `by_xxx` 字典，前端直接 `Object.entries()` 循环
- 高亮/精选数据单独一个数组（如去除了"活动跟进"等通用名的活动），避免前端做二次过滤

### 面板集成：非侵入式链接

**不要修改作战室 HTML 内部结构**（图表/表格/业务逻辑耦合度高，patch 容易 div 失衡或 JS 断裂）。改为：

1. 在顶栏导航区添加一个 `<a>` 链接按钮，指向独立面板
2. 按钮风格与现有导航一致（复用 CSS 变量）
3. `target="_blank"` 新窗口打开，不影响作战室状态

示例（作战室白底蓝调主题）：
```html
<a href="/工具/飞书实时数据面板.html" target="_blank"
   style="font-size:10px;color:var(--pri);text-decoration:none;
          padding:5px 12px;border:1px solid var(--pri);border-radius:6px;
          font-weight:600;white-space:nowrap"
   onmouseover="this.style.background='var(--pri)';this.style.color='#fff'"
   onmouseout="this.style.background='transparent';this.style.color='var(--pri)'">
  📡 飞书数据</a>
```

示例（统一工作台暗色顶栏）：
```html
<a href="/工具/飞书实时数据面板.html" target="_blank"
   style="padding:4px 10px;border:1px solid rgba(255,255,255,.3);
          border-radius:6px;background:transparent;color:rgba(255,255,255,.7);
          text-decoration:none;font-size:11px"
   onmouseover="this.style.background='rgba(255,255,255,.15)';this.style.color='#fff'"
   onmouseout="this.style.background='transparent';this.style.color='rgba(255,255,255,.7)'">
  📡 飞书数据</a>
```

### 面板 HTML 设计规范

- **三 Tab 结构**：活动 / 客诉 / 阻塞，每个 Tab 独立渲染函数
- **零外部依赖**：纯 HTML+CSS+JS，不依赖 Chart.js/HermesTable
- **加载态**：`Promise.all` 并行 fetch，失败时显示错误信息+启动指南
- **KPI 卡片先行**：每个 Tab 顶部 3-4 个 KPI 卡片，下方是表格明细
- **安全停止**：所有数据从本地 JSON 加载，不上云、不走外网

### 入口文件索引

| 面板 | 路径 | 入口位置 |
|------|------|---------|
| 飞书实时数据面板 | `工具/飞书实时数据面板.html` | 作战室 / 统一工作台 顶栏 |
| KA客户3年对比 | `工具/KA客户3年对比.html` | 综合管理仪表盘 |
| 科研测试反馈 | `工具/科研测试反馈.html` | 综合管理仪表盘 |
| 宝锐三表一书 | `工具/宝锐三表一书.html` | 独立工具 |

## CloudBase 自动同步模式（2026-07-25）

将本地 MCP 管道产出的 JSON 自动推送到腾讯 CloudBase 静态托管，实现公网访问的数据自动更新。

### ⚠️ Cron 脚本路径陷阱（2026-07-27）

Hermes cron 的 `--script` 参数**必须**指向 `~/.hermes/scripts/` 目录下的相对路径，不能直接写项目目录的绝对路径或相对路径。如果实际脚本位于项目目录（如 `~/Desktop/Hermes输出-工作类/脚本/cb_sync.sh`），必须在 `~/.hermes/scripts/` 下创建一个**包装脚本**，让 cron 调用它。

**错误配置**（会报 `Script not found`）：
```bash
cronjob create --name "CloudBase数据同步" --schedule "0 */2 * * *" \
  --script "/Users/liuxinyuan/Desktop/Hermes输出-工作类/脚本/cb_sync.sh" --no_agent
# 或
--script "脚本/cb_sync.sh"
```

**正确做法**：
```bash
# 1. 创建包装脚本（只需一行，转发到真实脚本）
cat > ~/.hermes/scripts/cb_sync.sh << 'EOF'
#!/bin/bash
exec bash "$HOME/Desktop/Hermes输出-工作类/脚本/cb_sync.sh"
EOF
chmod +x ~/.hermes/scripts/cb_sync.sh

# 2. 用相对路径创建 cron 任务（相对于 ~/.hermes/scripts/）
cronjob create --name "CloudBase数据同步" --schedule "0 */2 * * *" \
  --script cb_sync.sh --no_agent
```

包装脚本本身不要包含业务逻辑，只负责转发；业务逻辑继续放在项目目录的 `脚本/cb_sync.sh` 里，便于版本管理和备份。详见 `references/hermes-cron-script-wrapper.md`。

### ⚠️ 下单人字段取数：必须用订单角色，禁止用客户 owner 映射（2026-07-28）

**问题**：`pull_orders.py` 曾通过 `mcp_customers.json` 的 `owner` 字段映射"创建者"。客户 owner 离职后未更新（黄文清 26 个客户、李丽佳 32 个客户），导致所有关联订单显示已离职人员。

**根因**：客户的 `owner` 字段是指定客户负责人，不等于每笔订单的实际下单人。下单人信息存储在订单工作项的**角色分配**中，不在客户字段。

**正确方案**：调用 `get_workitem_brief` 获取每笔订单的 `role_members`，提取 `name == "下单人"` 的成员：

```python
result = mcp("get_workitem_brief", {"project_key": PK, "work_item_id": wid})
role_members = result.get('work_item_attribute', {}).get('role_members', [])
for rm in role_members:
    if rm.get('name') == '下单人':
        xdr = {'name_cn': rm['members'][0]['name'], 'email': rm['members'][0]['email']}
```

**缓存策略**：186 个订单逐个调 `get_workitem_brief` 耗时约 60s（0.3s 间隔限速）。必须建 `order_role_cache.json` 缓存，新订单才查询：

```python
role_cache = json.load(open('order_role_cache.json'))  # {orderId: {name_cn, email}}
uncached = [o for o in unique_orders if o['name'] not in role_cache]
# 只对 uncached 调用 get_workitem_brief，结果写回缓存
```

**取值优先级**：订单角色（下单人）> 客户→销售员映射（`@biori.com` 兜底）

**效果**：黄文清/李丽佳从 58 条订单中彻底消除，下单人命中率 186/186 = 100%。

### 商机沙盘数据管道

源文件：`2026商机达成沙盘.xlsx`（13个Sheet，按销售员分，共339个商机）
输出：`数据/opportunity_sandbox.json`
仪表盘：销售工作台.html 新增「商机沙盘」Tab
注意：销售员中有离职/调岗人员（黄明月/任鑫/龙友波/杜建），需按区域维度聚合

## 详细资料（按需查阅）

> 以下文件不在本 SKILL.md 内，需要时再读；正文只保留每次都会用到的部分。

- **`references/data-pipeline.md`** — 含 25 节：mcp_kpi_pipeline 的"单月"bug、人员映射（ERP销售员 → KPI people）、cron 覆盖风险（务必同步修复）、订单管理 Tab 的「完整明细」（2026-09-23 新增）、订单管理 renderOrders 金额统计彻底改用 ERP（2026-09-07）、mcp_k…
- **`references/details.md`** — 含 14 节：共享模块、① 客户 Top5 把「周」当「月」取数（最严重）、通用教训、前端日期 toISOString() UTC 偏移（2026-09-07）、错误链路、两个断点
- **`references/frontend-ui.md`** — 含 1 节：② Top5 不理会表格筛选

### 既有参考文件（原正文未引用，此处补齐索引）

- **`references/crm-format-2026.md`** — CRM活动导出格式
- **`references/customer-360-ownership-authority.md`** — customer_360 归属权威化
- **`references/customer-ownership-official-list.md`** — 客户归属权威源：《诊断原料&生命科学客户清单》
- **`references/customer-profile-synthesis.md`** — 客户概况提炼（按客户维度汇总客诉/订单/测试/活动）
- **`references/echarts-monthly-report-pattern.md`** — ECharts 自包含HTML报告生成规范
- **`references/erp-column-layout-variants.md`** — ERP导出文件列布局变异
- **`references/erp-format-2026.md`** — ERP销售订单格式版本对照（2026-07-07）
- **`references/erp-format-20260701.md`** — ERP销售订单格式 — 20260701后新格式
- **`references/erp-quick-lookup.md`** — ERP 跨年客户+货号快速查询（2026-07-10）
- **`references/erp-refresh-mtime-fix.md`** — refresh_erp.py mtime 排序陷阱
- **`references/h1-archive-role-mapping-pitfall.md`** — H1 订单存档管道：致命陷阱与正确方案
- **`references/h1-erp-crossfill.md`** — H1 存档 ERP 交叉填充（2026-07-28）
- **`references/h1-monthly-archive-pipeline.md`** — H1 月度归档拉取管道（2026-07-28）
- **`references/html-event-handler-anti-pattern.md`** — HTML innerHTML 事件处理器反模式
- **`references/html-js-common-pitfalls.md`** — 工作台 HTML JS 常见问题
- **`references/js-orphan-brace-pattern.md`** — HTML/JS 孤儿括号诊断与修复
- **`references/ka-3year-data-generation.md`** — KA客户3年数据生成
- **`references/kpi-proportional-allocation-pitfall.md`** — KPI Pipeline 比例分摊根因分析（2026-08-03）
- **`references/kpi-refresh-pipeline.md`** — KPI 进度刷新管道（MCP 直连）
- **`references/kpi-target-file-format.md`** — 三部门KPI目标文件格式
- **`references/mcp-realtime-api-server.md`** — MCP 实时 API 服务器架构（2026-07-26 上线）
- **`references/mcp-realtime-migration-plan.md`** — MCP 实时数据迁移计划（2026-07-26）
- **`references/mcp-realtime-v2-architecture.md`** — MCP 实时数据架构 v2.0（2026-07-26）
- **`references/mcp-sales-amount-limitation.md`** — MCP 销售订单金额字段限制
- **`references/patch-quote-escaping.md`** — Patch工具HTML引号转义陷阱
- **`references/sf-express-tracking-pipeline.md`** — 顺丰路由查询管道（MCP销售订单 → shipment JSON）
- **`references/sf-feishu-push-pipeline.md`** — 顺丰物流 + 飞书弹窗推送全链路
- **`references/sf-opcode-reference.md`** — 顺丰快递 opCode 状态码参考
- **`references/test-feedback-workflow.md`** — 科研测试反馈工作流
- **`references/three-year-monthly-key-format.md`** — ERP 全年文件同步 three_year_monthly 的月键格式坑（2026-08-21）
- **`references/upload-pipeline.md`** — 工作台文件上传管道（2026-08-07）
