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

12 KiB
Raw Permalink Blame History

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

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

⚠️ 安全:路径必须在 .envALLOWED_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=150OCR 移动端模型单页约 1.5~4s,首请求因模型预热会更慢。


7. Java 客户端 (RuoYi)

client/ 目录下:

  • OcrClient.java
  • InvoiceResult.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 负载均衡