# 智能质检报告 — 出件接口 前端联调方案 > 后端模块:`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 | 否 | 备注 | **请求示例** ```json { "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\ | 数据缺口清单,见下 | **`generate` 响应** | 字段 | 类型 | 说明 | |---|---|---| | `id` | Long | 实例编号,拿它去查详情 | | `reportNo` | String | 最终生效的报告编号(自动生成或入参指定) | | `templateVersion` | String | 本次使用的模板版本号 | | `status` | Number | 报告状态:`0` 生成中 / `1` 生成成功 / `2` 生成失败。当前只会返回 `1` | | `errors` | Array\ | 数据缺口清单 | **`regenerate` 响应** | 字段 | 类型 | 说明 | |---|---|---| | `id` | Long | 实例编号 | | `reportNo` | String | 报告编号(与生成时相同) | | `errors` | Array\ | 数据缺口清单 | **`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\ | 创建时间区间,`["开始","结束"]`,格式 `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` 为空 |