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

118 lines
3.4 KiB
Python
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.
"""金额工具:中文大写金额解析 + 与小写金额比对"""
from __future__ import annotations
import re
from typing import Optional
import cn2an
# 小写金额正则
_NUM_PATTERN = re.compile(r"¥?\s*(\d{1,8}(?:,\d{3})*\.\d{2})")
_TAX_AMOUNT_PATTERN = re.compile(r"税\s*额\s*[¥:]?\s*(\d{1,8}(?:,\d{3})*\.\d{2})")
_PRETAX_PATTERN = re.compile(r"(?:不合?税价|不含税)\s*[¥:]?\s*(\d{1,8}(?:,\d{3})*\.\d{2})")
_TOTAL_PATTERN = re.compile(r"价税合计[^\d]*[¥]?\s*(\d{1,8}(?:,\d{3})*\.\d{2})")
def normalize_cn_amount(text: str) -> str:
"""中文金额归一化:圆→元、〇→零"""
if not text:
return ""
return text.replace("圆", "元").replace("", "零").replace(" ", "")
def extract_cn_amount(text: str) -> Optional[str]:
"""从原文里匹配出第一段大写金额字符串
策略(按优先级):
1. "价税合计" 后面括号内
2. 任意中括号里的中文金额
3. 含"元"或"圆"的最长中文字符串
"""
if not text:
return None
# 1. 价税合计之后括号里
m = re.search(r"价税合计[^\(]*[\((]([零壹贰叁肆伍陆柒捌玖拾佰仟万亿圆元角分整]+)[\))]", text)
if m:
return m.group(1)
# 2. 任意中括号里的中文金额
m = re.search(r"[\((]([零壹贰叁肆伍陆柒捌玖拾佰仟万亿圆元角分整]{3,30})[\)]", text)
if m:
return m.group(1)
# 3. 含"元"/"圆"的中文片段
for cand in re.findall(r"[零壹贰叁肆伍陆柒捌玖拾佰仟万亿圆元角分整]{3,30}", text):
if "元" in cand or "圆" in cand:
return cand
return None
def parse_cn_amount(cn_text: str) -> Optional[float]:
"""把中文大写金额转 float,例如 '贰佰元整' → 200.0"""
if not cn_text:
return None
try:
s = normalize_cn_amount(cn_text)
# cn2an 要求带 '元' 或 '圆' 结尾
s = s.rstrip("整")
if not s.endswith("元"):
s += "元"
value = cn2an.cn2an(s, "smart")
return float(value)
except Exception:
return None
def amount_consistent(cn_text: Optional[str], num_amount: Optional[float]) -> Optional[bool]:
"""大写 vs 小写金额比对"""
if cn_text is None or num_amount is None:
return None
cn_value = parse_cn_amount(cn_text)
if cn_value is None:
return None
return abs(cn_value - num_amount) < 0.011
def extract_num_amount(text: str) -> Optional[float]:
"""从文本里提取第一个形如 1234.56 或 ¥1,234.56 的金额"""
if not text:
return None
m = _NUM_PATTERN.search(text)
if not m:
return None
try:
return float(m.group(1).replace(",", ""))
except ValueError:
return None
def extract_total_amount(text: str) -> Optional[float]:
m = _TOTAL_PATTERN.search(text)
if m:
try:
return float(m.group(1).replace(",", ""))
except ValueError:
pass
return extract_num_amount(text)
def extract_tax_amount(text: str) -> Optional[float]:
m = _TAX_AMOUNT_PATTERN.search(text)
if m:
try:
return float(m.group(1).replace(",", ""))
except ValueError:
pass
return None
def extract_pretax_amount(text: str) -> Optional[float]:
m = _PRETAX_PATTERN.search(text)
if m:
try:
return float(m.group(1).replace(",", ""))
except ValueError:
pass
return None