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: 文档
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
pip install -r requirements.txt
cp .env.example .env
python run.py # → http://127.0.0.1:8801
B. Docker
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:
{
"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:
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):
{
"file_path": "E:/invoice/abc.pdf"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file_path |
string | ✅ | 服务器本地绝对路径(正反斜杠均可) |
curl:
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:
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
{
"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"}
响应:
{
"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 字段)
{
"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)
{
"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.javaInvoiceResult.java/InvoiceFields.java/OcrLine.java
Service 调用:
@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:
@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 可省):
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.27</version>
</dependency>
8. 错误码速查
调用方拿到响应后建议这样分流:
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 负载均衡