# 质检单 → 质检报告 出件 前端联调方案
> 后端模块:`yudao-module-qcreport` 接口前缀:`/admin-api/qc-report/instance`
> 上游数据源:`yudao-module-mes`(`MesQcReportApi`,本模块不直接读 MES 表)
> 本轮范围:**由 MES 质检单(IQC/IPQC/OQC/RQC)直接出报告**。前端只需选「哪张单据 + 用哪张模板」,
> 数据全部由后端从 MES 取,**前端不再自己拼 `context`**。
## 涉及页面
| 页面 | 路由 | 说明 | 状态 |
|---|---|---|---|
| MES 质检单列表(IQC/IPQC/OQC/RQC 四个页面) | `/mes/qc/iqc`、`ipqc`、`oqc`、`rqc` | 行操作新增「出报告」:选模板 → 调本接口 → 跳报告实例列表(已带筛选条件) | **已落地** |
| 报告实例列表页 | `/qc/report-instance` | 生成后按 `businessType` + `businessId` 反查得到;搜索表单已加「业务单据」字段并支持路由带入 | 已落地(`views/mes/qc/report/instance/index.vue`) |
| 模板设计器 | `/qc/report-template` | 无改动(模板需先在设计器里配好检验项表格并发布) | 已落地 |
> 与 `qc_report_instance_frontend_integration.md` 的分工:那份讲的是「调用方自己准备数据」的通用出件;
> 本份讲的是「数据从 MES 取」的专用入口。两者最终都落到同一个实例表、同一个列表页。
## 业务流程与数据带入
1. **质检单 → 出报告**:用户在质检单**列表行操作**里点「出报告」→ 前端弹窗列出可选模板
(**只列出 `reportType` 与该单据类型一致的模板**)→ 调 `generate-from-qc`。
2. **后端六道校验**(按业务优先级,只报第一处,不一次抛多条):
类型合法 → 单据存在 → 已完成检验 → 已填判定 → 模板类型匹配 → 有可用检验项。
3. **后端取数并出件**:`MesQcReportApi` 装配质检单四层结构(单头 / 判定依据 / 样品 / 实测值)
→ 归一成报告上下文 → 走**既有的** `generate` 流程(版本发布校验、编号生成、快照冻结、判定引擎全部照旧)。
4. **结果回到前端**:弹窗内就地展示结果(`reportNo` / `itemCount` / `undecidableCount` / `warnings` / `errors`),
底部「查看报告列表」跳转报告实例列表页并带上筛选条件。
5. **反查**:报告实例列表页用 `businessType=mes_qc_iqc` + `businessId=1` 即可查出该单据出过的所有报告。
**带入关系要点**
- `qcType` + `qcId` 是主线,从质检单列表行带入。`qcType` 用字典 `mes_qc_type` 的**数值**(`1`/`2`/`3`/`4`)。
- `templateId` 由用户在弹窗里选。**前端必须先按 `reportType` 过滤模板列表**,否则用户会选到后端必然拒绝的组合。
- `version` 不传时后端取模板的 `currentVersion`(要求已发布)。设计器刚存了草稿想立刻出件才需要显式传。
- `reportNo` 不传由后端自动生成(`QR + 日期 + 4 位流水`)。
- **列表 → 报告实例的反查筛选是路由 query 带入**:跳转时带 `businessType`(表名,如 `mes_qc_oqc`)
与 `businessId`(质检单主键的字符串形式)两个参数,报告实例列表页在挂载时把它们预填进搜索表单,
用户落地即看到刚出的报告,不需要自己再筛一遍。
## UI 入口(已落地)
| 位置 | 元素 | 展示规则 | 说明 |
|---|---|---|---|
| 质检单列表行操作 | 「出报告」按钮 | **仅当 `status === 已完成` 时显示** | 与「编辑」「删除」(仅草稿可见)互斥,避免用户对未完成的单据点 |
| 弹窗标题 | `出报告 - {单据编号}` | — | 编号由列表行带入,仅用于展示 |
| 弹窗选模板区 | 单选模板列表 | 由后端 `/qc-report/template/selectable` 返回:`reportType` 与本单类型一致、`status=启用`、**`currentVersion` 指向的版本处于「已发布」、且该版本的画布有可渲染的内容** | 无可选项时给空状态提示,引导先去模板设计器配好并发布 |
| 弹窗结果区 | 成功提示 + 告警区 | 生成成功后就地切换,不关弹窗 | `undecidableCount > 0` 用**蓝色信息条**(不是黄色告警):这项数里绝大多数是「过程参数」这类正常情况,标黄会在几乎每份报告上误报 |
| 弹窗底部 | 「查看报告列表」 | 结果区才出现 | 关弹窗后跳报告实例列表并带上 `businessType` + `businessId` |
> **弹窗标题不带类型中文名**(仍保持这个做法):`mes_qc_rqc` 这一路曾有措辞分歧——
> 后端枚举、表注释、菜单权限子项都写作「退货检验」,只有菜单父项与 `mes_qc_type` 字典写作「退料检验」。
> 现已统一为**「退货检验」**(理由:RQC 的对象同时覆盖生产退料、销售退货、售后退货,
> 「退货检验」是能覆盖三者的说法;且它本就是多数派)。标题仍不带类型名,少一处需要跟着变的文案。
## API
| 方法 | 路径 | 说明 | 权限码 |
|---|---|---|---|
| POST | `/qc-report/instance/generate-from-qc` | 由质检单生成报告(数据从 MES 取) | `qc-report:instance:generate` |
| GET | `/qc-report/template/selectable` | 取「可用于出件」的模板列表 | `qc-report:template:query` |
> `qc-report:instance:generate` 是**独立权限码**,与 `:create`、`:query` 都不通用。
**`GET /qc-report/template/selectable` 请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reportType` | String | 是 | 报告类型,即 `mes_qc_type` 的**数值字符串**,如 `"1"` / `"4"`。传 `"IQC"` 这类英文名会一个都查不到 |
**响应:** `Template[]`(**数组,不是分页结构**),字段与模板分页接口一致。
**为什么单开这个接口,而不是在前端过滤模板分页结果:**
两条过滤条件都落在**版本表**上,分页接口返回的 `currentVersion` 只是模板表上的一个字符串,看不到版本状态与画布内容。
1. **版本未发布**:停用一个版本**不会**清空模板的 `currentVersion`,于是会出现「`currentVersion` 有值、但那个版本已停用」的模板;这种模板选中后出件必然失败(`1_070_101_003`)。
2. **画布没有可渲染内容**:GrapesJS 把「打开过设计器但一个组件都没放」的项目存成 `{"assets":[],"styles":[]}`——它非空,所以旧判据会放它过去,渲染时又取不到根组件、body 渲染成空串,**不报错**,用户最终拿到一张白纸 PDF。
两条都是「看起来能选、选了必出问题」,判断它们要读版本行,放在后端做才是单点;
前端拿着分页结果自己推导会得到「看起来对但会漏」的结论。
> 已补入 `system_menu`:`id=1075416`,名称「报告实例质检单出件」,类型 3(按钮),
> 挂在「报告实例」(`parent_id=1075410`)下,`sort=5`。非超管角色需授予该菜单后才可见按钮 / 可调用接口
> (超管由代码直返全部菜单,不受影响)。
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `qcType` | Integer | **是** | 质检类型:`1` 来料检验(IQC) / `2` 过程检验(IPQC) / `3` 出货检验(OQC) / `4` 退货检验(RQC) |
| `qcId` | Long | **是** | 质检单编号(四张表各自的主键) |
| `templateId` | Long | **是** | 报告模板编号。**其 `reportType` 必须与 `qcType` 一致** |
| `version` | String | 否 | 模板版本号,如 `v1.0`。不传则取模板的 `currentVersion` |
| `reportNo` | String | 否 | 报告编号。传了就用它(撞号会报错),不传由后端自动生成 |
**请求示例**
```json
{
"qcType": 1,
"qcId": 1,
"templateId": 1,
"version": "v1.1"
}
```
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | Long | 报告实例编号,拿它去查详情 / 跳转 |
| `reportNo` | String | 最终生效的报告编号 |
| `templateVersion` | String | 本次使用的模板版本号 |
| `itemCount` | Integer | 写入报告的检验项条数 |
| `undecidableCount` | Integer | **没有判定结论的项数**(「无判定规则」+ 规则执行失败的「待判定」),见「业务规则」 |
| `warnings` | Array\ | 软提示:样品的取舍、没成组的分组项、为组头补出来的行、指标已删除、未录入实测值 |
| `errors` | Array\ | 渲染期数据缺口(与通用出件接口同一套机制) |
**响应示例**
```json
{
"code": 0,
"data": {
"id": 38,
"reportNo": "QR20260919-0001",
"templateVersion": "v1.1",
"itemCount": 14,
"undecidableCount": 5,
"warnings": [
"以下 2 个分组项在本单的检验明细里没有自己的行(多半是检验模板只选了子项),报告里为它们各补了一行来显示组名与公式:「水分、酸含量」",
"以下 1 个指标是指标库里的分组项,但本单没有属于它们的子项,报告里按独立检验项列出、组名不合并:「色度」"
],
"errors": []
}
}
```
### 报告上下文新增字段(设计器可绑定)
出件时后端写进报告的每个检验项上,新增了下面 4 个字段。它们不在这条接口的响应里,而是体现在**报告上下文**中,
供模板在设计器里绑定(「分组检验项表」组件已内置这些绑定,一般不需要手工配):
| 字段 | 类型 | 取值 | 说明 |
|---|---|---|---|
| `item.requirement` | String | 组头行 = 该指标的公式;其余行 = 单据上的标准要求 | 「检测要求」列。与 `standardValue` 并存不合并:两者来源不同(一个是指标主数据的公式,一个是单据行的标准要求) |
| `item.group.childName` | String | 组内子项 = 自己的名字;组头行与独立项 = 空串 | 「子项」列 |
| `item.group.span` | Integer | 组内第一行 = 该组总行数;同组其余行与独立项 = `1` | 「检验项目」列的合并行数 |
| `item.group.hidden` | String | 组内第一行与独立项 = 空串;同组其余行 = `hidden` | `hidden` 属性靠「在不在」起作用(写成 `hidden="false"` 照样隐藏),所以只有「空串 = 这一格可见」这一个可判定的形态 |
**没有分组的报告不会带 `group`**:整个报告一个分组都没有时,`item.group` 这个键不出现,模板里也就不会多出一层空格子。
一旦报告里有分组,则**每一项都会带 `group`**(独立项给空串与 `1`)—— 字段缺席会让合并属性被跳过,反而把该隐藏的格子露出来。
**两个字段必须成对使用**:`item.group.span` 与 `item.group.hidden` 只作用在「检验项目」这一列上。
不要把它们绑到别的列(如「检测要求」):组内子项各自的检测要求要逐行显示,并进组头格会把它们翻掉。
## 字段展示规则
| 字段 | 展示位置 | 说明 |
|---|---|---|
| `undecidableCount` | 生成成功后的提示条 | **不要当成故障报警**。它数的是「没有结论的项」,其中绝大多数是「过程参数」这类正常情况(试样质量 m、称量瓶 m0 —— 只供公式取数,本来就没有可判定的规格)。提示文案建议「本次有 N 项没有判定结论」;**若确实要区分**「正常无规则」与「规则没跑通」,请以报告详情里各检验项的 `resultText` 为准(`无判定规则` / `待判定`),不要用这个总数反推 |
| `warnings` | 生成成功后的提示条 | 建议可折叠;内容已经是完整中文句子,直接原文展示即可,不要改写 |
| `errors` | 同上 | 数据缺口,与通用出件接口同义 |
| `reportNo` | 成功提示、跳转后详情页头部 | **业务唯一键**,建议带「复制编号」 |
| `id` | 不展示 | 用来跳转 `/qc/report-instance` 详情 |
## 业务规则说明
| 场景 | 规则 |
|---|---|
| **谁提供数据** | 后端。前端**不要**自己拼 `context` 传进来,本接口不接受 `context` 参数 |
| **类型不合法** | `qcType` 不在 1~4 → `1_070_105_000`,报文里列出收到的值与四个合法值 |
| **单据不存在** | → `1_070_105_001`,报文带单据类型中文名与 ID |
| **单据未完成检验** | 状态不是「已完成」→ `1_070_105_002`,报文带**当前状态中文名**,用户一眼知道卡在哪一步 |
| **单据没有判定** | 已完成但 `checkResult` 为空 → `1_070_105_003`,报文列出四个可选判定 |
| **模板类型不符** | 模板 `reportType` ≠ `qcType` → `1_070_105_005`,报文**同时给出模板类型与单据类型两边的中文名**。**前端应在选模板时就把不符的过滤掉**,这条是兜底 |
| **模板没配报告类型** | 模板 `report_type` 为空 → 同样 `1_070_105_005`,但报文改成提示「请先给该模板设置报告类型」 |
| **没有可用检验项** | → `1_070_105_006`,报文说明具体原因(如「只有分组标题」) |
| **能出多份报告吗** | **能**。同一张质检单可以反复出件,每次生成一份独立报告(历史留痕需要)。**不做重复生成拦截**,前端也不必拦 |
| **检验项怎么来的** | 质检单「判定依据行」(`max_threshold`/`min_threshold`/标准值)与「实测值明细」(按样品 × 指标)**交叉**展开。**不做任何指标过滤**,全部进报告 |
| **多个样品怎么办** | 同一个(样品 × 指标)展开成**一条**报告检验项。样品编号只在**样品数 > 1** 时折进该项的备注(如「样品 S1」),单一样品时不写,避免污染 |
| **样品太多怎么办** | 样品数超过 3 时,报告头的「样品编号」字段**留空**并发 `warnings`(列出前 3 个与前 N 总数),不截断明细数据 |
| **分组会怎样(本轮改动)** | 分组项**不再被跳过**。分组父项(如「水分」)作为一行进报告,组内子项紧随其后,组名在报告上纵向合并 —— 报告能看出哪些子项属于哪一组,父项的公式也不再丢 |
| **组名与公式放哪** | 组名放「检验项目」列并向下合并;公式放组头那一行的「检测要求」列。**组内子项各自的检测要求仍逐行保留**,二者不互相覆盖 |
| **谁是组头** | 后端按指标的 `parent_id` 判归属(不看行序、不看数值类型),并给每行下发 `group.span`(合并几行)与 `group.hidden`(这一格要不要让位)。前端**不需要也不会**自己算合并行数 |
| **分组项本单没有子项** | 按独立检验项列出、组名不合并,并把名字收进 `warnings`(「…是指标库里的分组项,但本单没有属于它们的子项…」) |
| **组头本单没有行** | 若检验模板只勾了子项,后端**为组头补一行**来放组名与公式,并在 `warnings` 里说明为哪几个组头补了行。补出来的行实测值为空,判定为「无判定规则」,不影响合格率与结论 |
| **指标被删了会怎样** | 单据行引用了一个已被删除的指标 → 该条跳过,`warnings` 里报「有 N 条明细所引用的检验指标已被删除」 |
| **没录实测值会怎样** | 该项照常进报告,但 `warnings` 提示「第 N 项「X」尚未录入实测值」;**组头的锚点行除外**(它按定义没有实测值,值都在子项行上,报出来是误报) |
| **报告行序变了** | 从此前的「行表顺序」改为按指标的 `sort_order` 升序(缺失排最后,同值保持行序),组内子项按各自的 `sort_order`。这样报告顺序与检验单的设计意图一致;默认不需要前端做任何事 |
| **哪张画布组件能渲染分组** | 需要模板里用**「分组检验项表」(`GroupedQualityTable`)**。用它之外的表(如基础「检验项表格」)不会出现合并效果 —— 数据照常下发,只是模板没画。二者用其一:有分组用前者,纯平层用后者 |
| **判定谁算** | 报告级的 `result`/`resultText`/`conclusion`/`passRate` 由**后端判定引擎**算(逐项规则 → 规格上下限 → 数据自带结果),与通用出件一致 |
| **质检单的原判定去哪了** | 存进报告的 `report.qcResult` / `report.qcResultText` 两个**新字段**,与引擎判定**并存、互不覆盖**。质检单上的判定是人工拍的板(合格/特采/不合格退货/不合格报废),引擎只有三态装不下它,所以分开存 |
| **`undecidableCount` 怎么算** | 数「快照里 `result` 为空串的项」。这正是引擎对「待判定」的定义,也顺带把「卡在规则报错上」的项算进去,不另算一遍判定条件 |
| **原判定是数字** | `qcResult` 是数字字符串(`"1"`~`"4"`),`qcResultText` 才是中文(合格/特采/不合格退货/不合格报废)。**画布上显示 `qcResultText`** |
| **模板必须已发布** | 与通用出件一致,草稿版本会被拒(`1_070_101_003`)。复用既有错误码,本接口不另造 |
| **模板已停用** | 复用 `1_070_100_002` |
| **报告编号撞号** | 复用 `1_070_102_001`。已软删除的编号**不回收** |
| **数据快照冻结** | 出件时把判定后的数据整份冻结。MES 里的单据后来怎么改,**都不影响这份历史报告** |
## 注意事项
- **`engine/context.ts` 的 `qcResult` / `qcResultText` 已补齐**(类型接口 + `emptyReport()` 空串默认值),
设计器的绑定面板里现在可以选到这两个字段,画布上也能绑。若要用它们,模板需自行绑到某个文本组件上。
- **模板列表要按 `reportType` 过滤**:模板的 `report_type` 存的是字典数值字符串(`"1"`/`"2"`/`"3"`/`"4"`),
与 `qcType` **直接相等比较**即可,不要拿 `"IQC"` 这类英文名去比。
- **`qcType` 传数值不传字符串名**:后端按 `1`~`4` 识别,传 `"IQC"` 会得到 `1_070_105_000`。
- **不要缓存 `warnings`**:它只在本次响应里出现,**不落库**。事后回看历史报告时看不到当时有哪些提示。
需要留痕请在出件成功时自己记一份。
- **`warnings` 与 `errors` 分工**:`warnings` 是「数据取舍的软提示」(跳过、截断、缺值),
`errors` 是「模板绑定了但没数据的字段」这类渲染期缺口。**两者都要展示**,不要只显示其中一个。
- **`itemCount` 不等于质检单的行数**:指标被删会被跳过、一个指标多条实测值会展开成多条、
组头在本单没有行时后端会补一行,所以这个数**只能用来展示**,不要拿它去和质检单行数做断言。
- **部分字段刻意不带过去**:MES 侧检验项上的「检验方法」(`checkMethod`)与「检验工具」(`tool`)
在报告上下文里**没有承载位**,本轮未传递。若报告要展示这两列,需要先扩 `InspectionItem` 契约
(那是改动前端可见的数据契约,需单独确认后再做)。
- **质检单上没有「部门」**:报告的 `department` 字段恒为空,不要拿别的字段硬凑。
- **批次号取哪个字段按类型不同**:来料检验取供应商批号,出货/退货取批次号,过程检验两者都没有(只有工单号)。这是后端按业务语义定的,前端不用管。
- **报告实例落库时 `businessType` 是表名**(`mes_qc_iqc` / `mes_qc_ipqc` / `mes_qc_oqc` / `mes_qc_rqc`),
与前端路由/字典里的英文枚举同名但与字典数值不同,反查时**直接用这四个字符串**。
- **失败时不要重试出件**:五道校验失败都是「数据或选择不对」,重试同样会失败。按报文提示去改数据或换模板。
- **失败报文由请求拦截器自动弹出,业务代码不要再弹一次**:`code != 0` 时拦截器已把后端 `message`
显示成 toast,调用处只写 `catch {}` 即可,重复提示会让用户看到两条一样的报错。
- **权限**:本接口权限码 `qc-report:instance:generate`,已补入 `system_menu`(`id=1075416`)。
## 错误码
### 本接口新增(`1_070_105_xxx`)
| 错误码 | 报文 | 触发场景 |
|---|---|---|
| `1_070_105_000` | 质检类型「9」不合法,只支持 IQC(来料检验)、IPQC(过程检验)、OQC(出货检验)、RQC(退货检验) | `qcType` 不在 1~4 |
| `1_070_105_001` | 来料检验的质检单(ID=999999)不存在或已被删除,请刷新列表后重新选择 | `qcId` 查不到 |
| `1_070_105_002` | 来料检验的质检单「IQC20260919001」当前状态为「草稿」,尚未完成检验,不能生成报告。请先把该单据提交并完成检验判定后再生成 | 单据状态非「已完成」 |
| `1_070_105_003` | 来料检验的质检单「IQC20260919001」尚未填写检验判定(判定为空),不能生成报告。请先在该单据上填写判定结论(合格 / 特采 / 不合格退货 / 不合格报废)后再生成 | 已完成但 `checkResult` 空 |
| `1_070_105_005` | 报告模板的报告类型是「出货检验」,与本次质检单的类型「来料检验」不一致,无法生成报告。请重新选择一张来料检验的报告模板 | 模板与单据类型不符(或模板未配类型) |
| `1_070_105_006` | 来料检验的质检单「IQC20260919001」(ID=1)没有任何检验指标,无法生成报告。请先在该单据上录入检验指标与实测值后再生成 | 单据没有任何检验指标行。若其中部分明细引用的指标已被删除,报文会在括号里补一句「其中 N 条明细所引用的检验指标已被删除」 |
### 复用既有错误码(本接口不另造副本)
| 错误码 | 报文 | 触发场景 |
|---|---|---|
| `1_070_100_000` | 模板不存在 | `templateId` 找不到 |
| `1_070_100_002` | 该模板已停用,不能生成报告 | 模板 `status=1` |
| `1_070_101_000` | 模板版本不存在 | `version` 传了但查不到 |
| `1_070_101_003` | 该模板版本未发布 | `version` 指向草稿 |
| `1_070_101_007` | 该模板还没有已发布的版本,无法生成报告,请先在设计器中发布一个版本 | 不传 `version` 且 `currentVersion` 为空 |
| `1_070_101_008` | 该模板版本没有画布内容,无法生成报告,请先在设计器中设计并保存模板内容 | 版本有记录但画布空 |
| `1_070_102_001` | 报告编号「X」已被占用,请换一个编号,或留空由系统按 QR+日期+流水 自动生成 | 指定了重复编号 |
> **与原始方案文档的偏离(已落地口径)**:方案文档 §7 里列的 `105_004`(模板不存在)与 `105_007`(模板无已发布版本)
> **未实现**,改为复用上面的既有码。理由:那几条文案本来就是精确的,再造一遍只会多两条会漂移的副本。