feat(ocr): 扫描件与图片识别——OCR 进文字层、资质库上传识别、大图浏览

计划见 docs/architecture/vision-plan.md(VS1–VS13,docs/decisions.md)。

- 验证:OCR(RapidOCR + PP-OCRv6 small,CPU)和 DeepSeek Flash 看图在国动投标文件
  和 7 张证书上初测(tech-validation §7,代码 spikes/s5-ocr/)
- OCR 服务 services/ocr(镜像、Compose 服务):整页转正(含 180°)、红章附近按红色通道重读
- 扫描页和插图识别后并入文字层 v6,句子标 source=ocr;出处标「OCR」(迁移 0013);
  依据识别文字的问题降级,规则不查扫描附件;国动端到端见 check-eval §16
- 资质库上传识别:添加资质对话框最上面传证书照片,识别后填表,只预填和 OCR 文字
  对得上的字段;识别值留痕、照片挂成附件(迁移 0014,提示词 vision.cert_fields v1)
- 资质库改为查看详情(只读键值 + 附件缩略图),附件用大图浏览控件看,不开新标签页
  (设计规范 §7.23);PDF 附件按页出预览图

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-05 17:50:13 +08:00
parent 0b783ababa
commit 3344f09563
63 changed files with 4973 additions and 345 deletions

View File

@@ -1705,7 +1705,7 @@ paths:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/QualificationInput' }
schema: { $ref: '#/components/schemas/QualificationSave' }
responses:
'201':
description: OK
@@ -1725,7 +1725,7 @@ paths:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/QualificationInput' }
schema: { $ref: '#/components/schemas/QualificationSave' }
responses:
'200':
description: OK
@@ -1764,6 +1764,87 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/Qualification' }
/qualifications/{qualification_id}/files/{file_id}/previews:
parameters:
- $ref: '#/components/parameters/QualificationId'
- name: file_id
in: path
required: true
schema: { $ref: '#/components/schemas/Id' }
get:
tags: [qualifications]
operationId: getQualificationFilePreviews
summary: 附件的预览图(图片一张,PDF 每页一张,最多 10 页),详情和大图浏览用
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
required: [file_id, filename, pages]
properties:
file_id: { $ref: '#/components/schemas/Id' }
filename: { type: string }
pages:
type: array
items:
type: object
required: [page, width, height, url]
properties:
page: { type: integer }
width: { type: integer }
height: { type: integer }
url: { type: string, description: 预览图(JPEG)的短时签名地址 }
/qualification-scans:
post:
tags: [qualifications]
operationId: createQualificationScan
summary: 上传证书扫描件识别(后台识别,轮询结果;不扣额度,每个企业每天限 100 次)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [upload_id]
properties:
upload_id: { $ref: '#/components/schemas/Id' }
responses:
'201':
description: 已排队
content:
application/json:
schema: { $ref: '#/components/schemas/QualificationScan' }
'429': { $ref: '#/components/responses/RateLimited' }
'503':
description: '`ocr_unavailable`:扫描件识别服务没有开或连不上,可以先手动填写'
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
/qualification-scans/{scan_id}:
parameters:
- name: scan_id
in: path
required: true
schema: { $ref: '#/components/schemas/Id' }
get:
tags: [qualifications]
operationId: getQualificationScan
summary: 识别结果
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/QualificationScan' }
delete:
tags: [qualifications]
operationId: deleteQualificationScan
summary: 丢弃识别结果(连同原件)
responses:
'204': { description: 已删除 }
/qualifications/{qualification_id}/files/{file_id}:
parameters:
- $ref: '#/components/parameters/QualificationId'
@@ -2584,7 +2665,7 @@ components:
Citation:
type: object
description: 出处,不可变(data-model §5.1)
required: [id, document_id, document_label, page_no, sentence_keys, quote, quads, normalized_sha256, created_by_kind]
required: [id, document_id, document_label, page_no, sentence_keys, quote, quads, normalized_sha256, created_by_kind, ocr]
properties:
id: { $ref: '#/components/schemas/Id' }
document_id: { $ref: '#/components/schemas/Id' }
@@ -2601,6 +2682,7 @@ components:
type: array
items: { $ref: '#/components/schemas/Quad' }
created_by_kind: { $ref: '#/components/schemas/Origin' }
ocr: { type: boolean, description: 原句是扫描页或插图上 OCR 识别出来的,可能有错字(界面标「识别文字」) }
JobError:
type: object
description: 给用户看的失败原因;不含文档内容
@@ -3200,15 +3282,120 @@ components:
level: { type: [string, 'null'], maxLength: 40 }
cert_no: { type: [string, 'null'], maxLength: 80, description: 界面上必填(查标时用它核对附件),接口可空,以后批量导入时不一定读得全 }
issuer: { type: [string, 'null'], maxLength: 120 }
holder: { type: [string, 'null'], maxLength: 120, description: 证书主体(企业名、著作权人、持证人) }
valid_from: { type: [string, 'null'], format: date }
valid_to: { type: [string, 'null'], format: date, description: 空表示长期有效;要晚于 valid_from }
QualificationSave:
description: 新增、修改资质。从识别结果保存时带上 scan:记下识别值,原件挂成附件(同一个原件不重复挂)
allOf:
- $ref: '#/components/schemas/QualificationInput'
- type: object
properties:
scan:
type: [object, 'null']
required: [scan_id, index]
properties:
scan_id: { $ref: '#/components/schemas/Id' }
index: { type: integer, minimum: 0, description: 识别结果里的第几张证书(从 0 数) }
QualificationScan:
type: object
description: 一次证书识别(vision-plan 第 2 步)。没保存的 24 小时后删除
required: [id, filename, status, error, pages, certificates, created_at]
properties:
id: { $ref: '#/components/schemas/Id' }
filename: { type: string }
status: { type: string, enum: [queued, running, succeeded, failed] }
error:
oneOf:
- type: 'null'
- $ref: '#/components/schemas/ScanError'
pages:
type: array
items:
type: object
required: [page, width, height, rotation, url]
properties:
page: { type: integer }
width: { type: integer, description: 识别用的页面图宽(像素),高亮框按它换算成比例 }
height: { type: integer }
rotation: { type: integer, description: OCR 转正用的角度(0 / 90 / 180 / 270) }
url: { type: string, description: 页面图的短时签名地址 }
certificates:
type: array
items: { $ref: '#/components/schemas/ScanCertificate' }
created_at: { $ref: '#/components/schemas/Timestamp' }
ScanError:
type: object
required: [code, message]
properties:
code: { type: string, examples: [ocr_unavailable, model_unavailable] }
message: { type: string }
ScanCertificate:
type: object
required: [page, category, kind, name, cert_no_field, fields, masked, suggest, notes, existing]
properties:
page: { type: integer }
category:
type: string
enum: [enterprise_qualification, system_cert, software_copyright, product, personnel, business_license, other]
kind: { $ref: '#/components/schemas/QualificationKind' }
name: { type: string, description: 建议的资质名称:证书标题,后面括注软件名或专业 }
cert_no_field: { type: string, enum: [cert_no, reg_no], description: 证书编号取哪个字段(软著取登记号) }
fields:
type: object
description: 字段名 → 识别结果。name、holder、level、specialty、software、cert_no、reg_no、issuer、issued_on、valid_from、valid_to、validity_text
additionalProperties: { $ref: '#/components/schemas/ScanField' }
masked:
type: array
description: 证书上有、但被打码或看不清的字段,要手动填写
items: { type: string }
suggest:
type: object
description: 推算的建议值,用户点了才填。valid_to:证书只写了有效期长度(如「五年」)时按发证日期推算
properties:
valid_to: { type: string, format: date }
notes:
type: array
items:
type: object
required: [code]
properties:
code: { type: string, enum: [personnel, product, business_license, holder_mismatch] }
holder: { type: string }
tenant: { type: string }
existing:
description: 资质库里证书编号相同的一条(换证时建议更新它)
oneOf:
- type: 'null'
- type: object
required: [id, name]
properties:
id: { $ref: '#/components/schemas/Id' }
name: { type: string }
ScanField:
type: object
required: [value, check, boxes]
properties:
value: { type: string, description: 证书上的原样文字 }
check:
type: string
enum: [ok, date_ok, low_conf, mismatch, bad_line, bad_format]
description: ok / date_ok 才自动填;low_conf(马赛克、模糊)、mismatch(识别结果和 OCR 对不上)等只作建议
day: { type: string, format: date, description: 日期字段换算成的日期 }
boxes:
type: array
description: 所引 OCR 行在页面图上的位置(像素)[x0, y0, x1, y1]
items:
type: array
items: { type: number }
Qualification:
allOf:
- $ref: '#/components/schemas/QualificationInput'
- type: object
required: [id, files, late_for, created_at, updated_at]
required: [id, files, recognized, late_for, created_at, updated_at]
properties:
id: { $ref: '#/components/schemas/Id' }
recognized: { type: boolean, description: 是从识别结果保存的(识别值留痕) }
files:
type: array
items: