backup: 价格bug修复 + 订单批量识别设计文档

This commit is contained in:
2026-07-19 16:17:12 +08:00
parent 968a6f8d22
commit cc66448327
6 changed files with 473 additions and 35 deletions
@@ -0,0 +1,240 @@
# 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 已记录) |