# 客诉数据管道：三个致命缺陷（2026-07-27 诊断）

客诉 tab 渲染函数 (`renderComplaints`) 与数据管道 (`mcp_refresh.py` → `mcp_pipeline.py`) 之间存在三重不匹配：

### 缺陷1：数据源为空
`mcp_pipeline.py` 用 `list_todo` 拉取当前用户待办，再过滤 `project_key=658288abfb8bd616b17025f1`（售后管理）的客诉。Token `m-abfb29e8-...` 绑定的用户若无待办，返回空列表 → `mcp_complaints.json` items=0。

### 缺陷2：管道输出字段 vs 页面期望字段不匹配

```
管道输出:  id ✓ | name ✓ | days ✓ | urgency | node_name | blockers | ka
页面期望:  id ✓ | name ✓ | days ✓ | created❌ | area❌ | status❌ | priority❌ | type❌ | operator❌ | creator❌
```

即使数据非空，筛选（区域/状态/优先级/类型/处理人）也全部失效。

### 缺陷3：硬编码日期
`mcp_pipeline.py` 第 17 行 `TODAY = datetime(2026, 7, 9)` 不随时间更新，`extract_days()` 计算的天数恒定偏小。

### 修复方案（2026-07-27 已实施）

**放弃 `list_todo`，改用 MQL 直接查询**。`list_todo` 依赖 Token 绑定用户的待办，不可靠。MQL 能直接分页拉取全量数据：

```python
# pull_complaints.py 核心逻辑
COLUMNS = 'name, start_time, current_status_operator, work_item_id, work_item_status, field_9e2144, owner, field_3ce9fe'
MQL = f"SELECT {COLUMNS} FROM `售后管理`.`客户反馈单` ORDER BY start_time DESC LIMIT 50 OFFSET {offset}"
```

**字段映射表**（客诉类型，PK=658288abfb8bd616b17025f1，type_key=6669433056a98249604376de，56个字段）：

| 页面字段 | MCP 字段 | 飞书显示名 | 取值方式 |
|---------|---------|-----------|---------|
| `name` | `name` | 流程名称 | string_value |
| `days` | 计算 | — | `(date.today() - created).days` |
| `created` | `start_time` | 创建时间 | string_value |
| `area` | `field_9e2144` | 反馈区域 | key_label_value.label |
| `status` | `work_item_status` | 状态 | key_label_value_list[0].label |
| `type` | `field_3ce9fe` | 反馈/客诉类型 | key_label_value.label |
| `operator` | `current_status_operator` | 当前负责人 | user_value_list → name_cn join |
| `creator` | `owner` | 创建者 | user_value.name_cn |
| `id` | `work_item_id` | 工作项ID | long_value → str |

**飞书链接格式**：客诉在 `br-shgl` 空间，不是 `baoyu`：
```
https://project.feishu.cn/br-shgl/khts/detail/{work_item_id}
```

### ⚠️ 双数据源 id 字段不兼容（2026-08-10 修复）

客诉有两个数据源，输出字段名不同：
- `api_server.py` → 输出 `id`（映射自 `work_item_id`）
- `mcp_complaints.json`（MCP cron 管道）→ 输出 `work_item_id`（无 `id` 字段）

前端渲染必须兼容两者：
```javascript
// ✅ 防御性：优先 work_item_id，回退 id
'https://project.feishu.cn/br-shgl/khts/detail/' + (c.work_item_id || c.id || '')

// ❌ 只用 c.id → mcp_complaints.json 数据源下所有客诉链接变为 /detail/undefined
'https://project.feishu.cn/br-shgl/khts/detail/' + c.id
```

**影响文件**：`工具/index.html` L2207、`工具/app.html` L1538。修复后需 `tcb hosting deploy` 部署到 CloudBase。

**自动化**：`mcp_refresh.py` 第 2 步改为直接调用 `pull_complaints.py`，下次 `cb_sync.sh`（每 2h）自动刷新。

### 客诉 operator 字段：必须是数组，字符串会崩页面（2026-07-27 血案）

`renderComplaints` 对 `c.operator` 调用了 `.forEach()`（收集处理人列表）和 `.join(', ')`（渲染表格）。如果 `operator` 是字符串（如 `"刘欣, 匡婷"`），这些调用会抛 `TypeError`，**整个渲染函数中断 → 页面点不动/空白无报错**。

**铁律**：`current_status_operator`（`user_value_list`）MUST 在管道中输出为数组：
```python
# ✅ 正确：输出 name_cn 数组
'operator': [x.get('name_cn', '') for x in ops] if isinstance(ops, list) else []

# ❌ 错误：逗号拼接字符串 → 页面 crash
'operator': ', '.join(x.get('name_cn', '') for x in ops)
```

**症状识别**：切换客诉 tab 后页面不响应（点不动、无列表、无报错） → `grep -c "operator.*join" page.html` 确认页面侧调用了 `.join()`，再 `python3 -c "import json; ..."` 确认 `mcp_complaints.json` 中 `operator` 的类型。

### 客诉隔离逻辑演进（2026-07-27 终版：按创建者）

最终隔离逻辑：**非 admin 只显示自己创建的客诉**（`ci.creator === userName`），不再按 area 或客户归属。验证结果：赵云浩 313→35 条。

```javascript
var userName = (state.user || {}).name;
if (userName && ((state.user || {}).role !== 'admin')) {
  items = items.filter(function(ci) { return ci.creator === userName; });
}
```

### 订单隔离逻辑演进（2026-07-27 终版→2026-07-29 升级：按 creatorEmail）

**终版隔离逻辑**：`getShipmentOrders()` 对非 admin 按 `o.creatorEmail === userEmail` 过滤。`creatorEmail` 比 `creator`（中文名）更可靠——订单数据中 `creatorEmail` 覆盖率 100%（2033/2033），且与登录邮箱完全一致。

```javascript
function getShipmentOrders() {
  var orders = (shipments.orders || []).slice();
  // 非管理员：按 creatorEmail 精确匹配
  if (!isAdminUser() && state.user && state.user.email) {
    var userEmail = state.user.email;
    orders = orders.filter(function(o) { return o.creatorEmail === userEmail; });
    // 同时修正 fullYearTax 为个人累计
    fullYearTax = orders.reduce(function(s, o) { return s + (o.taxTotal || o.amount || 0); }, 0);
  }
  return { orders: orders, updated: shipments.updated_at || '', h1Summary: h1Summary, fullYearTax: fullYearTax };
}
```

**为什么不用 `o.creator`（中文名）**：中文名可能重名、可能与其他字段不一致。`creatorEmail` 是唯一标识。

**为什么不用 `USER_DEPTS`（部门映射）**：部门映射需要手动维护，新人加入容易遗漏。`creatorEmail` 自动生效，无需维护映射表。

### 新旧登录格式兼容层（2026-07-29）

`state.user` 在旧硬编码登录是字符串（`'zhaoyunhao'`），在新 API 登录是对象（`{name, role, dept, email, tabs}`）。直接 `USER_DEPTS[state.user]` 或 `AUTH[state.user]` 对对象格式必崩。

**统一兼容函数**（在文件顶部 `USER_DEPTS` 后定义）：

```javascript
function getUserName() {
  if (!state.user) return '';
  if (typeof state.user === 'object') return state.user.name || '';
  return state.user;
}
function getUserRole() {
  if (!state.user) return '';
  if (typeof state.user === 'object') return state.user.role || 'sales';
  return state.user;
}
function isAdminUser() {
  var r = getUserRole();
  return r === 'admin' || r === 'gm' || r === 'director';
}
```

**用法**：所有需要判断权限或获取用户名的地方，统一用这三个函数替代直接访问 `state.user`。

### 订单管理 Tab 精简模式（2026-07-29）

**用户需求**：订单管理 Tab 仅保留订单明细表格 + 筛选器 + 查看全部订单按钮，删除所有分析面板（统计卡片/客户Top5/订单类型占比/物流异常/按部门/签收弹窗/飞书底栏/分析周期切换）。

**保留内容**：
- 📊 订单管理 标题 + 更新时间
- 📦 订单明细 标题 + 📋 查看全部订单 → 飞书链接
- 筛选栏：日期范围/部门(多选)/下单人(多选)/状态(多选)/搜索框
- 订单表格（可点击展开物流轨迹）
- `renderShipmentDetail()` + `toggleShipmentDetail()`

**删除内容**：
- `orderState.analysisPeriod` + 本周/本月/本季/本年切换
- 统计卡片（发货/价税合计/运输中/已签收/物流异常/暂无路由）
- 三栏分析（客户Top5/订单类型占比/物流异常）
- 按部门柱状图
- 备货状态统计
- 签收弹窗（newDelivered + closeDeliveryModal）
- 飞书链接底栏

**适用文件**：`工具/index.html`（CloudBase 部署入口）和 `工具/销售工作台.html`（本地入口）。

### 三年逐月数据用户隔离（2026-07-29）

**问题**：三年逐月数据（`three_year_monthly.json`）是公司级聚合，无 per-user 版本。非 admin 用户看到全公司数据。

**方案**：在 `renderThreeYearMonthly()` 中按用户年度销售额占比分摊：
```javascript
// 非管理员：从客户数据计算用户年度占比，分摊到各月
var custs = getFilteredCustomers();
var u24 = custs.reduce(function(s, c) { return s + (c.sales_2024 || 0); }, 0);
var r24 = t24 ? u24 / t24 : 0;  // 用户占公司比例
// 用比例乘公司月度数据
d['2024'][String(m)] = Math.round(((src['2024']||{})[String(m)] || 0) * r24);
```

**精度**：全年合计精准（来自 `_key_customers.json` 的 `sales_2024/2025/2026` 汇总），月度分布按公司趋势等比缩放。如需精确月度数据需从 ERP 原始订单按用户聚合。

### 新增用户上线检查清单（2026-07-29）

新增销售员时逐项验证，缺一不可：

| # | 检查点 | 命令/方法 |
|---|--------|----------|
| 1 | `_personnel.json` 存在且在职 | `grep <name> _personnel.json` |
| 2 | `DEFAULT_USERS` 有对应邮箱 | `grep <email> 销售工作台服务.py` |
| 3 | 订单数据有 `creatorEmail` | Python 查 `shipment_status.json` 中该邮箱订单数 |
| 4 | `diag_kpis.json` 有年度目标 | 手动确认 `annual_target ≠ 0` |
| 5 | `diag_salespeople.json` region 一致 | 与 `_personnel.json` 对比 |
| 6 | 工作台 HTML PERSONNEL region 一致 | 与 `_personnel.json` 对比 |
| 7 | API 登录可通 | `curl -X POST localhost:8765/api/login` |
| 8 | **`_key_customers.json` 包含该销售员客户** | 从作战室 `diag_customers.json` 提取后合并 → 运行 `merge_workbench_data.py` → 部署 |
| 9 | 浏览器实测 | 登录 → 订单管理 → 确认金额为个人数据（非全公司） |

**第 8 项是关键**：`_key_customers.json` 是看板 Top10 + 客户 Tab 的数据源。新人无此条目 → 客户 Top10 为空。需从 `diag_customers.json` 提取客户并合并入 `_key_customers.json`，再运行 `merge_workbench_data.py` 重新生成 `workbench_data.json` 并部署。

### 客户隔离逻辑演进（2026-07-27 终版：按 `current_status_operator` 匹配）

最终隔离方案：从 `current_status_operator` 提取所有操作人名称数组，非 admin 只要当前用户在数组中即显示：

**adaptMcpFields 新增 `operators` 数组**：
```javascript
operators: (function() {
  var ops = c.current_status_operator;
  if (Array.isArray(ops)) return ops.map(function(x){return x.name_cn||x.name||'';});
  if (ops && typeof ops === 'object') return [ops.name_cn || ops.name || ''];
  return [];
})(),
```

**过滤逻辑**：
```javascript
var userName = (state.user || {}).name;
if (userName && ((state.user || {}).role !== 'admin')) {
  all = all.filter(function(c){
    return c.owner === userName || (c.operators || []).indexOf(userName) >= 0;
  });
}
```

**数据源**：`pull_customers.py` 的 COLUMNS 必须包含 `current_status_operator`，MQL 返回 `user_value_list` 格式。验证结果：赵云浩 1680→63 条（包含所有 FP 分配给他的客户）。

### 飞书客户链接格式修正

客户详情页在飞书中的正确 URL 格式不是 `baoyu/workitem/{id}`，而是：
```
https://project.feishu.cn/xsguanli/kh/detail/{work_item_id}
```
定义专用常量 `FEISHU_CUSTOMER_URL = 'https://project.feishu.cn/xsguanli/kh/detail/'`。

`field_ac5caf`（客户，`workitem_related_multi_select`）在 MQL 中返回 `key_label_value_list` 格式：
```json
[{"key": "4203833184", "label": "湖南国测生物科技有限公司"}]
```
这与 `select` 的 `key_label_value`（单个对象）不同，是数组。

**管道侧**（pull_complaints.py）：
```python
# COLUMNS 加上 field_ac5caf
COLUMNS = '..., field_ac5caf'

# parse_items 处理 key_label_value_list
if isinstance(v, list) and len(v) > 0:
    if isinstance(v[0], dict) and 'label' in v[0]:
        fields[k] = v[0]['label']; continue
```

**页面侧**：客诉隔离从按 `c.area` 改为按 `客诉客户名 → MCP客户dept → 匹配用户dept`：
```javascript
// 构建 客户名→dept 映射
var mcpAll = (state.data.mcpCustomers || {}).customers || [];
var custDeptMap = {};
mcpAll.forEach(function(cs) { custDeptMap[cs.name] = cs.dept || ''; });
items = items.filter(function(ci) {
  var cn = ci.customer || '';
  if (!cn && ci.name) { var p = ci.name.split('-'); if (p.length >= 2) cn = p[1]; }
  return userDepts.indexOf(custDeptMap[cn] || '') >= 0;
});
```

> 为什么不能按 `area`：area 是客诉登记时手动填的区域标签，与客户实际归属的部门可能不一致。按客户→部门才是正确归属。

### 新增字段可能是空的（MCP 已配置但 IT 未填）

`field_8c870a`（联系人数量）在字段配置中存在，但 MQL 拉回的 1680 条客户中**全部为空**（0 条有值）。这不是 bug——IT 可能在飞书项目中为字段加了配置但尚未填充数据。

**识别方法**：新增字段映射后，先用 `pull_customers.py` 拉取一次 → `python3 -c "sum(1 for c in d['customers'] if c.get('field_xxx'))"` 确认填充率 → 全空则告知用户联系 IT。
