# ry-ocr — 本地发票识别服务 API 文档 基于 **PaddleOCR 3.x + FastAPI** 的本地部署发票识别微服务。 完全离线运行,无任何云依赖,适合内网 / 等保环境。 服务默认监听 `0.0.0.0:8801`,在线文档:`http://localhost:8801/docs` --- ## 0. TL;DR | 接口 | 用途 | 鉴权 | |---|---|---| | `GET /health` | 健康检查 | 无 | | `POST /recognize/invoice` | 上传文件识别 (multipart) | 无 | | `POST /recognize/invoice/by-path` | 服务器本地路径识别 (JSON) | 白名单 | | `POST /recognize/text` | 纯文本字段抽取 (跳过 OCR) | 无 | **识别流程**(默认 `QR_FULL_OCR=true`): ``` 文件 → PDF/图片 → 扫 QR (opencv) → 解出 3 字段? ├─ 是 + fast mode → 直接返回 (engine="qr", 跳过 OCR) ├─ 是 + full mode → 继续 OCR + 抽取, QR 字段覆盖 OCR 结果 └─ 否 / 格式不合法 → 直接 not_invoice, 不跑 OCR ``` --- ## 1. 快速启动 ### A. 本地 Python ```bash pip install -r requirements.txt cp .env.example .env python run.py # → http://127.0.0.1:8801 ``` ### B. Docker ```bash docker-compose up -d curl http://localhost:8801/health ``` 首次启动会下载模型到 `/root/.paddleocr`(约 100MB),`docker-compose.yml` 已挂载 volume 持久化。 --- ## 2. 配置项 (`.env`) | 变量 | 默认 | 说明 | |---|---|---| | `APP_HOST` | `0.0.0.0` | 监听地址 | | `APP_PORT` | `8801` | 监听端口 | | `USE_GPU` | `false` | 是否使用 GPU | | `OCR_LANG` | `ch` | OCR 语言 (ch/en/chinese_cht) | | `OCR_ENGINE` | `mobile` | `mobile`=CPU 友好 / `server`=高精度需 GPU | | `MAX_UPLOAD_MB` | `20` | 上传接口单文件最大体积 | | `PDF_DPI` | `150` | PDF 转图片 DPI (扫描件建议 250~300) | | `ALLOWED_DIRS` | (空) | `by-path` 接口允许的根目录, 空=禁用 | | `OCR_PAGE_TIMEOUT_S` | `15` | 单页 OCR 超时 | | `OCR_TOTAL_TIMEOUT_S` | `60` | 整流程超时 | | **`QR_FULL_OCR`** | **`true`** | QR 命中后是否继续跑全量 OCR | | `LOG_LEVEL` | `INFO` | 日志级别 | **`QR_FULL_OCR` 双模式:** | 取值 | 行为 | |---|---| | `true` | QR 命中 → 12 字段全抽取 (QR 3 字段覆盖 OCR) ← 默认 | | `false` | QR 命中 → 仅返回 3 字段, 跳过 OCR (从 4.5s 降到 0.2s) | `ALLOWED_DIRS` 格式: - Windows(分号分隔):`ALLOWED_DIRS=E:\gitee\guoju-hegui;D:\uploads` - Linux(冒号分隔):`ALLOWED_DIRS=/data/invoices:/tmp/uploads` --- ## 3. 接口详解 ### 3.1 `GET /health` 健康检查。检查 PaddleOCR 引擎是否就绪。 **响应 200:** ```json { "status": "ok", "version": "0.1.0", "engine_ready": true } ``` `engine_ready=false` → 服务降级但仍能响应,建议先排查 OCR 模型加载问题。 --- ### 3.2 `POST /recognize/invoice` multipart/form-data 上传发票图片或 PDF。 **请求:** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `file` | file | ✅ | 图片 (PNG/JPG/JPEG/BMP/WEBP/TIFF) 或 PDF | **curl:** ```bash curl -X POST http://localhost:8801/recognize/invoice \ -F "file=@/path/to/invoice.pdf" ``` **错误码:** | HTTP | 场景 | |---|---| | 400 | 文件为空 | | 413 | 文件超过 `MAX_UPLOAD_MB` | | 422 | 缺少 file 字段 | **响应 (`InvoiceResult`) — 见 §4。** --- ### 3.3 `POST /recognize/invoice/by-path` 传入**服务器本地路径**识别,避免重复上传大文件。 > ⚠️ **安全**:路径必须在 `.env` 的 `ALLOWED_DIRS` 白名单内才会被执行。 > resolve 后必须等于或为某个允许根目录的后代;否则 403。 > `ALLOWED_DIRS` 为空时整个接口 403(默认禁用)。 **请求体 (`PathRecognizeRequest`):** ```json { "file_path": "E:/invoice/abc.pdf" } ``` | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `file_path` | string | ✅ | 服务器本地绝对路径(正反斜杠均可) | **curl:** ```bash curl -X POST http://localhost:8801/recognize/invoice/by-path \ -H "Content-Type: application/json" \ -d '{"file_path": "E:/gitee/guoju-hegui/guoju0808/ry-ocr/fapiao.pdf"}' ``` **错误码:** | HTTP | 场景 | |---|---| | 403 | 路径不在 `ALLOWED_DIRS` 白名单, 或 `ALLOWED_DIRS` 未配置 | | 404 | 文件不存在 | | 400 | 不是文件 (路径是目录) | --- ### 3.4 `POST /recognize/text` 纯文本字段抽取,**不调用 OCR**。便于接入其他识别引擎(百度/腾讯/扫描件 OCR SDK 等)。 **Query 参数:** | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `raw_text` | string | ✅ | OCR 原始文本 (多行用 `\n` 分隔) | **curl:** ```bash curl -X POST 'http://localhost:8801/recognize/text?raw_text=电子发票%0A发票号码:24922000000006110014%0A价税合计(大写)叁万玖仟伍佰圆整%0A(小写)%EF%BF%A539500.00' ``` **响应:** `{"fields": {...InvoiceFields}}` --- ## 4. 响应模型 ### 4.1 `InvoiceResult` (主响应) | 字段 | 类型 | 说明 | |---|---|---| | `success` | bool | 整体是否成功 | | `is_invoice` | bool | 是否被判定为发票 (false=非发票) | | `raw_text` | string | 全部 OCR 文本拼接 (快路径为 `[QR only] ...`) | | `lines` | OCRLine[] | 分行识别结果 | | `fields` | InvoiceFields | 结构化字段 | | `page_count` | int | PDF 页数 / 图片=1 | | `engine` | string | `paddleocr` / `qr` | | `elapsed_ms` | int | 服务端识别耗时 (毫秒) | | `error` | string? | 失败原因描述 | | `error_code` | string? | 见 §4.4 错误码表 | | `from_qr` | bool | 是否从 QR 取到了 3 个核心字段 | | `qr_raw` | string? | 二维码原始文本 (排查用) | | `qr_error` | string? | `no_qr` / `bad_format` | ### 4.2 `InvoiceFields` (fields 子对象) | 字段 | 类型 | 来源 | |---|---|---| | `invoice_type` | string? | OCR: "电子发票"/"增值税专用发票"等 | | `invoice_no` | string? | **QR (权威)** / OCR | | `invoice_code` | string? | OCR (数电票此字段为空) | | `invoice_date` | string (YYYY-MM-DD) | **QR (权威)** / OCR | | `amount` | float? | **QR (权威)** / OCR — 价税合计小写 | | `amount_cn` | string? | OCR — 价税合计大写 | | `amount_pretax` | float? | OCR — 不含税金额 | | `tax_amount` | float? | OCR — 税额 | | `seller_name` | string? | OCR | | `seller_tax_no` | string? | OCR | | `buyer_name` | string? | OCR | | `buyer_tax_no` | string? | OCR | | `amount_match` | bool? | 大写金额 vs 小写金额一致性 | 标 **QR (权威)** 的字段:当 QR 命中时,无论 OCR 结果如何,最终值取 QR。 ### 4.3 `OCRLine` ```json { "text": "发票号码:24922000000006110014", "confidence": 0.998, "box": [[915, 67], [1191, 67], [1191, 83], [915, 83]] } ``` ### 4.4 `error_code` 表 | 取值 | 含义 | 触发场景 | |---|---|---| | `not_invoice` | 非发票 | QR 没扫到 / 格式不合法 | | `unsupported` | 不支持的文件类型 | 后缀不是 PDF/图片 | | `process_failed` | 处理失败 | PDF 渲染异常等 | | `timeout` | 超时 | 达到单页/总流程超时 | | `ocr_failed` | OCR 异常 | PaddleOCR 内部错误 | --- ## 5. 完整示例 ### 5.1 真发票 PDF(默认模式 → 12 字段) **请求:** `POST /recognize/invoice/by-path` body=`{"file_path":"E:/.../fapiao.pdf"}` **响应:** ```json { "success": true, "is_invoice": true, "raw_text": "电子发票\n(电子发票)\n发票号码:24922000000006110014\n...", "lines": [...37 行], "fields": { "invoice_type": "电子发票", "invoice_no": "24922000000006110014", "invoice_code": null, "invoice_date": "2024-02-02", "amount": 39500.0, "amount_cn": "叁万玖仟伍佰圆整", "amount_pretax": 37264.15, "tax_amount": 2235.85, "seller_name": "青岛鸿图华构信息技术有限公司", "seller_tax_no": "91370222MA3N7N3Y1H", "buyer_name": "北京国钜科技实业股份有限公司", "buyer_tax_no": "91110108MA01EMTK2E", "amount_match": true }, "page_count": 1, "engine": "paddleocr", "elapsed_ms": 4516, "from_qr": true, "qr_raw": "01,31,,24922000000006110014,39500.00,20240202,,A371", "qr_error": null } ``` ### 5.2 真发票 PDF(快路径 `QR_FULL_OCR=false` → 仅 3 字段) ```json { "success": true, "is_invoice": true, "raw_text": "[QR only] 01,31,,24922000000006110014,39500.00,20240202,,A371", "lines": [], "fields": { "invoice_no": "24922000000006110014", "amount": 39500.0, "invoice_date": "2024-02-02" }, "page_count": 1, "engine": "qr", "elapsed_ms": 209, "from_qr": true, "qr_raw": "01,31,,24922000000006110014,39500.00,20240202,,A371", "qr_error": null } ``` ### 5.3 非发票图片(无 QR) ```json { "success": false, "is_invoice": false, "error": "未识别到发票二维码(可能不是发票图片)", "error_code": "not_invoice", "raw_text": "", "lines": [], "fields": {}, "page_count": 1, "engine": "paddleocr", "elapsed_ms": 220, "from_qr": false, "qr_raw": null, "qr_error": "no_qr" } ``` --- ## 6. 性能基线 (PP-OCRv5_mobile, CPU) | 场景 | HTTP 耗时 | 服务端 OCR | 备注 | |---|---|---|---| | 真发票 PDF (默认) | 4.5s | 4516ms | 含 1500ms PDF 渲染 | | 真发票 PDF (快路径) | **0.22s** | 209ms | QR 解出即返回 | | 非发票图片 | **0.22s** | 220ms | QR 没扫到, 不跑 OCR | | 非发票文字截图 | **0.38s** | 377ms | 同上 | PDF 转图 DPI=150;OCR 移动端模型单页约 1.5~4s,**首请求**因模型预热会更慢。 --- ## 7. Java 客户端 (RuoYi) `client/` 目录下: - `OcrClient.java` - `InvoiceResult.java` / `InvoiceFields.java` / `OcrLine.java` **Service 调用:** ```java @Service @RequiredArgsConstructor public class InvoiceOcrService { private final OcrClient ocrClient = new OcrClient("http://127.0.0.1:8801"); public InvoiceResult recognize(MultipartFile file) { File tmp; try { tmp = File.createTempFile("inv_", "_" + file.getOriginalFilename()); file.transferTo(tmp); } catch (IOException e) { throw new RuntimeException("保存临时文件失败", e); } try { InvoiceResult r = ocrClient.recognize(tmp); if (!Boolean.TRUE.equals(r.getSuccess())) { throw new RuntimeException("OCR 识别失败: " + r.getError()); } return r; } finally { tmp.delete(); } } } ``` **Controller:** ```java @RestController @RequestMapping("/business/invoice") public class InvoiceOcrController { private final InvoiceOcrService ocrService; @PostMapping("/recognize") public AjaxResult recognize(@RequestParam("file") MultipartFile file) { return AjaxResult.success(ocrService.recognize(file)); } } ``` 依赖(已用 hutool 可省): ```xml cn.hutool hutool-http 5.8.27 ``` --- ## 8. 错误码速查 调用方拿到响应后建议这样分流: ```python if not resp.success: if resp.error_code == "not_invoice": # 不是发票 — 直接告诉用户"请上传发票图片" elif resp.error_code == "timeout": # 超时 — 建议重试 / 提高 DPI elif resp.error_code in ("unsupported", "process_failed"): # 文件问题 — 提示格式 else: # 其他 OCR 异常 — 兜底 if not resp.is_invoice: # 跟 not_invoice 等价 — 多数情况下 success=False 也伴随 is_invoice=False pass ``` --- ## 9. 局限 & 后续 - **无 QR 的老式纸质发票** 当前会判 not_invoice — 需新增「无 QR 回退 OCR」配置项可破 - **表格明细** (货物/数量/单价) 未抽取 — 需要时接 PP-Structure - **字段抽取基于正则**,对版式变化敏感;如有大量样本可考虑 LayoutLMv3 微调 - **并发**:PaddleOCR 非进程安全,**`workers=1`**;高并发前置 nginx 负载均衡