"""批量处理服务:扫描 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 .ocr_service import OCRService from .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