编辑 | blame | 历史 | 原始文档

智能质检报告 — 出件接口 前端联调方案

后端模块:yudao-module-qcreport 接口前缀:/admin-api/qc-report/instance
本轮范围:**HTML 出件 + PDF 出件**。PDF 由后端调 Chromium 打印,接口只返回归档结果与签名地址;
报告实例表**不存文件地址**,PDF 统一挂在附件库(system_storage_attachment)里。

涉及页面

页面 路由 说明 状态
报告实例列表页 /qc/report-instance 分页查看已出件的报告,按模板/编号/业务单据/状态/时间筛选 已落地(views/mes/qc/report/instance/index.vue)
报告预览 / 详情弹窗 同上 展示渲染出的 HTML;数据快照页签;**「导出 PDF」按钮调 pdf 接口** 已落地(.../instance/modules/detail.vue)
模板设计器(既有) /qc/report-template 「预览」按钮需要调用本文档的 preview 接口 仍待接入(本轮只加了「AI 导入」按钮)
MES 质检单(既有,后续) — 「出报告」入口,把质检单数据归一化后调 generate 后续轮次(本轮由调用方自行组织数据)

菜单已入库:「报告实例」挂在「质量管理」(parent_id=5500)下,component 填 mes/qc/report/instance/index,前端动态路由解析到同名 .vue。

业务流程与数据带入

  1. 设计器 → 预览:设计器把当前画布保存成版本后,用「当前画布对应的模板 + 版本」加一份示例数据调用 preview,
    拿到 HTML 直接展示。**预览不消耗报告编号**,可以反复调用;**预览接受草稿版本**(见「业务规则」)。
  2. 业务系统 → 出件:调用方(本轮是手工构造,后续是 MES 质检单)准备「报告上下文」→ 调 generate
    → 后端渲染、落库、冻结数据快照 → 返回实例编号与报告编号。
  3. 列表 → 详情:列表拿到 id → 调 get 取 renderHtml 展示;需要看「当时用的什么数据」再调 snapshot。
  4. 详情 → 重新生成:调 regenerate 用**冻结的快照**重渲,覆盖产物 HTML。数据与编号都不变。
    已归档的 PDF 会被一并作废(见「业务规则」)。
  5. 详情 → 导出 PDF:点「导出 PDF」调 pdf 接口 → 后端打印并归档 → 接口返回本次的下载地址(临时签名)。
    页面要长期显示「这份报告有 PDF」请调**附件列表接口**(见「PDF 归档怎么查」),不要缓存接口返回的签名地址。

带入关系要点

  • templateId 是主线,从模板列表带入设计器、再带入出件。
  • version 可以不传:不传时后端取**模板当前的版本**(currentVersion);一定要用某个特定版本才需要显式传。
    设计器预览建议始终显式传版本——见「业务规则」里对 currentVersion 的说明。
  • reportNo 可以不传:不传时由后端按 QR + 日期 + 4 位流水 自动生成。
  • businessId / businessType 只做留痕与查询过滤,后端不校验、不解析它指向的单据。

API

方法 路径 说明 权限码
POST /qc-report/instance/preview 只渲染,**不落库、不生成、不消耗报告编号** qc-report:instance:query
POST /qc-report/instance/generate 渲染 + 落库 + 冻结数据快照 qc-report:instance:create
GET /qc-report/instance/get?id= 详情:元信息 + 渲染产物 HTML(**不含**数据快照) qc-report:instance:query
GET /qc-report/instance/snapshot?id= 取冻结的数据快照 qc-report:instance:query
GET /qc-report/instance/page 分页 qc-report:instance:query
POST /qc-report/instance/regenerate?id= 用冻结快照 + 同一模板版本重渲,覆盖产物 HTML qc-report:instance:create
POST /qc-report/instance/pdf?id= 打印实例已存的产物 HTML 成 PDF 并归档,可重复调用 qc-report:instance:export
DELETE /qc-report/instance/delete?id= 删除实例(**已归档的 PDF 一并清理**) qc-report:instance:delete

✅ 前端实例页落地时,qc-report:instance:* 的菜单与按钮权限**已同步补入 system_menu**
(菜单「报告实例」+ 查询/出件/导出PDF/删除四条按钮 + qc-report:template:ai-import 一条)。
未插这些行之前,非超管角色调这些接口会 403;超管由代码直返全部菜单,联调从不受影响。

出件 / 预览请求参数(preview 与 generate 同一套)

参数 类型 必填 说明
templateId Long 是 模板编号
version String 否 模板版本号,如 v1.0。不传则取该模板的 currentVersion
reportNo String 否 报告编号。传了就用它(撞号会报错),不传由后端自动生成
businessId String 否 业务单据编号,仅留痕与查询用
businessType String 否 业务单据类型,如 mes_qc_oqc,仅留痕与查询用
context Object 是 报告数据,结构见下

context 结构(与设计器里的数据契约同构)

字段 类型 必填 说明
report Object 否 报告级字段。显式传 null 会被后端兜成空字段集,不会报错
report.reportNo String 否 报告编号。**顶层 reportNo 优先于它**
report.reportName String 否 报告名称
report.sampleNo String 否 样品编号
report.productCode String 否 产品编码
report.productName String 否 产品名称
report.spec String 否 规格型号
report.batchNo String 否 批次号
report.workOrderNo String 否 工单号
report.inspectType String 否 检验类型
report.inspector String 否 检验员
report.inspectDate String 否 检验日期
report.department String 否 部门
report.customerName String 否 客户名称
report.supplierName String 否 供应商名称
report.result / report.resultText / report.conclusion / report.passRate String 否 判定结果,由后端算出并覆盖,调用方不用传
report.total / report.passCount / report.failCount Number 否 同上,由后端算出
inspectionItems Array 是 检验项列表,**不能为空**(空数组会报错,见「错误码」)
inspectionItems[].index Number 否 序号,渲染 {{index}} 用
inspectionItems[].itemCode String 否 检验项编码
inspectionItems[].itemName String 否 检验项名称
inspectionItems[].standardValue String 否 标准值(文本,如 10±0.5)
inspectionItems[].actualValue String 否 实测值
inspectionItems[].unit String 否 单位
inspectionItems[].upperLimit / lowerLimit Number 否 规格上下限。**不传就是没有规格区间**,判定会落到规则或数据自带结果上
inspectionItems[].result / resultText String 否 判定结果,**由后端算出并覆盖**
inspectionItems[].remark String 否 备注

请求示例

{
  "templateId": 5,
  "version": "v1.0",
  "businessId": "OQC20260918001",
  "businessType": "mes_qc_oqc",
  "context": {
    "report": { "reportName": "出货检验报告", "productName": "法兰", "inspector": "张三" },
    "inspectionItems": [
      { "index": 1, "itemName": "外观", "standardValue": "无划痕", "actualValue": "合格" },
      { "index": 2, "itemName": "长度", "standardValue": "10±0.5", "actualValue": 10.2, "upperLimit": 10.5, "lowerLimit": 9.5 }
    ]
  }
}

响应字段

preview 响应

字段 类型 说明
reportNo String 本次渲染用的编号。**来自入参**,没传就是空 —— 预览不生成编号
html String 完整 HTML 文档,可直接预览/打印
errors Array<String> 数据缺口清单,见下

generate 响应

字段 类型 说明
id Long 实例编号,拿它去查详情
reportNo String 最终生效的报告编号(自动生成或入参指定)
templateVersion String 本次使用的模板版本号
status Number 报告状态:0 生成中 / 1 生成成功 / 2 生成失败。当前只会返回 1
errors Array<String> 数据缺口清单

regenerate 响应

字段 类型 说明
id Long 实例编号
reportNo String 报告编号(与生成时相同)
errors Array<String> 数据缺口清单

pdf 响应

字段 类型 说明
id Long 实例编号
reportNo String 报告编号
fileName String PDF 文件名,规则是 <报告编号>.pdf(如 QR20260918-0001.pdf)
byteSize Long PDF 字节数
blobId Long 文件编号(system_storage_blob 主键)
attachmentId Long 附件关联编号(system_storage_attachment 主键)
previewURL String 预览地址。**临时签名,会过期**,别存起来长期用
downloadURL String 下载地址。**临时签名,会过期**,下载动作请拿即时返回的这个值
durationMs Long 本次出件总耗时(毫秒),含排队等并发名额、必要时重启浏览器的等待
pdfDurationMs Long 打印耗时(毫秒),仅含等页面资源与打印。与 durationMs 之差就是等待成本
browserStatus String READY(复用了已在运行的浏览器)/ RESTARTED(本次重启了浏览器)。**仅供参考与排查**,前端不需要分支处理

pdf 是**显式触发**的:用户点「导出 PDF」才生成。它不会随 generate / regenerate 自动跑,
因为每导一次都要拉起 Chromium 打印(秒级),出件接口不该背这个开销。

get 响应:id、reportNo、templateId、templateVersion、businessId、businessType、renderHtml、status、creator、createTime、updateTime。
注意不含 dataSnapshot(体积大且列表/详情用不到),要它请调 snapshot。

snapshot 响应:{ "report": {...}, "inspectionItems": [...] },即出件当时冻结的那份数据(含算好的 PASS/FAIL 与合格率)。

page 请求参数(其余为通用分页参数 pageNo / pageSize)

参数 类型 说明
templateId Long 模板编号,精确匹配
reportNo String 报告编号,**模糊匹配**
businessType String 业务单据类型,精确匹配
businessId String 业务单据编号,精确匹配
status Number 报告状态,精确匹配
createTime Array<String> 创建时间区间,["开始","结束"],格式 yyyy-MM-dd HH:mm:ss

PDF 归档怎么查(不新增专用查询接口)

报告实例表里**没有也不会有** pdfUrl / pdfPath 这类字段。PDF 统一通过 system 模块的附件库里查,
走的是全项目通用的附件接口(ERP / CRM 页面同款姿势):

方法 路径 说明
GET /system/storage-attachment/list?recordType=qc_report_instance&recordId=<实例编号> 查这份报告归档的 PDF
参数 值 说明
recordType 固定 qc_report_instance 报告实例的记录类型,写死这个值
recordId 报告实例编号(instance.id)

响应是一个数组,每项含 url / previewURL / downloadURL / name / id(附件编号)等。
正常情况数组里最多一条(再次导出会替换掉旧的,不会越导越多)。

前端展示「PDF」按钮的逻辑建议:

  1. 进详情页时调一次附件列表接口,有数据 → 显示「下载 PDF」;没有 → 显示「导出 PDF」。
  2. 点「导出 PDF」调 pdf 接口,成功后用返回的 downloadURL 直接下载(或重新查一次附件列表刷新状态)。
  3. 不要把 downloadURL 存进前端状态长期使用:它是带签名的临时地址,过期后会 401/403。

字段展示规则

字段 展示位置 说明
reportNo 列表列、详情头部、HTML 内 列表建议加「复制编号」;**它是业务唯一键,不能重复**
templateVersion 列表列、详情 建议显示成「模板名 v1.0」;历史报告要能看出用的是哪一版
businessType / businessId 列表列、详情 建议用字典把 businessType 转成中文(如 mes_qc_oqc → 出货检验单)
status 列表列 用字典 qc_report_instance_status 渲染标签(生成中/生成成功/生成失败)
renderHtml 详情、预览 用 iframe 或整块 HTML 容器渲染;**不要把 HTML 当纯文本展示**
PDF 是否存在 详情页「下载 PDF / 导出 PDF」按钮、列表列 数据来源是附件列表接口(recordType=qc_report_instance),不是实例表。regenerate 之后要刷新这个状态
creator / createTime 列表列、详情 —
errors 出件/预览/重新生成后 用可折叠面板或提示条展示,不要静默丢弃,见「业务规则」

业务规则说明

场景 规则
报告编号谁生成 后端。格式 QR + yyyyMMdd + '-' + 4 位流水(如 QR20260918-0001),**按天独立,跨天从 0001 重来**。传入 reportNo 可覆盖
指定编号撞号 报错并**指明是哪个编号**(不是笼统「编号已存在」)。要么换编号,要么留空让后端生成
预览不消耗序号 preview 只渲染,不落库、不取流水号。可以反复点预览,不会把当天的编号跳号
删掉的编号不会被重用 删除报告是软删除,编号仍然占位,当天的流水**只增不减**。删掉 …-0001 之后下一份是 …-0002,**不会回到 0001**。前端不要假设「编号连续」
用哪个版本(出件) generate 要求版本已发布,草稿会被拒(1_070_101_003)。出件会把版本号冻结进实例、而实例并不存内容,用草稿出件等于给历史报告埋一颗「版本号对得上、内容已经变了」的雷
用哪个版本(预览) preview 接受草稿版本,也接受已停用版本,只要版本存在且有画布内容即可。设计器的主流程就是「保存草稿 → 点预览」,草稿会变在预览这里没有后果(不落库、不冻结数据、不消耗编号)
currentVersion 为空时 不传 version 且模板 currentVersion 为空 → 1_070_101_007。**此时 preview 也拿不到版本**,设计器预览必须显式传 version(如刚保存的草稿 v1.1)
数据快照冻结 出件时把「判定后的数据」整份冻结进实例。业务系统里的数据后来怎么变,**都不影响这份历史报告**
重新生成动的是什么 只重渲**排版产物**,用的是**实例里冻结的快照**与**同一个模板版本**。数据、报告编号都不变
重新生成不要求版本仍已发布 版本后来被停用,历史报告也不能因此打不开,所以 regenerate 允许用已停用的版本
判定结果由后端算 result / resultText / conclusion / passRate / total / passCount / failCount 一律以**后端算出的**为准,调用方传了也会被覆盖
result 与 resultText 分工 result 是机器值(PASS / FAIL / 空字符串),resultText 是给人看的中文(合格 / 不合格 / 待判定)。**画布上显示的是 resultText**,所以产物 HTML 里搜不到 PASS 字样,别据此以为没判定
无法判定的项 既没有规格上下限、也没有命中任何规则的检验项 → result 为空、resultText 为 待判定,**不计入 passCount/failCount 但计入 total**,所以可能出现 total=3 pass=1 fail=1(合格率按 pass/total 算)。同时它会出现在 errors 里(如「第 1 项「外观」没有规格上下限也没有判定规则,无法判定」)
数据缺口清单 模板绑定了但数据里没有的字段、以及被安全策略拦下的模板属性,都会出现在 errors 里。**它只在 preview/generate/regenerate 的响应里出现,不落库** —— 事后回看历史报告时**看不到**当时有哪些字段缺值。需要留痕请在出件时自行记录
PDF 打的是哪份 HTML 打的是**实例里已经存着的那份 HTML**,不重新渲染。所以「详情页看到的」与「打出来的」必然出自同一份字节,**不会出现页面上好好的、打出来变形**
重渲即作废已归档 PDF 调 regenerate 之后,之前导出的 PDF 会被作废(附件列表会变成空)。原因:产物 HTML 换了,再留着旧 PDF 就是「页面是新排版、下载到的是旧文件」的静默不一致。**前端在 regenerate 成功后要把「下载 PDF」按钮恢复成「导出 PDF」**
PDF 纸面与页面预览一致 A4/A3/A5/Letter、横竖向、页边距都取自模板的纸张配置,与 HTML 里 @page 用的是同一份数字,不存在「预览 210mm、PDF 打 210.5mm」这类漂移
PDF 是"显式生成"的 只有调 pdf 接口才会有 PDF。generate / regenerate 都不会顺带生成 PDF
重复导出是安全的 再导一次会替换掉旧的归档(附件仍是 1 条),不会越导越多、也不会留孤儿文件。**可以放心让用户重复点**
出件很慢时先告诉用户 首次导出或浏览器刚崩过要重启时需要几秒,接口在 durationMs 里如实返回。前端建议点按钮后转圈直到响应,不要自己设定时器判超时
并发上限 服务端同时进行的 PDF 打印有上限(默认 2),排队超过 30 秒会返回 1_070_103_007。批量导出时不要并发猛打,串行或小并发
单份报告文件多页 长报告(检验项多)会自动分页,printBackground 已开,判定底色会打出来;表头跨页重复
PDF 功能可被运维关掉 部署环境没浏览器时会关掉(yudao.qcreport.pdf.enabled=false),此时 pdf 接口返回 1_070_103_008。前端应原样提示,不要当成系统异常
删除实例 软删除。删掉后 get 返回 null(不报错),前端要兜空。**已归档的 PDF 会一并清理**
模板停用 停用的模板**不能出新件**;已有历史报告不受影响

注意事项

  • 列表/详情不要展示 dataSnapshot 字段:get 与 page 都不返回它,这是有意的(体积大)。
  • get 对已删除的实例返回 data:null,不是报错。所有按 id 取详情的入口都要判空。
  • HTML 渲染要注意样式隔离:产物是完整 HTML 文档(自带 @page 与样式),
    直接塞进页面会污染全局样式,建议用 iframe 承载。
  • 预览响应里的 reportNo 可能为空,这是正常的(预览不生成编号),不要拿它当出件结果。
  • inspectionItems 不能传空数组:没有检验项的「报告」没有意义,后端会直接报错。
  • businessId 不会是数字类型:虽然表里是 varchar,但调用方给的可能是工单号、批次号一类的字符串,前端别按 Long 处理。
  • errors 可能很长,尤其模板刚做好时。建议折叠展示 + 显示条数。
  • 权限:接口权限码是 qc-report:instance:query / :create / :delete / :export。
    已入库到 system_menu(见「API」节下的说明)。create 是**预置**的:本轮页面没有出件入口,
    它是给后续 MesQcReportApi 轮准备的,不是遗漏。
  • 「AI 导入」按钮用独立权限码 qc-report:template:ai-import:能改模板不等于能用大模型识别文件
    (每次调用都产生费用),两者必须分开授权,不能合并到模板的编辑权限里。
  • 预览必须 sandbox=""(空属性,不是省略该属性):空值 = 完全禁用脚本、表单与同源,只让 CSS 生效。
    产物 HTML 是渲染结果,不需要也不应该让它执行任何东西。
  • 重复导出不堆文件已实测:同一实例连点 3 次,库中始终只有 1 条有效附件
    (前两条附件行与 blob 行均为 deleted=1),磁盘目录里也只留当前这一份 PDF。
  • 导出的 PDF 中文依赖服务器的字体:开发机(Windows)自带微软雅黑没问题;
    生产 Linux 服务器若没装 CJK 字体会打成方框。这属于部署前置,前端不需要处理,但**验收时要在目标环境上看一眼中文**。
  • pdf 接口的返回地址是签名的临时地址,带过期时间与使用次数限制,
    不要把它写进数据库、也不要放进列表接口缓存,需要时重新查附件列表或重新导出。
  • 附件列表里的 name 是 PDF 文件名(QR20260918-0001.pdf),下载时用它命名即可。

错误码

错误码 报文 触发场景
1_070_100_000 模板不存在 templateId 找不到
1_070_100_002 该模板已停用,不能生成报告 模板 status=1
1_070_101_000 模板版本不存在 version 传了但查不到
1_070_101_003 该模板版本未发布 generate 的 version 指向草稿(**preview 不报这个错**)
1_070_101_007 该模板还没有已发布的版本,无法生成报告,请先在设计器中发布一个版本 不传 version 且模板 currentVersion 为空(preview 同样会报,此时须显式传 version)
1_070_101_008 该模板版本没有画布内容,无法生成报告,请先在设计器中设计并保存模板内容 版本有记录但画布是空的
1_070_102_000 报告实例不存在 id 找不到(get 除外,它返回 null)
1_070_102_001 报告编号「X」已被占用,请换一个编号,或留空由系统按 QR+日期+流水 自动生成 指定了重复编号。**含已被软删除的编号**(删掉的号不回收)
1_070_102_001 自动生成报告编号时连续 5 次与已有报告冲突,请稍后重试;也可在请求里显式指定一个未被占用的报告编号 仅在同一时刻有多个出件请求抢同一个流水号时出现;正常使用不应看到
1_070_102_002 报告实例缺少数据快照,无法重新生成 快照为空的脏数据
1_070_102_003 报告实例没有渲染产物 HTML,无法生成 PDF,请先对该报告执行「重新生成」 pdf 时实例的 render_html 为空(脏数据)。**提示里已给出动作:先「重新生成」**
1_070_103_004 PDF 渲染浏览器启动失败,请检查 Chromium 是否已安装 服务器没装 Chrome / 配置的 executable-path 不对。报文里会带上当前配置的浏览器来源与实际原因
1_070_103_005 PDF 生成失败 打印过程中浏览器报错(含等页面资源超时)
1_070_103_006 PDF 已生成,但归档到附件库失败 打印成功、存档失败。**注意它不等于「PDF 没生成」**,前端的提示文案不要写成「生成失败」
1_070_103_007 当前 PDF 渲染任务已达上限,请稍后重试 并发排队超过 30 秒。报文里带当前并发上限值
1_070_103_008 PDF 出件功能未启用,请联系管理员 运维把 yudao.qcreport.pdf.enabled 关成了 false
1_070_103_000 检验项列表为空,没有可判定的内容:请至少传入一个检验项(inspectionItems)再出件 inspectionItems 为空