Files
郭庆泰 c3eb8ed9c3 feat: OCR 服务 + 会议材料模块
ry-ocr/ (新)
  本地发票识别微服务 (PaddleOCR 3.x + FastAPI, 8801)
  - QR 优先: 扫到二维码即取开票时间/发票号/金额; 没扫到/格式不合法直接判非发票, 不跑 OCR
  - 配置 QR_FULL_OCR 控制快路径(false, 0.2s)还是全字段(true, 4.5s)
  - /recognize/invoice (multipart) + /recognize/invoice/by-path (本地路径, 白名单) + /recognize/text
  - is_invoice / from_qr / qr_raw / qr_error / error_code 字段
  - 12 字段发票抽取 (regex + 启发式, 左右主体识别)
  - 超时保护 (15s 单页 / 60s 总流程) + PaddleOCR 单例 + ThreadPoolExecutor

ry-api/ruoyi-business/
  - pom.xml: 加 hutool-http/json/core 5.8.27, lombok 1.18.30 (OcrClient @Slf4j 所需)
  - ocr/: OcrClient + InvoiceResult/Fields/Line + ZipExtractor + InvoiceOcrScheduler
  - oss/: OssUploader + OssConfMeta (OCR 识别后重传 OSS)
  - config/: OcrConfig + OcrExecutorConfig (后台线程池)
  - service/impl/InvoiceOcrService: 后台提交 OCR, ZIP 路径解压识别, 替换场景先清旧
  - 会议材料 CRUD 全套 (BizMeetingAuditLog/Executor/Invoice/Material/Supervisor):
    controller + service + mapper + domain + xml

ry-vue3/
  - MeetingDetail.vue (新建): 会议详情页 (含评分维度章节, 改只读)
  - Meetings.vue / OssFileUploader.vue / router / Login.vue: 适配新字段

ry-api/ruoyi-admin/
  - RuoYiApplication.java + application.yml: 启用 @Async 异步支持

_self/
  - manager_meetings.md / manager_meeting_detail.md: 文档
2026-08-22 00:23:22 +08:00

422 lines
12 KiB
Markdown
Raw Permalink 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 — 本地发票识别服务 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=150OCR 移动端模型单页约 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
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.27</version>
</dependency>
```
---
## 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 负载均衡