# 核销表 · 第12环 归档

> 90分级验收（孙总方法论·硬门禁）。已吸收独立交叉验证修正（⚠️）。auto=机器判定，human=目视。

## 模块目标
①history 按日期/品类筛选(现平铺) ②approved 下载交付包 zip 结构完整 ③多套打包(1-3套各独立笔记夹) ④客户输入物(产品/标杆)可导出(数据归属红线)。

## Ralph 修改步骤

| # | 改什么 | 落点 |
|---|--------|------|
| S1 | list_tasks 加 date_from/date_to/product_id query param；fromisoformat 转换，invalid 返回 40001 | tasks.py:137-149 |
| S2 | _fmt_task 加 product_name(调用方传入)；list_tasks 一次性 `Product.id.in_([...])` 建 id→name dict 避免N+1；⚠️顶部补 `from app.models.product import Product`(当前无此import) | tasks.py:58-64 |
| S3 | history/page.tsx 加 dateFrom/dateTo state + 两个 `<input type=date>`(提 DateRangeFilter 子组件防超100行) | history/page.tsx:28-44 + JSX 50-94 |
| S4 | HistoryTable 表头/行加"产品"列(显 product_name ?? '-') | history/components.tsx:70-127 |
| S5 | TaskListItem 加 `product_name?:string` | dto.tasks.ts:8-16 |
| S6 | ⚠️多套打包：查所有 is_selected=True TextCandidate 按 angle_label 分组每组取第一；notes 列表长度=选中套数(改 line 77-85 为 for loop)；images_data 循环外查一次，每套传 `list(images_data)` 副本(退化:图共享复制进各套夹) | packaging_task.py:45-47,77-85 |
| S7 | 新建 GET /products/export + /benchmarks/export(format=json/csv)；require_write_permission；⚠️benchmarks/export 需 Product JOIN 填 product_name(复用 _fmt_benchmark 不够,只有 product_id) | products.py + benchmarks.py |
| S8 | 配置页产品 tab + 历史归档页各加"导出数据"按钮，通用 ExportButton(url,filename) 调 getBlob 下载 | history/components.tsx 或 config/ExportTab.tsx |

## 验收清单（做完一个勾一个）

- [ ] **A1**〔auto〕日期筛选：传 date_from/date_to → items 全落区间；传 date_from=2099 → 空数组 total=0。⚠️审计补:再验 `date_from=not-a-date` → code=40001(非500)。
- [ ] **A2**〔auto〕product_id 筛选：传 product_id=1 → 每条 product_id=1 且 product_name 非空。
- [ ] **A3**〔human〕前端产品列：/history 表头有"产品"列，每行显产品名(非数字)，与 GET /products/{id}.name 一致。
- [ ] **A4**〔human〕日期控件：有开始/结束 date input，填后表格刷新，XHR URL 含 date_from/date_to，结果≤全量。
- [ ] **A5**〔human〕下载交付包：approved 任务点"下载交付包"，≤60s 触发下载 delivery-{id}.zip(>1KB)；`unzip -l` 含 note_01/文案.txt/发布清单/合规说明。
- [ ] **A6**〔auto〕zip 结构完整：`unzip -l` grep 文案.txt/发布清单/合规说明/note_01//.jpg 全非空。⚠️审计补:若 .jpg 空先查 ImageCandidate.url 文件容器内可达(packaging_task.py:64-70 读失败静默跳过)。
- [ ] **A7**〔auto〕文案.txt 格式：`unzip -p note_01/文案.txt` 含"【标题】"、不以 `{` 开头(JSON 已解析)。
- [ ] **A8**〔human+auto〕多套打包：⚠️审计改 human(需先 seed 3条 is_selected TextCandidate)→构造后 `unzip -l|grep 'note_0[1-3]/'|wc -l`≥3，三套标题互不相同。
- [ ] **A9**〔auto〕产品 JSON 导出：GET /products/export → 200,application/json,数组,每元素含 id/name/selling_points,且 selling_points 是数组(非JSON字符串)。
- [ ] **A10**〔auto〕标杆 CSV 导出：GET /benchmarks/export?format=csv → 200,text/csv,首行表头含 product_id/highlights/created_at,行数≥已录标杆数。
- [ ] **A11**〔auto〕导出权限：⚠️审计注——require_write_permission 当前只查 workspace_members 不分角色(workspace_guard.py:65-88)；未登录调 → code=40101，workspace 成员 → 200。
- [ ] **A12**〔human〕不显 token 余额(红线)：⚠️审计改 human(curl 拿不到Next.js水合后DOM)——浏览器 Ctrl+F 搜"余额/用量/tokens/balance"无结果。

## 待北哥定义（不阻断代码）
- 多套打包图片套间对应：ImageCandidate 无 set_index，是否加列关联文案？还是退化"图共享复制进每套夹"(本期默认退化)。
- 导出入口位置(产品管理 tab 按钮 / 历史归档页全局)。
- CSV 是否加 UTF-8 BOM(Windows Excel 不乱码)。
