Files

12 KiB
Raw Permalink Blame History

ry-ocr-java — 本地发票识别服务 (Spring Boot 版)

基于 PaddleOCR ONNX Runtime + Spring Boot 3.3 的本地部署发票识别微服务, 完全对齐 Python 版 ry-ocr 的接口契约与业务逻辑。

完全离线运行,无任何云依赖,适合内网 / 等保环境。

服务默认监听 0.0.0.0:8802,在线文档:http://localhost:8802/swagger-ui.html


与 ry-ocr (Python) 的关系

维度 ry-ocr (Python) ry-ocr-java
端口 8801 8802
Web 框架 FastAPI Spring Boot 3.3
OCR 引擎 PaddleOCR 3.x (Python) PaddleOCR ONNX Runtime (Java 推理)
QR 解码 OpenCV QRCodeDetector ZXing
PDF 渲染 PyMuPDF (fitz) Apache PDFBox
中文金额 cn2an (Python 库) 自实现简化版
接口路径 完全一致 完全一致
响应字段名 snake_case snake_case (与 Python 一致)
错误码 一致 一致

可与 ry-ocr 并列部署,互不冲突;测试通过后再决定切换。


0. TL;DR

接口 用途 鉴权
GET /health 健康检查
POST /recognize/invoice 上传文件识别 (multipart)
POST /recognize/invoice/by-path 服务器本地路径识别 (JSON) 白名单
POST /recognize/text 纯文本字段抽取 (跳过 OCR)

识别流程(默认 app.ocr.qr-full-ocr=true):

文件 → PDF/图片 → 扫 QR (ZXing) → 解出 3 字段?
  ├─ 是 + fast mode   → 直接返回 (engine="qr", 跳过 OCR)
  ├─ 是 + full mode   → 继续 OCR + 抽取, QR 字段覆盖 OCR 结果
  └─ 否 / 格式不合法  → 直接 not_invoice, 不跑 OCR

1. 快速启动

前置条件

启动

cd ry-ocr-java
mvn spring-boot:run            # → http://127.0.0.1:8802

Docker (TODO)

docker build -t ry-ocr-java .
docker run -p 8802:8802 ry-ocr-java

2. 配置项 (application.yml)

配置项 默认 说明
server.port 8802 监听端口
app.version 0.1.0 服务版本 (与 /health.version 对应)
app.upload.max-mb 20 上传接口单文件最大体积
app.upload.pdf-dpi 150 PDF 转图片 DPI (扫描件建议 250~300)
app.ocr.page-timeout-s 15 单页 OCR 超时
app.ocr.total-timeout-s 60 整流程超时
app.ocr.qr-full-ocr true QR 命中后是否继续跑全量 OCR
app.ocr.lang ch OCR 语言 (ch/en/chinese_cht)
app.ocr.models-dir models 模型目录 (相对/绝对)
app.allowed-dirs "" by-path 接口允许的根目录, 空 = 禁用

qr-full-ocr 双模式:

取值 行为
true QR 命中 → 12 字段全抽取 (QR 3 字段覆盖 OCR) ← 默认
false QR 命中 → 仅返回 3 字段, 跳过 OCR (从 4.5s 降到 0.2s)

app.allowed-dirs 格式:

app:
  allowed-dirs:
    - "E:\\gitee\\guoju-hegui"
    - "D:\\uploads"
# 或单字符串 (Windows 分号, Linux 冒号):
app:
  allowed-dirs: "E:\\gitee\\guoju-hegui;D:\\uploads"

3. 接口详解

3.1 GET /health

健康检查。检查 OCR 引擎是否就绪。

响应 200

{
  "status": "ok",
  "version": "0.1.0",
  "engine_ready": true
}

engine_ready=false → 服务降级但仍能响应,建议先排查 ONNX 模型加载问题。 /recognize/text 接口不依赖引擎,仍可用。


3.2 POST /recognize/invoice

multipart/form-data 上传发票图片或 PDF。

请求:

字段 类型 必填 说明
file file 图片 (PNG/JPG/JPEG/BMP/WEBP/TIFF) 或 PDF

curl

curl -X POST http://localhost:8802/recognize/invoice \
  -F "file=@/path/to/invoice.pdf"

错误码:

HTTP 场景
400 文件为空
413 文件超过 max-mb 限制

响应InvoiceResult 见 §4。


3.3 POST /recognize/invoice/by-path

传入服务器本地路径识别,避免重复上传大文件。

⚠️ 安全:路径必须在 app.allowed-dirs 白名单内才会被执行。 app.allowed-dirs 为空时整个接口 403(默认禁用)。

请求体 (PathRecognizeRequest)

{
  "file_path": "E:/invoice/abc.pdf"
}

注: JSON 字段名是 file_path (snake_case, 与 Python ry-ocr 完全一致).

字段 类型 必填 说明
file_path string 服务器本地绝对路径(正反斜杠均可)

curl

curl -X POST http://localhost:8802/recognize/invoice/by-path \
  -H "Content-Type: application/json" \
  -d '{"file_path": "E:/gitee/guoju-hegui/guoju0808/ry-ocr/fapiao.pdf"}'

错误码:

HTTP 场景
403 路径不在白名单, 或白名单未配置
404 文件不存在
400 不是文件 (路径是目录)

3.4 POST /recognize/text

纯文本字段抽取,不调用 OCR。便于接入其他识别引擎(百度/腾讯/扫描件 OCR SDK 等)。

Query 参数:

参数 类型 必填 说明
raw_text string OCR 原始文本 (多行用 \n 分隔)

curl

curl -X POST 'http://localhost:8802/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 小写金额一致性

4.3 OCRLine

{
  "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 异常 ONNX 推理内部错误

5. 错误码速查

调用方拿到响应后建议这样分流:

if (!resp.isSuccess()) {
    if ("not_invoice".equals(resp.getErrorCode())) {
        // 不是发票 — 直接告诉用户"请上传发票图片"
    } else if ("timeout".equals(resp.getErrorCode())) {
        // 超时 — 建议重试 / 提高 DPI
    } else if ("unsupported".equals(resp.getErrorCode())
            || "process_failed".equals(resp.getErrorCode())) {
        // 文件问题 — 提示格式
    } else {
        // 其他 OCR 异常 — 兜底
    }
}

if (Boolean.FALSE.equals(resp.getIsInvoice())) {
    // 跟 not_invoice 等价 — 多数情况下 success=False 也伴随 is_invoice=False
}

6. Java 客户端调用 (hutool)

@Service
public class InvoiceOcrService {

    private final OcrClient ocrClient = new OcrClient("http://127.0.0.1:8802");

    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();
        }
    }
}

客户端类 (OcrClient / InvoiceResult / InvoiceFields / OcrLine) 直接复用 ry-ocr/client/*.java,只需改 baseUrl 为 http://127.0.0.1:8802

注意: 由于 Java 客户端使用 camelCase (getInvoiceNo) 解析 JSON, 而服务端返回 snake_case (invoice_no), 现有客户端需要适配字段名。 详见 ry-ocr-javary-api 的字段映射对照。


7. 项目结构

ry-ocr-java/
├── pom.xml
└── src/main/
    ├── java/com/ruoyi/ocr/
    │   ├── OcrApplication.java               # Spring Boot 入口
    │   ├── config/OcrProperties.java         # @ConfigurationProperties("app")
    │   ├── api/OcrController.java            # 4 个 REST 接口
    │   ├── core/
    │   │   ├── OcrEngine.java                # ONNX Runtime + 单例 + 超时
    │   │   ├── TextDetector.java             # DB 检测
    │   │   ├── TextRecognizer.java           # CRNN 识别
    │   │   ├── DbPostProcessor.java          # DB 后处理 (box 提取)
    │   │   ├── CtcDecoder.java               # CTC 解码
    │   │   ├── Dictionary.java               # 字典加载
    │   │   ├── PdfProcessor.java             # PDFBox 渲染
    │   │   └── ImageProcessor.java           # Java 2D 旋转/增强
    │   ├── model/                             # 5 个 DTO + QrDecodeResult
    │   ├── service/
    │   │   ├── QrDecoder.java                # ZXing + 8 字段解析
    │   │   ├── InvoiceExtractor.java         # 正则 + box 坐标归属
    │   │   └── RecognizeService.java         # 端到端流水线
    │   ├── util/AmountUtils.java             # cn2an 简化版 + 金额正则
    │   └── exception/OcrTimeoutException.java
    └── resources/
        ├── application.yml
        └── models/                            # ONNX 模型 (需手动放)

8. 局限 & 后续

  • 无 QR 的老式纸质发票 当前会判 not_invoice — 需新增「无 QR 回退 OCR」配置项
  • 表格明细 (货物/数量/单价) 未抽取 — 需要时接 PP-Structure
  • 字段抽取基于正则,对版式变化敏感
  • DB 后处理简化Java 版用 bounding box + unclip 替代 PaddleOCR 原始的 polygon + findContours;精度可能略低,可后续替换
  • 并发ONNX Runtime 内部串行推理;HTTP 层 Tomcat 默认 200 线程
  • 客户端字段映射:现有 Java OcrClient 用 camelCase 解析 JSON,需适配 snake_case 字段名(建议字段名都改成 @JsonProperty 显式标注)