# 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 已记录) |