Files

240 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 已记录) |