Files
guoju0808/ry-ocr-java/README.md
T

374 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ry-ocr-java — 本地发票识别服务 (Spring Boot 版)
基于 **PaddleOCR ONNX Runtime + Spring Boot 3.3** 的本地部署发票识别微服务,
**完全对齐** Python 版 [ry-ocr](../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. 快速启动
### 前置条件
- JDK 17+
- Maven 3.9+
- ONNX 模型文件 (详见 [src/main/resources/models/README.md](src/main/resources/models/README.md))
### 启动
```bash
cd ry-ocr-java
mvn spring-boot:run # → http://127.0.0.1:8802
```
### Docker (TODO)
```bash
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` 格式:
```yaml
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**
```json
{
"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**
```bash
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`)**
```json
{
"file_path": "E:/invoice/abc.pdf"
}
```
> 注: JSON 字段名是 `file_path` (snake_case, 与 Python ry-ocr 完全一致).
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file_path` | string | ✅ | 服务器本地绝对路径(正反斜杠均可) |
**curl**
```bash
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**
```bash
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`
```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 异常 | ONNX 推理内部错误 |
---
## 5. 错误码速查
调用方拿到响应后建议这样分流:
```java
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)
```java
@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-java` 与 `ry-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 显式标注)