# DESIGN — 订单批量识别（OCR 元信息提取 + 批量一键处理）

## 一、整体架构

```mermaid
graph TB
    A[扫描 data/input/*.png/jpg] --> B[主流程入口]
    B --> C[OCRService.process_image]
    C --> D[TableOCR 返回 Excel 数据]
    D --> E[metadata_extractor 识别三字段]
    E --> F[product_db.save_order_metadata]
    F --> G[重命名 input 原图片]
    G --> H[OrderService.process_excel]
    H --> I[ExcelProcessor.fill_template → 新文件名]
    I --> J[汇总日志]
```

## 二、模块设计

### 1. `app/core/ocr/metadata_extractor.py`（新增）

**核心类** `OrderMetadataExtractor`

```python
class OrderMetadataExtractor:
    def extract(ocr_text: str, ocr_rows: list[list[str]]) -> OrderMetadata:
        """从 OCR 原始文本和解析后的二维数组提取三字段。
        
        Returns:
            OrderMetadata(supplier, bill_date, total_amount, raw_supplier_text)
        """
```

**提取算法**：

#### Supplier（供应商）
1. 取前 20 行（OCR 顶部）
2. 行内关键词匹配：`供货单`、`供应商`、`供货方`、`批发`、`酒行`、`商行`、`经销`、`专卖店`、`配送单`、`送货单`
3. 命中后取该行清理（去数字、空白、特殊字符）后的文本
4. 多行匹配时取最长非空行
5. 兜底：取非空前 3 行中第一个中文 ≥ 4 字的行（启发式）

#### BillDate（单据日期）
1. 全文正则匹配，按优先级：
   - `(\d{4})[-./年](\d{1,2})[-./月](\d{1,2})日?` → 标准化 YYYYMMDD
   - `(\d{4})(\d{2})(\d{2})` → YYYYMMDD（紧邻的 8 位数字）
2. 多个匹配时取**第一个**
3. 校验：年 1900~2100，月 1~12，日 1~31

#### TotalAmount（应付金额）
1. 关键词优先：`应付金额`、`实付金额`、`金额合计`、`应付合计`、`合计金额`、`总计`
2. 取关键词后最近一个 `数字.数字` 或 `数字`
3. 兜底：取所有金额中最大值（排除单价列）

### 2. `app/core/db/product_db.py` 扩展

**新增表** `order_metadata`：

```sql
CREATE TABLE IF NOT EXISTS order_metadata (
    file_hash TEXT PRIMARY KEY,        -- OCR 输出 xlsx 的 hash，与原图同 hash
    supplier TEXT DEFAULT '',          -- 供应商
    bill_date TEXT DEFAULT '',          -- 单据日期 YYYYMMDD
    total_amount REAL DEFAULT 0,        -- 应付金额
    raw_supplier_text TEXT DEFAULT '',  -- 原始文本，便于回查
    source_image TEXT DEFAULT '',       -- 原图路径
    created_at TEXT DEFAULT '',
    updated_at TEXT DEFAULT ''
);
```

**新增方法**：
- `save_order_metadata(file_hash, supplier, bill_date, total_amount, raw_supplier_text, source_image)`
- `get_order_metadata(file_hash) -> dict | None`
- `list_all_metadata() -> list[dict]`（用于批量处理后的日志汇总）

### 3. `app/services/order_service.py` 扩展

**修改** `process_excel(excel_path)` 流程：

```python
def process_excel(self, excel_path):
    # ... 现有 fill_template 流程 ...
    
    # 1. 从 output/xxx.xlsx 的文件名提取 hash（现有逻辑已用）
    file_hash = Path(excel_path).stem
    
    # 2. 读 OCR 原始文本（来自 OCRService 缓存或从 xlsx 头部回读）
    ocr_text = self._read_ocr_text(file_hash)  # 新增
    
    # 3. 提取元信息
    meta = self.extractor.extract(ocr_text, ocr_rows)
    
    # 4. 落库
    self.product_db.save_order_metadata(file_hash, ...)
    
    # 5. 应用新文件名（result + 原图）
    new_result_name = f"采购单_{meta.bill_date or '未知'}_{meta.supplier or '未知供应商'}_{file_hash}.xls"
    # 替换 result_path
    # 原图重命名：找到 data/input/ 下对应的图（hash 一致）
    
    return new_result_path, meta
```

**OCR 文本获取策略**：
- OCR 写入 `data/output/{hash}.xlsx` 时，同步写 `data/output/{hash}.meta.json`（含 OCR 原始 text）
- 新增 `app/core/ocr/table_ocr.py` 写入 metadata 时 dump 原始 text
- 如 meta.json 不存在（历史文件），回退：从 xlsx 顶部 cell 拼文本

### 4. `app/services/processor_service.py` 扩展

**新增方法**：

```python
class ProcessorService:
    def process_all_inputs(self) -> dict:
        """扫描 data/input/ 下所有图片，串行处理。
        
        Returns:
            {
                'total': int,
                'success': int,
                'failed': int,
                'results': [
                    {'image': '...', 'hash': '...', 'status': 'success',
                     'supplier': '...', 'bill_date': '...', 'total_amount': '...',
                     'result_file': '...', 'error': None},
                    ...
                ]
            }
        """
```

**流程**：
1. 扫描 `data/input/{*.png,*.jpg,*.jpeg,*.bmp,*.pdf}`
2. 对每个图片：
   - 调 `OCRService.process_image`（已有）
   - 拿到 `data/output/{hash}.xlsx`
   - 调 `OrderService.process_excel`（已扩展）
   - 收集结果
3. 整体串行（避免百度 API 限流和数据库锁）
4. 返回汇总 dict

### 5. `app/ui/action_handlers.py` 扩展

**新增方法** `batch_process_all_inputs`：
1. 弹进度对话框（不可关闭，tk.Toplevel + progressbar + 实时日志 Text）
2. 在线程中调 `ProcessorService.process_all_inputs`
3. 完成后：
   - 在主窗口日志区显示汇总
   - 弹出 MessageBox 列出失败清单
   - 自动打开 `data/result/` 文件夹（可选）

**新增按钮**：`"一键处理全部图片"` 放在 GUI 主流程区，与现有"一键处理"按钮并列

## 三、关键接口契约

### OrderMetadata dataclass

```python
@dataclass
class OrderMetadata:
    supplier: str = ''
    bill_date: str = ''      # YYYYMMDD
    total_amount: float = 0.0
    raw_supplier_text: str = ''
    
    def is_complete(self) -> bool:
        return bool(self.supplier and self.bill_date)
```

### 文件命名

- **result xls**：`采购单_{YYYYMMDD}_{供应商}_{hash}.xls`
  - 供应商名清理：`/`、`\`、空格、`*`、`?`、`:`、`"`、`<`、`>`、`|` 替换为 `_`
  - hash 长度 32（与现有 md5 一致）
- **原图片**：`{原stem}_{YYYYMMDD}_{供应商}_{hash}.{原扩展名}`
  - 不覆盖已重命名的图片

### 数据库迁移

`ProductDatabase._connect` 中初始化时检查表存在并创建。新增方法：

```python
def _init_order_metadata_table(self, conn):
    conn.execute("""
        CREATE TABLE IF NOT EXISTS order_metadata (...)
    """)
```

## 四、依赖与时序

### 调用时序

```
main_window 点击 "一键处理全部图片"
  ↓
action_handlers.batch_process_all_inputs (UI 线程)
  ↓
processor_service.process_all_inputs (后台线程)
  ├─ for each image:
  │   ├─ ocr_service.process_image
  │   ├─ ocr_text 写到 data/output/{hash}.meta.json  ← 新增
  │   ├─ order_service.process_excel
  │   │   ├─ 读 {hash}.meta.json → OrderMetadata
  │   │   ├─ product_db.save_order_metadata
  │   │   ├─ 重命名 input 原图
  │   │   ├─ fill_template 到 临时名
  │   │   └─ 重命名到 采购单_YYYYMMDD_供应商_hash.xls
  │   └─ 收集结果
  └─ 返回汇总 dict
  ↓
action_handlers 显示日志汇总 + MessageBox
```

## 五、异常处理

| 场景 | 处理 |
|---|---|
| `data/input/` 为空 | UI 提示"无待处理图片"，不报错 |
| OCR 失败（网络/凭据） | 单张失败跳过，结果记 `error`，继续下一张 |
| process_excel 失败 | 同上 |
| 元信息识别失败 | supplier/bill_date 填空为"未知"，继续 |
| 原图重命名冲突 | 加 `_{N}` 后缀 |
| 数据库写入失败 | log 警告，不阻断流程 |

## 六、性能预期

- 单张图 OCR 1~3 秒 + 处理 0.5 秒 = 1.5~3.5 秒
- 10 张串行：15~35 秒（UI 显示进度条）
- 数据库写入 < 10ms/张，可忽略

## 七、风险与缓解

| 风险 | 缓解 |
|---|---|
| 供应商名称过长或包含特殊字符 | 清理规则 + 截断到 50 字符 |
| 日期格式五花八门 | 正则覆盖 4 种主流格式 + 校验范围 |
| OCR 文本被错误识别 | 保留 `raw_supplier_text` 便于回查修正 |
| 批量处理中程序崩溃 | 串行处理 + 已处理的不重做（processed_files.json 已记录） |