# baoyu-workbench-patterns — data pipeline

### MCP 连接参数
```
Token: m-abfb29e8-3104-434f-9944-8d0bb592f8cd
Endpoint: https://project.feishu.cn/mcp_server/v1
销售管理 project_key: 6593cd71471290e3cc6be6e6
客诉项目 project_key: 658288abfb8bd616b17025f1 (售后管理, simple_name=br-shgl, 类型=khts)
客户项目 project_key: 658bb60520ea78a2125f1b99
```


### 销售订单 MQL（关键字段）
- 可用字段：`单据编号`, `创建时间`, `创建者`, `金额`, `货号#`, `单价`, `销售数量`, `work_item_id`
- **不可用字段（关系型）**：`关联客户`, `销售员`, `产品`, `订单状态`, `关联活动`
- WHERE 子句支持：`WHERE 创建时间 >= "2026-07-01"`
- ORDER BY + LIMIT + OFFSET 均支持
- 2026年新格式客户在E列，2024-2025年旧格式在D列


### 管道脚本清单
| 脚本 | 产出的JSON | 数据量 | 更新频率 |
|------|-----------|--------|---------|
| `mcp_pipeline.py` | `feishu_complaints.json` + `feishu_blockers.json` | 24条客诉+42条阻塞 | cron每日 |
| `mcp_activities_pipeline.py` | `daily_activities_mcp.json` | 3,104条活动 | cron每日 |
| `mcp_prefect_pipeline.py` | `daily_sales_mcp.json` (含客户+单价) | 441条/月 | cron每日 |
| `mcp_kpi_pipeline.py` | 更新 `kpi_dashboard.json` | H2月度实际值 | cron每日 |


### ERP xlsx 处理陷阱
**Sheet名映射**：文件中 Sheet1 可能是数据透视表，Sheet2 可能是销售员清单，**真正的原始订单数据在 Sheet1**（26k+ 行），需先通过 `pd.ExcelFile` 遍历所有 sheet 找到含"客户"+"价税合计"列的 sheet。
- 2024文件：Sheet1(26661行)=数据，Sheet2(42行)=元数据，Sheet3=销售员映射
- 2025文件：Sheet1(30630行)=全量数据，还有1-5月(11244行)和6月(2326行)分表
- 2026文件：Sheet1(16629行)=数据，Sheet2(36行)=元数据

**ERP新旧格式检测**：**必须检测首个 DATA 行（非 header，非汇总行），而非 HEADER 行**。Header 行的 G 列值为 `合同日期` 不含 `-` 字符。

```python
# 正确做法：跳过 header（rows[0]）和汇总行（"共X个"），检测首个真实数据行
is_new = False
for row in rows[1:]:
    cells = {cl(c.get('r')): cv(c) for c in row.findall('s:c', ns)}
    doc = cells.get('A','').strip()
    if '共' in doc and '个' in doc: continue  # 汇总行
    g = cells.get('G', '').strip()
    if g and len(g) > 3:
        is_new = '-' in g  # "2026-07-07" → True
        break

# 基于检测结果取正确的列：
if is_new:
    client = cells.get('D','').strip() or cells.get('F','').strip()
    sales  = cells.get('B','').strip()
    date   = cells.get('G','').strip()
    amount = float(cells.get('C','0') or 0)
else:
    client = cells.get('A','').strip()
    sales  = cells.get('C','').strip()
    date   = cells.get('B','').strip()
    amount = float(cells.get('D','0') or 0)
```

**CRM新旧格式检测**：旧格式有 K 列（活动参与人员），新格式无 K 列，日期在 H 列而非 M 列。
```python
is_old_crm = bool(cells.get('K'))  # 旧格式有K列
```

**步骤2：HTML fetch JSON**
```javascript
fetch('/数据/xxx.json').then(r=>r.json()).then(d=>{render(d);});
```

## KPI H1/H2 分开展示模式

> ⚠️ **当月进展必须校验 prog.month 匹配当前月**（2026-09-01 踩坑）：kpi_progress.json 的 month 字段=数据里最大月份（非当前日历月），月初用户未传新 ERP 文件时停在上月。getKPI 里 current = prog.total 会把上月数据当本月进展（9/1 显示「9月进展292.5万」实为8月累计、达成率虚高32.2%）。修复：解析 prog.month 末尾月份与 new Date().getMonth()+1 比对，不匹配则 current=0。共 4 处都要改：admin 分支 current / 非admin 分支 current / filteredCurrent（index.html 专属）/ getShipmentOrders 的 fullYearTax。

> 📊 2026年度目标已确认，详见 `references/2026-kpi-targets-confirmed.md`：全公司6,950万（大客户部2,650+销售拓展3,400+生命科学900）、三部门H1月度目标vs达成。
> 🎨 双色同柱对比（目标灰底+达成彩色）详见 `references/kpi-dashboard-dual-bar.md`。
> 🎯 看板 KPI 卡片三层递进（当月→当季→当年）详见 `references/kanban-kpi-cards-progressive.md`。\n> ⚠️ 改造注意事项（ytd漏H2/CloudBase部署/vision工具集）详见 `references/2026-08-09-session-pitfalls.md`。

KPI页面结构：顶部4卡（年度目标/H1实际/H2目标/7月MCP销售），下方4表。

**H1表**（1-6月，已审计）：列=姓名/部门 + 年目标 + 1-6月 + H1合计 + 完成率(进度条)

**H2表**（7-12月，含季度分组）：
```javascript
function renderH2(id,data){
  var h='<tr><th rowspan="2">部门</th><th rowspan="2">年目标</th>';
  h+='<th colspan="3" style="background:#fff3e0">Q3（7-9月）</th>';
  h+='<th colspan="3" style="background:#fce4ec">Q4（10-12月）</th>';
  h+='<th rowspan="2">H2目标</th></tr><tr>';
  // 月列头 + 季度合计行
  data.forEach(function(d){
    var tgt=d.h2_months, act=d.h2_actual_months;
    tgt.forEach(function(t,i){
      var actStr=act[i]>0?'<br><span>实'+fmt(act[i])+'</span>':'';
      h+='<td>'+fmt(t)+actStr+'</td>';
    });
  });
  // 季度合计行：grand[0]=Q3, grand[1]=Q4
}
```
**销售单目标达成模式**：Promise.all 并行加载销售JSON + KPI JSON，从KPI提取当月目标（h2_months[0]），计算达成率。

```javascript
Promise.all([
  fetch('/数据/daily_sales_mcp.json'),
  fetch('/数据/kpi_dashboard.json').catch(function(){return null})
]).then(function(results){
  var monthAmt = ...; // MCP本月累计
  var monthTarget = kpi.depts.reduce(function(s,d){return s+d.h2_months[0]}, 0);
  var pct = (monthAmt/monthTarget*100).toFixed(1);
  // KPI卡片: 今日/本月累计/本月目标/达成率
});
```

## 验证清单

每次修改 HTML 后：
```bash
sed -n '/<script>/,/<\/script>/p' xxx.html | sed '1d;$d' > /tmp/_check.js
node --check /tmp/_check.js && echo "JS ✅"
```

**验证「A 在 B 之前」时要从 body 锚点往后搜**：自包含页的 `<style>` 在前，`find('wf-intro')` 会先命中 CSS 定义（`find('wf-steps')` 同理），于是“顺序为假”的误报。正确做法：先 `b=h.find('id="wfBody"')` 定位正文，再在 `h.find(x, b)` 上比位置；同理，凡用子串存在性验证布局/顺序，都要先排除 head 里的 `.<class>{...}` 定义。

## APIfetch容灾 + 缓存破坏（2026-07-10 实战）


### 问题：`/api/activities` MCP卡死导致页面永久白屏

API服务器实时MCP调用可能挂起，`fetch()` 不抛错也不resolve → `Promise.all` 全部请求被阻塞 → 页面永远停在"加载中"。


### 数据管道

```python
# 2024/2025: ERP Excel Sheet1 → 按合同月份汇总价税合计
df = pd.read_excel(fp, sheet_name='Sheet1')
df = df[(df['是否赠品']=='否') & (df['价税合计']>0)]
df['month'] = df['合同月份'].astype(str).str[-2:].astype(int)
monthly = df.groupby('month')['价税合计'].sum().to_dict()
# 2026: h1_monthly.json (1-6月) + july_1_20_snapshot.json (7月)
```


### 数据源

`数据/h2_monthly_targets.json`：三部门 7-12月目标（万元），格式：
```json
{"depts":[{"dept":"销售拓展部","annual":3400,"months":{"7月":245.6,...}}],
 "monthly_total":{"7月":574.3,...},"h2_total":4785.9}
```


### 数据提取

```python
# 每个Sheet=一个销售员（陈溪子/林潇/黄文才…共13人）
# 列：销售|部门|序号|客户名称|商机名称|商机性质|2024/2025销售|2026目标|Q1-Q4拆分
# ⚠️ 数值单位不统一：有的用"142W"（万元），有的用"300000"（元）
# parse_w() 需处理W/万后缀，超过10万的数值除以10000归一化
def parse_w(v):
    if pd.isna(v): return 0
    s = str(v).replace('W','').replace('w','').replace('万','').replace(',','').strip()
    if s in ['', '-', '—', '－', 'nan']: return 0
    return float(s) / 10000 if float(s) > 100000 else float(s)
```


### 看板Tab（KPI + 月度趋势 + 同比环比 + 客户排名）

KPI数据从 `my_kpis.json` 加载（含年目标/H1达成/月度明细），与客户数据并行 fetch。

**进度条组件**（纯CSS，零依赖）：
```javascript
html+='<div style="background:#e0e0e0;border-radius:6px;height:20px;overflow:hidden">';
html+='<div style="background:'+(pct>=50?'var(--ok)':pct>=25?'var(--warn)':'var(--bad)')+';height:100%;width:'+Math.min(pct,100)+'%"';
```

**月度趋势柱状图**（纯CSS flex，零依赖）：
```javascript
months.forEach(function(m){
  var v=monthly[m]||0, h=Math.max(4,Math.round(v/maxVal*70));
  html+='<div style="flex:1;text-align:center">';
  html+='<div style="background:linear-gradient(180deg,var(--pri),rgba(26,115,232,.3));height:'+h+'px;border-radius:3px 3px 0 0"></div>';
});
```

**同比计算**：2024→2025: `(y25/y24-1)*100`, 2026H1 vs 2025半年度: `(y26/(y25/2)-1)*100`


### 数据源

`../数据/mcp_complaints.json`，结构：
```json
{"updated": "2026-07-25 00:51:14", "total": 193, "items": [
  {"id": 6928894630, "name": "20260326-天隆-M4211-3性能异常客诉", "days": 121, "created": "2026-03-26", "status": "", "area": "大客户", "operator": ["时彦祎","黄明月"]}
]}
```


### 数据源（已就绪）
- `kpi_dashboard.json`：`depts`(3部门) + `people`(22在职，字段 target/h1_months/h1_total/h1_rate/h2_months/h2_actual/h2_actual_months)
- `my_kpis.json`：`people`(32含离职，字段 annual_target/actual_ytd/completion_pct/monthly)
- `kpi_progress.json`：`by_salesperson`（当月各人实际）
- `diag_kpis.json`(24) / `diag_salespeople.json`(51)：诊断原料销售员明细


### 数据模型

```javascript
{text:'拜访圣湘', done:false, cat:'prep', client:'圣湘生物', due:'2026-07-15', date:'2026-07-13'}
```


### 总监视图（跨用户聚合）

```javascript
function viewClosedTodos(showAll){
  var closed=JSON.parse(localStorage.getItem('bior_sw_director_closed')||'[]');
  var display=showAll?closed:closed.filter(function(c){return c.salesperson===USER;});  // showAll=true→全部记录
  // 总监视图额外显示"销售员"列
}
```

## 总监/总经理聚合视图模式（2026-07-13）

顶栏用户选择器 `📊 大客户部` 选中时，聚合多个销售员（赵云浩+张立娅）的KPI和客户数据。


### 客户聚合

```javascript
function myCusts(){
  if(USER==='director'){
    return ALL_CUSTS.filter(function(c){return c.salesperson==='赵云浩'||c.salesperson==='张立娅';});
  }
  var sp=USER==='zhaoyunhao'?'赵云浩':'张立娅';
  return ALL_CUSTS.filter(function(c){return c.salesperson===sp;});
}
```


### KPI聚合（H1实际 + H2目标双源）

```javascript
if(USER==='director'){
  var k1=KPI_DATA.people.find(p=>p.name==='赵云浩');
  var k2=KPI_DATA.people.find(p=>p.name==='张立娅');
  kpi={annual_target:(k1?k1.annual_target:0)+(k2?k2.annual_target:0),
       actual_ytd:(k1?k1.actual_ytd:0)+(k2?k2.actual_ytd:0), monthly:{}};
  [k1,k2].forEach(function(k){
    if(k&&k.monthly)Object.keys(k.monthly).forEach(function(m){kpi.monthly[m]=(kpi.monthly[m]||0)+k.monthly[m];});
  });
}
```


### 前端本年统计合并
```javascript
// shipment_status.json 含 h1_summary + full_year_tax
if (orderState.analysisPeriod === 'year') {
    ps.total += data.h1Summary.total_orders || 0;
    ps.taxTotal = data.fullYearTax || 0;
}
```
收益：页面 8MB→2MB，日刷新 5min→30s。

## 📦 订单管理+物流轨迹集成模式（2026-07-24）★新范式

在销售工作台中新增Tab的完整模式：MCP数据管道 → 顺丰API物流查询 → 前端渲染+弹窗通知。


### 物流轨迹CSS时间线

```css
.shipment-timeline { position: relative; padding-left: 24px; }
.shipment-timeline::before { content: ''; position: absolute; left: 8px; top: 4px; bottom: 4px; width: 2px; background: var(--border); }
.shipment-node { position: relative; padding: 8px 0; }
.shipment-node::before { content: ''; position: absolute; left: -20px; top: 12px; width: 10px; height: 10px; border-radius: 50%; background: var(--pri); border: 2px solid #fff; box-shadow: 0 0 0 2px var(--pri); }
.shipment-node:first-child::before { background: var(--ok); box-shadow: 0 0 0 2px var(--ok); }
```


### 数据管道：MCP+顺丰API

详见 `baoyu-sales-copilot/references/sf-express-tracking-api.md`。

关键点：
- 顺丰API必须传 `checkPhoneNo`（手机后4位），否则路由为空
- 生产环境用 accessToken 鉴权，msgDigest 返回 A1006
- `trackingNumber` 必须数组格式 `["SFxxx"]`
- 沙箱测试在网页端操作，不用API

## 多模型对比分析模式（2026-07-22）

当需要评估不同模型的方案质量时，将同一 prompt 并行发送给多个模型，汇总对比。
