# 工作台文件上传管道（2026-08-07）

## 架构

```
浏览器(admin) → drag/drop → POST /api/upload (multipart)
  → 保存到 数据/上传/
  → 返回文件元数据 + 自动类型分类

浏览器 → POST /api/upload/process {file: saved_name}
  → ① 质检: 检测文件类型、校验关键列
  → ② 智能月份检测: 从单据编号前缀(ZHBR2607=7月/ZHBR2608=8月)识别月份
  → ③ 按月份路由:
      当前月 → kpi_progress.json
      历史月 → three_year_monthly.json + kpi_dashboard.json
  → ④ 合并 + CloudBase部署
  → ⑤ 归档到 _已处理归档/
```

## 后端接口（`脚本/销售工作台服务.py`）

### POST /api/upload
- 接收 multipart/form-data
- 按扩展名自动分类：`.xlsx/.xls/.csv` → ERP/飞书导出，`.docx` → 文档
- 保存到 `数据/上传/{timestamp}_{filename}`
- 写入 `upload_log.json`

### GET /api/uploads
- 返回最近 100 条上传记录

### POST /api/upload/process
- 三步管线：质检 → 处理 → 归档
- 仅在处理成功时归档，失败文件保留在上传目录

## 质检规则 (`run_quality_check`)

| 检测项 | 方法 |
|--------|------|
| 文件类型 | 扩展名 .xlsx/.xls/.csv |
| ERP订单 | 列含"单据编号"+"价税合计" |
| 飞书活动 | 列含"活动"/"创建者" |
| 关键列 | 单据编号、价税合计必须存在 |

**⚠️ openpyxl read_only=True 陷阱**：
- `read_only=True` 时，`iter_rows()` 返回 `ReadOnlyCell` 对象
- 只遍历第一行会只返回第一列 → 质检误报"1列"
- **修复**：去掉 `read_only=True`，使用 `values_only=True`
```python
# ❌ 错误
wb = openpyxl.load_workbook(path, data_only=True, read_only=True)
headers = [str(c.value) for c in next(ws.iter_rows(min_row=1, max_row=1))]  # 只有1列!

# ✅ 正确
wb = openpyxl.load_workbook(path, data_only=True)
headers = [str(c) for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
```

## 智能月份路由 (`process_uploaded_file`)

### 月份检测
```python
# 方法1: 从「单据编号」前缀采样前100行
for row in rows_list[:100]:
    bn = row.get('单据编号', '')
    if bn.startswith('ZHBR') and len(bn) >= 10:
        ym = '20' + bn[4:6] + '-' + bn[6:8]  # ZHBR2607xxx → 2026-07

# 方法2: 从文件名解析
m = re.search(r'(2026\d{2})', saved_name)  # 20260701-0731 → 2026-07
```

### 路由逻辑
- `detected_month == current_month` → `kpi_progress.json`（当月KPI）
- `detected_month != current_month` → `three_year_monthly.json` + `kpi_dashboard.json`（历史月校准）

## 合计行过滤

ERP导出的最后一行是合计行，特征：
- `合同日期`列 = `"合计"`
- `单据编号`列 = `None`

**⚠️ `str(None) = 'None'` 陷阱**：
- `not bn` 不会过滤 `'None'`（非空字符串）
- 必须显式过滤：`if not bn or bn == 'None' or '合计' in bn`

```python
bn = str(row.get(bn_col, '') or '').strip()
if not bn or bn == 'None' or '合计' in bn: continue  # 跳过合计行
```

## 前端集成（`工具/index.html`）

### Tabs 注册
```javascript
{ id: 'dataMgmt', label: '数据管理', icon: '📤', lines: ['da','ls'], adminOnly: true }
```

### adminOnly 过滤
```javascript
if (t.adminOnly && u.role !== 'admin') return false;  // renderNav中
```

### 列表分页
- 默认显示前 10 条
- 超过 10 条显示"展开剩余 N 条"，点击展开全部
- 可收起回到 10 条视图

### 处理按钮
- 仅 `ERP/飞书导出` 类型显示 ⚡处理 按钮
- 点击后三步管线，toast 显示每步状态

## 部署

```bash
# 重启服务
kill $(lsof -ti :8765)
cd ~/Desktop/Hermes输出-工作类
python3 脚本/销售工作台服务.py 8765 &

# 部署前端
tcb hosting deploy 工具/index.html /workbench/index.html -e bier-sales-d0gatbvlx288724e9
```
