163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
"""批量处理服务:扫描 data/input/ 下图片,串行跑完整 OCR → 采购单流程。
|
||
|
||
- 不合并(每张图片单独出一个 result)
|
||
- 默认串行(避免百度 OCR 限流)
|
||
- 返回 dict 汇总:total / success / failed / results[]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Any, Callable, Dict, List, Optional
|
||
|
||
from app.config.settings import ConfigManager
|
||
from app.core.utils.log_utils import get_logger
|
||
from app.services.ocr_service import OCRService
|
||
from app.services.order_service import OrderService
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.bmp', '.webp', '.tif', '.tiff'}
|
||
|
||
|
||
class BatchService:
|
||
"""批量处理服务。"""
|
||
|
||
def __init__(self, config: Optional[ConfigManager] = None, missing_barcodes_cb: Optional[Callable[[List[str]], None]] = None):
|
||
self.config = config or ConfigManager()
|
||
self.missing_barcodes_cb = missing_barcodes_cb
|
||
self.ocr_service = OCRService(self.config)
|
||
self.order_service = OrderService(self.config, missing_barcodes_cb=self.missing_barcodes_cb)
|
||
self._input_folder = self.config.get_path(
|
||
'Paths', 'input_folder', fallback='data/input', create=True
|
||
)
|
||
|
||
@property
|
||
def input_folder(self) -> str:
|
||
return self._input_folder
|
||
|
||
def list_input_images(self) -> List[str]:
|
||
"""列出 data/input/ 下所有待处理图片(按 mtime 升序,先入先出)。"""
|
||
if not os.path.isdir(self._input_folder):
|
||
return []
|
||
files = []
|
||
for name in os.listdir(self._input_folder):
|
||
p = os.path.join(self._input_folder, name)
|
||
if not os.path.isfile(p):
|
||
continue
|
||
ext = os.path.splitext(name)[1].lower()
|
||
if ext in IMAGE_EXTS:
|
||
files.append(p)
|
||
files.sort(key=lambda p: os.path.getmtime(p))
|
||
return files
|
||
|
||
def process_all_inputs(
|
||
self,
|
||
progress_cb: Optional[Callable[[int, int, dict], None]] = None,
|
||
) -> Dict[str, Any]:
|
||
"""批量处理 data/input/ 下全部图片。
|
||
|
||
Args:
|
||
progress_cb: 可选回调 fn(done_count, total_count, latest_result_dict)
|
||
|
||
Returns:
|
||
{
|
||
'total': int,
|
||
'success': int,
|
||
'failed': int,
|
||
'results': [
|
||
{
|
||
'image': str,
|
||
'hash': str,
|
||
'status': 'success' | 'failed',
|
||
'supplier': str,
|
||
'bill_date': str,
|
||
'total_amount': float,
|
||
'result_file': str | None,
|
||
'error': str | None,
|
||
},
|
||
...
|
||
]
|
||
}
|
||
"""
|
||
images = self.list_input_images()
|
||
total = len(images)
|
||
results: List[Dict[str, Any]] = []
|
||
|
||
if total == 0:
|
||
logger.info("data/input/ 下无待处理图片")
|
||
return {'total': 0, 'success': 0, 'failed': 0, 'results': []}
|
||
|
||
logger.info(f"批量处理开始: 共 {total} 张图片")
|
||
success = 0
|
||
failed = 0
|
||
|
||
for idx, image_path in enumerate(images, start=1):
|
||
entry: Dict[str, Any] = {
|
||
'image': image_path,
|
||
'hash': '',
|
||
'status': 'failed',
|
||
'supplier': '',
|
||
'bill_date': '',
|
||
'total_amount': 0.0,
|
||
'result_file': None,
|
||
'error': None,
|
||
}
|
||
try:
|
||
file_hash = Path(image_path).stem
|
||
entry['hash'] = file_hash
|
||
|
||
# 1) OCR(写 data/output/{hash}.xlsx + .meta.json)
|
||
excel_path = self.ocr_service.process_image(image_path)
|
||
if not excel_path:
|
||
entry['error'] = 'OCR 失败'
|
||
failed += 1
|
||
results.append(entry)
|
||
if progress_cb:
|
||
progress_cb(idx, total, entry)
|
||
continue
|
||
|
||
# 2) process_excel(识别元信息 + 重命名 + 落库)
|
||
result_file = self.order_service.process_excel(excel_path)
|
||
if not result_file:
|
||
entry['error'] = '处理失败'
|
||
failed += 1
|
||
results.append(entry)
|
||
if progress_cb:
|
||
progress_cb(idx, total, entry)
|
||
continue
|
||
|
||
# 3) 回查元信息(process_excel 内部已落库)
|
||
meta_row = self.order_service.metadata_db.get(file_hash) or {}
|
||
entry['result_file'] = result_file
|
||
entry['supplier'] = meta_row.get('supplier', '') or ''
|
||
entry['bill_date'] = meta_row.get('bill_date', '') or ''
|
||
entry['total_amount'] = float(meta_row.get('total_amount') or 0.0)
|
||
entry['status'] = 'success'
|
||
success += 1
|
||
logger.info(
|
||
f"[{idx}/{total}] 处理完成: {Path(image_path).name} -> "
|
||
f"{Path(result_file).name} | 供应商={entry['supplier']!r} "
|
||
f"日期={entry['bill_date']!r} 金额={entry['total_amount']:.2f}"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[{idx}/{total}] 处理异常: {image_path}: {e}", exc_info=True)
|
||
entry['error'] = str(e)
|
||
failed += 1
|
||
|
||
results.append(entry)
|
||
if progress_cb:
|
||
progress_cb(idx, total, entry)
|
||
|
||
summary = {
|
||
'total': total,
|
||
'success': success,
|
||
'failed': failed,
|
||
'results': results,
|
||
}
|
||
logger.info(
|
||
f"批量处理完成: 总 {total} 张, 成功 {success}, 失败 {failed}"
|
||
)
|
||
return summary |