backup: 价格bug修复 + 订单批量识别设计文档
This commit is contained in:
+32
-10
@@ -178,18 +178,30 @@ class ProductDatabase:
|
||||
try:
|
||||
row = conn.execute("SELECT avg_price FROM products WHERE barcode=?",
|
||||
(str(barcode).strip(),)).fetchone()
|
||||
return row[0] if row and row[0] else None
|
||||
if not row:
|
||||
return None
|
||||
return row[0] if row[0] is not None else 0.0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_prices(self, barcodes: List[str]) -> Dict[str, float]:
|
||||
def get_prices(self, barcodes: List[str], column: str = 'price') -> Dict[str, float]:
|
||||
"""批量查进货价。
|
||||
|
||||
Args:
|
||||
barcodes: 条码列表
|
||||
column: 价字段,默认 'price'(Excel 导入的进货价,按商家主单位),
|
||||
可选 'avg_price'(OCR 学来的历史均价,调试用)。
|
||||
"""
|
||||
if not barcodes:
|
||||
return {}
|
||||
valid = {'price', 'avg_price', 'min_price', 'max_price'}
|
||||
if column not in valid:
|
||||
raise ValueError(f"column must be one of {valid}")
|
||||
conn = self._connect()
|
||||
try:
|
||||
placeholders = ','.join('?' * len(barcodes))
|
||||
rows = conn.execute(
|
||||
f"SELECT barcode, avg_price FROM products WHERE barcode IN ({placeholders})",
|
||||
f"SELECT barcode, {column} FROM products WHERE barcode IN ({placeholders})",
|
||||
[str(b).strip() for b in barcodes]).fetchall()
|
||||
return {r[0]: r[1] for r in rows if r[1]}
|
||||
finally:
|
||||
@@ -357,13 +369,23 @@ class ProductDatabase:
|
||||
else:
|
||||
# 高可信度源全字段覆盖;低可信度仅填空
|
||||
if source in ('template', 'user_confirmed') or new_conf > 50:
|
||||
conn.execute(
|
||||
"UPDATE products SET name=?, specification=?, unit=?, price=?, "
|
||||
"source=?, confidence=?, usage_count=?, last_seen=?, updated_at=?, "
|
||||
"avg_price=?, min_price=?, max_price=?, price_count=? WHERE barcode=?",
|
||||
(name or old_name, spec or old_spec, unit or old_unit, price,
|
||||
source, new_conf, new_count, now, now,
|
||||
new_avg, new_min, new_max, new_pc, barcode))
|
||||
# OCR 来源不覆盖 price(price 是商品资料 Excel 导入的进货价,应作为基准不被 OCR 学习的"按箱单价"覆盖)
|
||||
if source == 'ocr':
|
||||
conn.execute(
|
||||
"UPDATE products SET name=?, specification=?, unit=?, "
|
||||
"source=?, confidence=?, usage_count=?, last_seen=?, updated_at=?, "
|
||||
"avg_price=?, min_price=?, max_price=?, price_count=? WHERE barcode=?",
|
||||
(name or old_name, spec or old_spec, unit or old_unit,
|
||||
source, new_conf, new_count, now, now,
|
||||
new_avg, new_min, new_max, new_pc, barcode))
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE products SET name=?, specification=?, unit=?, price=?, "
|
||||
"source=?, confidence=?, usage_count=?, last_seen=?, updated_at=?, "
|
||||
"avg_price=?, min_price=?, max_price=?, price_count=? WHERE barcode=?",
|
||||
(name or old_name, spec or old_spec, unit or old_unit, price,
|
||||
source, new_conf, new_count, now, now,
|
||||
new_avg, new_min, new_max, new_pc, barcode))
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE products SET "
|
||||
|
||||
+13
-23
@@ -352,29 +352,8 @@ class ExcelProcessor:
|
||||
product['package_quantity'] = package_quantity
|
||||
logger.info(f"解析已设置的规格: {product['specification']} -> 包装数量={package_quantity}")
|
||||
|
||||
# 新增逻辑:根据规格推断单位为"件"
|
||||
if not product['unit'] and product.get('barcode') and product.get('specification') and product.get('quantity') and product.get('price') is not None:
|
||||
# 检查规格是否符合容量*数量格式
|
||||
volume_pattern = r'(\d+(?:\.\d+)?)\s*(?:ml|[mL]L|l|L|升|毫升)[*×xX](\d+)'
|
||||
match = re.search(volume_pattern, product['specification'])
|
||||
|
||||
# 判断是否需要推断单位为"件"
|
||||
if match:
|
||||
product['unit'] = '件'
|
||||
logger.info(f"根据规格推断单位: {product['specification']} -> 单位=件")
|
||||
else:
|
||||
# 检查简单的数量*数量格式
|
||||
simple_pattern = r'(\d+)[*×xX](\d+)'
|
||||
match = re.search(simple_pattern, product['specification'])
|
||||
if match:
|
||||
product['unit'] = '件'
|
||||
logger.info(f"根据规格推断单位: {product['specification']} -> 单位=件")
|
||||
|
||||
# 应用单位转换规则
|
||||
product = self.unit_converter.process_unit_conversion(product)
|
||||
|
||||
# 如果数量为0但单价和金额都存在,计算数量 = 金额/单价
|
||||
if (product['quantity'] == 0 or product['quantity'] is None) and product['price'] > 0 and product['amount']:
|
||||
# 如果数量为0但单价和金额都存在,先算数量(必须在 unit 推断之前,否则 unit 推断条件因 quantity=0 被跳过)
|
||||
if (product['quantity'] == 0 or product['quantity'] is None) and product['price'] and product['amount']:
|
||||
try:
|
||||
amount = parse_monetary_string(product['amount'])
|
||||
if amount is not None and amount > 0:
|
||||
@@ -383,6 +362,17 @@ class ExcelProcessor:
|
||||
product['quantity'] = quantity
|
||||
except Exception as e:
|
||||
logger.warning(f"通过金额和单价计算数量失败: {e}")
|
||||
|
||||
# 推断单位为"件":仅依赖规格文本(spec 已经是 "1*N"),不再要求 quantity 非零
|
||||
if not product['unit'] and product.get('specification'):
|
||||
volume_pattern = r'(\d+(?:\.\d+)?)\s*(?:ml|[mL]L|l|L|升|毫升)[*×xX](\d+)'
|
||||
simple_pattern = r'(\d+)[*×xX](\d+)'
|
||||
if re.search(volume_pattern, product['specification']) or re.search(simple_pattern, product['specification']):
|
||||
product['unit'] = '件'
|
||||
logger.info(f"根据规格推断单位: {product['specification']} -> 单位=件")
|
||||
|
||||
# 应用单位转换规则
|
||||
product = self.unit_converter.process_unit_conversion(product)
|
||||
|
||||
# 应用记忆库补全
|
||||
product = self._apply_memory(product)
|
||||
|
||||
+1
-2
@@ -34,7 +34,7 @@ purchase_order = 银豹-采购单模板.xls
|
||||
item_data = 商品资料.xlsx
|
||||
|
||||
[App]
|
||||
version = 2026.05.05.0239
|
||||
version = 2026.07.19.1522
|
||||
|
||||
[Gitea]
|
||||
base_url = https://gitea.94kan.cn
|
||||
@@ -45,4 +45,3 @@ token =
|
||||
[WebAuth]
|
||||
username = admin
|
||||
password_hash = $2b$12$nllT8o1QIMfWKuTlpQI3G./E2NS.gqf0EHZyNkJ8gMpVa9grTXRoC
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# ALIGNMENT — 订单批量识别(OCR 元信息提取 + 批量一键处理)
|
||||
|
||||
## 一、目标
|
||||
|
||||
完善 OCR → result 完整链路,新增 3 个能力:
|
||||
1. **OCR 元信息识别与持久化**:从图片 OCR 原文识别供应商、单据日期、应付金额,存 SQLite
|
||||
2. **基于元信息的文件命名**:result 文件 + 原图片按"采购单_YYYYMMDD_供应商_<hash>.xls"格式命名
|
||||
3. **批量一键处理**:扫描 `data/input/` 内所有图片,批量跑完整流程,每张独立出 result(默认不合并)
|
||||
|
||||
## 二、需求边界(已与用户确认)
|
||||
|
||||
### 必做
|
||||
- OCR 后从原始 OCR 文本识别 3 个字段:
|
||||
- **供应商名称**:从 OCR 文本顶部识别(关键词:供货/供应/酒行/商行/批发等,取匹配行原文)
|
||||
- **单据日期**:从 OCR 文本识别(关键词:日期/单据日期/年月日;正则提取 YYYY-MM-DD / YYYY/MM/DD / YYYY.MM.DD / YYYYMMDD 等多种格式)
|
||||
- **总金额**:取"应付金额/实付金额/金额合计"字段;如无则取"总计"行最大金额
|
||||
- 三个字段存到新增 SQLite 表 `order_metadata`(与现有 `product_cache.db` 共用文件)
|
||||
- result 文件名格式:`采购单_{YYYYMMDD}_{供应商名}_{原文件hash}.xls`
|
||||
- 原图片(input 目录)重命名为:`原名_{YYYYMMDD}_{供应商名}_{hash}.{原扩展名}`(保留扩展名,hash 不变)
|
||||
- 批量处理:`data/input/` 内全部图片 → OCR → process_excel → result;输出 `data/result/采购单_*.xls`
|
||||
- 完整流程跑完,日志末尾显示 3 个参数汇总(按文件列出供应商/日期/金额 + 成功/失败状态)
|
||||
- 三个字段任一识别失败 → 填"未知",文件后缀仍含原 hash 保证唯一性
|
||||
|
||||
### 不做
|
||||
- 不修改现有 fill_template 模板
|
||||
- 不改商品资料库 / 条码映射 / 单位转换逻辑
|
||||
- 不合并多个订单为单个 result(用户明确"默认全部不合并")
|
||||
- 不做供应商库匹配(图片原文识别优先)
|
||||
- 不做 OCR 失败重试机制增强(沿用现有 OCRService.process_image)
|
||||
|
||||
### 已确认决策
|
||||
| 决策点 | 选择 |
|
||||
|---|---|
|
||||
| 供应商识别来源 | OCR 原文顶部文本(关键词匹配) |
|
||||
| 总金额字段 | 应付金额/实付/金额合计,缺则取总计行最大金额 |
|
||||
| 日期格式 | YYYYMMDD 作文件名后缀 |
|
||||
| 识别失败默认值 | "未知供应商" + 原 hash |
|
||||
| Result 输出位置 | `data/result/`(不分子目录) |
|
||||
|
||||
## 三、对现有项目的影响
|
||||
|
||||
### 需要修改的文件
|
||||
- `app/core/ocr/table_ocr.py` 或新增 `app/core/ocr/metadata_extractor.py`:识别 3 个字段
|
||||
- `app/core/db/product_db.py`:新增 `order_metadata` 表及 CRUD
|
||||
- `app/services/order_service.py`:在 process_excel 流程里提取元信息 + 落库 + 应用新文件名
|
||||
- `app/ui/main_window.py` / `action_handlers.py`:新增"批量处理"按钮 + 进度条 + 完成后日志汇总
|
||||
- `app/services/processor_service.py`:新增批量入口 `process_all_inputs`
|
||||
|
||||
### 新增文件
|
||||
- `app/core/ocr/metadata_extractor.py`:单据元信息识别器
|
||||
- `tests/test_metadata_extractor.py`:单元测试
|
||||
|
||||
### 不需要修改
|
||||
- 模板、银豹格式、单位转换、记忆库核心逻辑
|
||||
|
||||
## 四、验收标准
|
||||
|
||||
### 功能验收
|
||||
- [ ] OCR 后能识别出供应商/日期/应付金额(真实单子样本)
|
||||
- [ ] 三个字段存到 `data/product_cache.db` 的 `order_metadata` 表
|
||||
- [ ] result 文件名 = `采购单_YYYYMMDD_供应商_<hash>.xls`
|
||||
- [ ] 原图片在 input 中按相同规则重命名(保留扩展名)
|
||||
- [ ] 一键处理按钮能扫描 `data/input/` 全部图片,串行处理(避免并发占资源)
|
||||
- [ ] 完整流程跑完,日志显示每张的处理结果 + 3 个参数 + 状态
|
||||
- [ ] 全部 191 个现有测试不破坏
|
||||
|
||||
### 边界验收
|
||||
- [ ] 三个字段都识别不到时,使用"未知"+原 hash
|
||||
- [ ] 部分识别失败,文件名仍包含原 hash 保证唯一性
|
||||
- [ ] `data/input/` 为空时,按钮点击给出"无待处理图片"提示
|
||||
|
||||
## 五、不确定点(已澄清或可后续处理)
|
||||
|
||||
- **OCR 文本顶部区域范围**:取 OCR 返回的前 20 行文本(覆盖常见采购单抬头区域),如有"供应商"明确标注则取该标注
|
||||
- **日期格式多样性**:正则同时支持 4 种格式,标准化为 YYYYMMDD
|
||||
- **并发处理**:本期串行(10 张单子每张 1~2 秒,串行 10~20 秒可接受);并发留待后续
|
||||
- **历史 result 重命名**:本期**不**迁移历史 result(避免破坏现有用户数据)
|
||||
|
||||
## 六、后续文档
|
||||
- DESIGN_订单批量识别.md(架构设计)
|
||||
- TASK_订单批量识别.md(任务拆分)
|
||||
@@ -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 已记录) |
|
||||
@@ -0,0 +1,106 @@
|
||||
# TASK — 订单批量识别(OCR 元信息提取 + 批量一键处理)
|
||||
|
||||
## 任务依赖图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
T1[任务1: metadata_extractor] --> T4[任务4: order_service 集成]
|
||||
T2[任务2: order_metadata 表] --> T4
|
||||
T3[任务3: OCR meta.json 落盘] --> T4
|
||||
T4 --> T5[任务5: 文件命名 + 重命名原图]
|
||||
T5 --> T6[任务6: processor_service 批量入口]
|
||||
T6 --> T7[任务7: UI 一键处理按钮 + 进度]
|
||||
T1 --> T8[任务8: 单元测试]
|
||||
T2 --> T8
|
||||
```
|
||||
|
||||
## 原子任务列表
|
||||
|
||||
### 任务 1:metadata_extractor.py
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | ocr_text (str)、ocr_rows (list[list[str]]) |
|
||||
| 输出契约 | OrderMetadata(supplier, bill_date, total_amount, raw_supplier_text) |
|
||||
| 实现约束 | 供应商关键词清单 + 启发式;日期正则;金额关键词 |
|
||||
| 验收标准 | 单元测试覆盖:完整三字段、部分缺失、OCR 噪声、日期格式 4 种 |
|
||||
| 依赖 | 无 |
|
||||
|
||||
### 任务 2:order_metadata 表 + CRUD
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | ProductDatabase 实例 |
|
||||
| 输出契约 | save_order_metadata / get_order_metadata / list_all_metadata |
|
||||
| 实现约束 | 建表 IF NOT EXISTS;幂等 save(PK = file_hash) |
|
||||
| 验收标准 | 测试创建/查询/更新/列出 |
|
||||
| 依赖 | 无 |
|
||||
|
||||
### 任务 3:OCR meta.json 落盘
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | OCRService.process_image 返回的 (excel_path, raw_text) |
|
||||
| 输出契约 | data/output/{hash}.meta.json 含 ocr_text |
|
||||
| 实现约束 | 修改 table_ocr.py 返回原始 text;OCRService 调用方写 meta.json |
|
||||
| 验收标准 | OCR 一次后 meta.json 存在且含完整 text |
|
||||
| 依赖 | 无 |
|
||||
|
||||
### 任务 4:order_service.process_excel 集成元信息
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | excel_path |
|
||||
| 输出契约 | (new_result_path, metadata) |
|
||||
| 实现约束 | 读 meta.json → extract → save → 应用新 result 文件名 |
|
||||
| 验收标准 | 跑真实 OCR 文件后 result 文件名符合新规则 |
|
||||
| 依赖 | T1、T2、T3 |
|
||||
|
||||
### 任务 5:原图重命名
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | (原图路径, metadata, file_hash) |
|
||||
| 输出契约 | 新图路径(已重命名) |
|
||||
| 实现约束 | 保留扩展名;清理非法字符;冲突加 _N |
|
||||
| 验收标准 | 处理后 input 中文件名带日期/供应商/hash |
|
||||
| 依赖 | T4 |
|
||||
|
||||
### 任务 6:processor_service.process_all_inputs
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | 无 |
|
||||
| 输出契约 | dict(total, success, failed, results) |
|
||||
| 实现约束 | 扫描 data/input/ 全部图片;串行;现有 processed_files.json 不重做 |
|
||||
| 验收标准 | input 放 3 张图 → 处理后 results 长度=3;status 准确 |
|
||||
| 依赖 | T4、T5 |
|
||||
|
||||
### 任务 7:UI 一键处理按钮 + 进度
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | UI 按钮点击事件 |
|
||||
| 输出契约 | 进度对话框 + 主窗口日志更新 + 完成 MessageBox |
|
||||
| 实现约束 | 后台线程 + UI 通信用 queue;进度不可关闭 |
|
||||
| 验收标准 | 点击按钮 → 进度可见 → 完成有汇总 |
|
||||
| 依赖 | T6 |
|
||||
|
||||
### 任务 8:单元测试
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 输入契约 | 测试用例集 |
|
||||
| 输出契约 | 191 + 新测试 全部 pass |
|
||||
| 实现约束 | tests/test_metadata_extractor.py;tests/test_order_metadata.py |
|
||||
| 验收标准 | `pytest tests/` 全绿 |
|
||||
| 依赖 | T1、T2 |
|
||||
|
||||
## 工作量评估
|
||||
|
||||
- T1:30 分钟(算法 + 10 个测试用例)
|
||||
- T2:15 分钟
|
||||
- T3:20 分钟
|
||||
- T4:30 分钟
|
||||
- T5:20 分钟
|
||||
- T6:30 分钟
|
||||
- T7:40 分钟(UI 线程)
|
||||
- T8:30 分钟
|
||||
- 测试与集成:30 分钟
|
||||
|
||||
## 交付物
|
||||
- 8 个原子任务全部通过
|
||||
- `docs/订单批量识别/FINAL_订单批量识别.md`:项目总结
|
||||
- `docs/订单批量识别/TODO_订单批量识别.md`:待办清单
|
||||
Reference in New Issue
Block a user