feat(detail): 参会人表新增 增值税及附加/摘要/现场照片 3 列 + 劳务协议展示, 角色=劳务形式, 费项改名 应发金额/个税税金/实发金额
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
# 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 显式标注)
|
||||
Reference in New Issue
Block a user