Files
orc-order-v2/docs/订单批量识别/DESIGN_订单批量识别.md
T

8.0 KiB
Raw Blame History

DESIGN — 订单批量识别(OCR 元信息提取 + 批量一键处理)

一、整体架构

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

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. 校验:年 19002100,月 112,日 1~31

TotalAmount(应付金额)

  1. 关键词优先:应付金额实付金额金额合计应付合计合计金额总计
  2. 取关键词后最近一个 数字.数字数字
  3. 兜底:取所有金额中最大值(排除单价列)

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

新增表 order_metadata

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) 流程:

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 扩展

新增方法

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

@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 中初始化时检查表存在并创建。新增方法:

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 13 秒 + 处理 0.5 秒 = 1.53.5 秒
  • 10 张串行:15~35 秒(UI 显示进度条)
  • 数据库写入 < 10ms/张,可忽略

七、风险与缓解

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