后端模块:
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。
preview,generateid → 调 get 取 renderHtml 展示;需要看「当时用的什么数据」再调 snapshot。regenerate 用**冻结的快照**重渲,覆盖产物 HTML。数据与编号都不变。pdf 接口 → 后端打印并归档 → 接口返回本次的下载地址(临时签名)。带入关系要点
templateId 是主线,从模板列表带入设计器、再带入出件。version 可以不传:不传时后端取**模板当前的版本**(currentVersion);一定要用某个特定版本才需要显式传。currentVersion 的说明。reportNo 可以不传:不传时由后端按 QR + 日期 + 4 位流水 自动生成。businessId / businessType 只做留痕与查询过滤,后端不校验、不解析它指向的单据。| 方法 | 路径 | 说明 | 权限码 |
|---|---|---|---|
| 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(本次重启了浏览器)。**仅供参考与排查**,前端不需要分支处理 |
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 |
报告实例表里**没有也不会有** 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」按钮的逻辑建议:
pdf 接口,成功后用返回的 downloadURL 直接下载(或重新查一次附件列表刷新状态)。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 取详情的入口都要判空。@page 与样式),iframe 承载。reportNo 可能为空,这是正常的(预览不生成编号),不要拿它当出件结果。inspectionItems 不能传空数组:没有检验项的「报告」没有意义,后端会直接报错。businessId 不会是数字类型:虽然表里是 varchar,但调用方给的可能是工单号、批次号一类的字符串,前端别按 Long 处理。errors 可能很长,尤其模板刚做好时。建议折叠展示 + 显示条数。qc-report:instance:query / :create / :delete / :export。system_menu(见「API」节下的说明)。create 是**预置**的:本轮页面没有出件入口,MesQcReportApi 轮准备的,不是遗漏。qc-report:template:ai-import:能改模板不等于能用大模型识别文件sandbox=""(空属性,不是省略该属性):空值 = 完全禁用脚本、表单与同源,只让 CSS 生效。deleted=1),磁盘目录里也只留当前这一份 PDF。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 为空 |