4 小时以前 ed01be06d17f1159d32e7fa2133a32f2986d3b9f
refactor(mes): 替换物料服务为MDM API并完善质检Word导出功能

- 将MesMdItemService和MesMdUnitMeasureService替换为MdmItemApi和MdmUnitMeasureApi
- 更新IPQC、IQC、OQC、RQC控制器中的物料和计量单位查询逻辑
- 在待检任务服务中添加MDM物料信息填充功能
- 完善产品检验单Word导出功能,实现检验/复核/批准签字自动带出姓名
- 更新文档中关于Word导出功能的描述,明确签字行的实现方式
- 修改本地环境数据库连接配置以适配新环境
- 更新流程图和数据文件中的相关描述信息
已修改11个文件
205 ■■■■■ 文件已修改
docs/project-business/data/flows.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/index.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/05-quality-flow.mmd 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_product_report_word_integration.md 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/MesQcRqcController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcProductReportWordBuilder.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/pendinginspect/MesQcPendingInspectServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcProductReportWordBuilderTest.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/flows.json
@@ -29,13 +29,13 @@
       {"stage": "联动", "action": "审批通过按工序标记: 倒冲→自动物料消耗扣线边库(允许负库存);质检→产出待检行等 IPQC;关键非检→直接产出入库", "roles": ["系统自动"]},
       {"stage": "完工", "action": "仅末道工序回写工单已生产数量,达标自动完成;MPS 累计判断完成并发事件给销售订单", "roles": ["系统自动"]}]},
    {"id": "quality", "name": "质量流程", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + 判定四果(合格/特采/退货/报废;OQC 改按产品等级 1型/2型/废品,仅废品判不合格) + NCR 评审处置关闭;AI 提供判定建议;NCR 当前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论/质检章/检验·复核·批准签字留空手填,Word 打印)。",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + 判定四果(合格/特采/退货/报废;OQC 改按产品等级 1型/2型/废品,仅废品判不合格) + NCR 评审处置关闭;AI 提供判定建议;NCR 当前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论带出检验结果、检验·复核·批准签字带出检测人·复核人·审核人姓名、质检章留空盖章,Word 打印)。",
     "steps": [
       {"stage": "来料", "action": "到货通知/外协入库→IQC,结论回写推进待入库或触发退货", "roles": ["IQC 质检员"]},
       {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC 质检员"]},
       {"stage": "出货", "action": "销售出库提交→OQC(检测结果按产品等级 1型/2型/废品必填,1型/2型判行合格,废品判行不合格),全部合格后出库单进入待拣货,出现废品则整单取消", "roles": ["OQC 质检员"]},
       {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC 质检员"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report 聚合后前端打印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word 导出产品检验单Word(无技术指标列·判定结论带出检验结果·质检章/检验·复核·批准签字留空手填·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report 聚合后前端打印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word 导出产品检验单Word(无技术指标列·判定结论带出检验结果·检验·复核·批准签字带出检测人/复核人/审核人姓名·质检章留空盖章·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "不合格", "action": "NCR: 待评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}]},
    {"id": "warehouse", "name": "仓储流程", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6",
     "summary": "15 类单据驱动库存台账(六元组、四量)变动,全部经事务流水留痕;支持调拨在途、盘点调整、批次/SN 追溯、库存占用与四级冻结、线边虚拟库负库存;对外 WmsStockApi 供 ERP 收发。",
docs/project-business/index.html
@@ -516,13 +516,13 @@
       {"stage": "联动", "action": "审批通过按工序标记: 倒冲→自动物料消耗扣线边库(允许负库存);质检→产出待检行等 IPQC;关键非检→直接产出入库", "roles": ["系统自动"]},
       {"stage": "完工", "action": "仅末道工序回写工单已生产数量,达标自动完成;MPS 累计判断完成并发事件给销售订单", "roles": ["系统自动"]}]},
    {"id": "quality", "name": "质量流程", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + 判定四果(合格/特采/退货/报废;OQC 改按产品等级 1型/2型/废品,仅废品判不合格) + NCR 评审处置关闭;AI 提供判定建议;NCR 当前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论/质检章/检验·复核·批准签字留空手填,Word 打印)。",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + 判定四果(合格/特采/退货/报废;OQC 改按产品等级 1型/2型/废品,仅废品判不合格) + NCR 评审处置关闭;AI 提供判定建议;NCR 当前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论带出检验结果、检验·复核·批准签字带出检测人·复核人·审核人姓名、质检章留空盖章,Word 打印)。",
     "steps": [
       {"stage": "来料", "action": "到货通知/外协入库→IQC,结论回写推进待入库或触发退货", "roles": ["IQC 质检员"]},
       {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC 质检员"]},
       {"stage": "出货", "action": "销售出库提交→OQC(检测结果按产品等级 1型/2型/废品必填,1型/2型判行合格,废品判行不合格),全部合格后出库单进入待拣货,出现废品则整单取消", "roles": ["OQC 质检员"]},
       {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC 质检员"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report 聚合后前端打印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word 导出产品检验单Word(无技术指标列·判定结论带出检验结果·质检章/检验·复核·批准签字留空手填·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report 聚合后前端打印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word 导出产品检验单Word(无技术指标列·判定结论带出检验结果·检验·复核·批准签字带出检测人/复核人/审核人姓名·质检章留空盖章·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "不合格", "action": "NCR: 待评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}]},
    {"id": "warehouse", "name": "仓储流程", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6",
     "summary": "15 类单据驱动库存台账(六元组、四量)变动,全部经事务流水留痕;支持调拨在途、盘点调整、批次/SN 追溯、库存占用与四级冻结、线边虚拟库负库存;对外 WmsStockApi 供 ERP 收发。",
docs/project-business/mermaid/05-quality-flow.mmd
@@ -23,4 +23,4 @@
  Q8[支撑数据:质检方案·检验指标·缺陷库·指标结果] --- A1
  Q9[化工报告增强:指标分组树+公式文本·平行测量批次·采样地点/证书编号/执行标准表头·复核人/审核人] --- Q8
  A1 & A2 & A3 & A4 --> P1[检验报告单打印·前端浏览器打印·判定结论带出检验结果+质检专用章留空盖章+签字区·无平行测量时列头称测量值]
  A1 & A2 & A3 & A4 --> P2[产品检验单Word导出·export-product-word·无技术指标列·判定结论带出检验结果/质检章留空盖章·Word打印]
  A1 & A2 & A3 & A4 --> P2[产品检验单Word导出·export-product-word·无技术指标列·判定结论带出检验结果·检验/复核/批准签字带出检测人/复核人/审核人姓名·质检章留空盖章·Word打印]
docs/qc_product_report_word_integration.md
@@ -48,10 +48,10 @@
| 检验日期 | `inspectDate`(新增响应字段) | |
| 检验项目 | 录入项名称 | **不含技术指标列**;分组行与公式文本不展示,分组下录入项平铺 |
| 检验结果 | 测量值 | 平行测量多值顿号合并(如 `10.023、10.115`);无值 `/` |
| 判定结论 | 留空 | 打印后手工填写 |
| 判定结论 | `checkResultText` | 自动带出检验结果文案(按 `qcType` 取字典,OQC 用 `mes_oqc_check_result`),未判定时留空 |
| 质检专用章 | 留空 | 打印后盖章 |
| 备注 | 单据 `remark`,为空时默认「检验结果仅对样品本身或本批次负责。」 | |
| 检验/复核/批准 | 固定留空 | 与判定结论/质检章一致,不输出姓名,打印后手写 |
| 检验 / 复核 / 批准 | `inspectorName` / `auditName` / `approveName` | 分别对应单据的检测人员、复核人、审核人;未指定人员时该段留空、打印后手写 |
## 接口变更点(`/mes/qc/indicator-result/report` 响应)
@@ -68,7 +68,8 @@
|------|------|
| 多样品 | `resultId` 不传取最早样品;与 `/report` 行为一致 |
| 技术指标 | 本版式固定不输出阈值/标准值列;需要技术指标对照时使用既有「打印报告」化验记录版式 |
| 判定结论 | 系统不写入结论(含 checkResult),一律留空手填 |
| 判定结论 | 自动带出检验结果文案;单据未判定(`checkResult` 为空)时留空手填 |
| 检验/复核/批准 | 分别取检测人员、复核人、审核人姓名打印;三项均可为空(打印后手写),不因缺人员而报错 |
| 空值 | 文本空打印 `/`;批量/日期为空打印 `/` |
| 分组指标 | 分组本身不占行,仅平铺其下录入项(含中间参数行,如"试样质量 m/g") |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java
@@ -13,8 +13,6 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -23,8 +21,10 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
@@ -55,6 +55,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@Tag(name = "管理后台 - MES 过程检验单(IPQC)")
@RestController
@RequestMapping("/mes/qc/ipqc")
@@ -64,9 +65,9 @@
    @Resource
    private MesQcIpqcService ipqcService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -145,12 +146,13 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // 批量查询计量单位(
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // 批量查询检测/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcIpqcDO::getInspectorUserId));
@@ -242,8 +244,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIpqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // 批量查询行明细
        List<MesQcIpqcLineDO> lines = ipqcLineMapper.selectListByIpqcIds(convertSet(list, MesQcIpqcDO::getId));
@@ -260,7 +262,7 @@
        for (MesQcIpqcDO ipqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(ipqc.getName());
            MesMdItemDO item = itemMap.get(ipqc.getItemId());
            MdmItemRespDTO item = itemMap.get(ipqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(ipqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(ipqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java
@@ -15,16 +15,16 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.iqc.MesQcIqcLineMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
@@ -73,9 +73,9 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIqcLineMapper iqcLineMapper;
@@ -154,12 +154,13 @@
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesQcIqcDO::getVendorId)).getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // 批量查询计量单位(通过物料的 unitMeasureId)
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // 批量查询检测/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcIqcDO::getInspectorUserId));
@@ -255,8 +256,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // 批量查询行明细
        List<MesQcIqcLineDO> lines = iqcLineMapper.selectListByIqcIds(convertSet(list, MesQcIqcDO::getId));
@@ -273,7 +274,7 @@
        for (MesQcIqcDO iqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(iqc.getName());
            MesMdItemDO item = itemMap.get(iqc.getItemId());
            MdmItemRespDTO item = itemMap.get(iqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(iqc.getVendorBatch());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(iqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -15,8 +15,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -25,9 +27,6 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
@@ -69,9 +68,9 @@
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -153,12 +152,13 @@
        // 批量查询客户
        Map<Long, CrmCustomerRespDTO> clientMap = customerApi.getCustomerMap(
                convertSet(list, MesQcOqcDO::getClientId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // 批量查询检测/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcOqcDO::getInspectorUserId));
@@ -196,8 +196,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcOqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // 批量查询行明细
        List<MesQcOqcLineDO> lines = oqcLineMapper.selectListByOqcIds(convertSet(list, MesQcOqcDO::getId));
@@ -214,7 +214,7 @@
        for (MesQcOqcDO oqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(oqc.getName());
            MesMdItemDO item = itemMap.get(oqc.getItemId());
            MdmItemRespDTO item = itemMap.get(oqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(oqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(oqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/MesQcRqcController.java
@@ -13,8 +13,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.MesQcRqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.MesQcRqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -23,8 +25,6 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.rqc.MesQcRqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
@@ -64,9 +64,9 @@
    @Resource
    private MesQcRqcService rqcService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -145,12 +145,13 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // 批量查询检测/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcRqcDO::getInspectorUserId));
@@ -242,8 +243,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcRqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(MDM 物料)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // 批量查询行明细
        List<MesQcRqcLineDO> lines = rqcLineMapper.selectListByRqcIds(convertSet(list, MesQcRqcDO::getId));
@@ -260,7 +261,7 @@
        for (MesQcRqcDO rqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(rqc.getName());
            MesMdItemDO item = itemMap.get(rqc.getItemId());
            MdmItemRespDTO item = itemMap.get(rqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(rqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(rqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcProductReportWordBuilder.java
@@ -18,7 +18,8 @@
 * 产品检验单(纸质"××产品质量检验单"版式)Word 构建器
 *
 * 版式:标题(公司 + 产品检验单)→ 表头信息 5 行 → 检验项目/检验结果 两列矩阵 →
 * 判定结论(自动带出检验结果)/质检专用章(留空盖章)→ 备注 → 检验/复核/批准签字行。
 * 判定结论(自动带出检验结果)/质检专用章(留空盖章)→ 备注 →
 * 检验/复核/批准签字行(分别带出检测人、复核人、审核人姓名)。
 * 不含技术指标列;分组项不展示,录入项平铺;平行测量多值顿号合并。
 */
public class MesQcProductReportWordBuilder {
@@ -53,7 +54,7 @@
            writeItemTable(doc, report);
            writeConclusionTable(doc, report);
            writeRemarkTable(doc, report);
            writeSignLine(doc);
            writeSignLine(doc, report);
            doc.write(out);
            return out.toByteArray();
        }
@@ -172,12 +173,23 @@
    // ========== 签字行 ==========
    /** 签字行固定留空,与判定结论/质检专用章一致,打印后手写 */
    private static void writeSignLine(XWPFDocument doc) {
    /**
     * 签字行:检验/复核/批准分别带出检测人(inspectorUserId)、复核人(auditUserId)、
     * 审核人(approveUserId)姓名;未指定人员时该段留空,打印后手写
     */
    private static void writeSignLine(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFParagraph paragraph = doc.createParagraph();
        paragraph.setAlignment(ParagraphAlignment.LEFT);
        paragraph.setSpacingBefore(400);
        addRun(paragraph, "检验:         复核:         批准:        ", 12, true);
        String text = "检验:" + signName(report.getInspectorName())
                + "    复核:" + signName(report.getAuditName())
                + "    批准:" + signName(report.getApproveName());
        addRun(paragraph, text, 12, true);
    }
    /** 签名段:有姓名则打印姓名并留签名余量,无姓名则整段留空便于手写 */
    private static String signName(String name) {
        return notBlank(name) ? name + "   " : "     ";
    }
    // ========== 表格基础 ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/pendinginspect/MesQcPendingInspectServiceImpl.java
@@ -1,7 +1,12 @@
package cn.iocoder.yudao.module.mes.service.qc.pendinginspect;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.pendinginspect.MesQcPendingInspectMapper;
@@ -9,6 +14,13 @@
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
/**
 * MES 待检任务 Service 实现类
@@ -19,6 +31,10 @@
    @Resource
    private MesQcPendingInspectMapper pendingInspectMapper;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Override
    public PageResult<MesQcPendingInspectRespVO> getPendingInspectPage(MesQcPendingInspectPageReqVO pageReqVO) {
@@ -26,8 +42,22 @@
        IPage<MesQcPendingInspectRespVO> page = MyBatisUtils.buildPage(pageReqVO);
        pendingInspectMapper.selectQcPendingPage(page,
                pageReqVO.getSourceDocCode(), pageReqVO.getQcType(), pageReqVO.getItemId());
        List<MesQcPendingInspectRespVO> list = page.getRecords();
        // 填充物料信息(来源单据的 item_id 为 MDM 物料 ID)
        if (CollUtil.isNotEmpty(list)) {
            Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                    convertSet(list, MesQcPendingInspectRespVO::getItemId));
            Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                    convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                    .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
            list.forEach(vo -> findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
                findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                        unitMeasure -> vo.setUnitName(unitMeasure.getName()));
            }));
        }
        // 返回结果
        return new PageResult<>(page.getRecords(), page.getTotal());
        return new PageResult<>(list, page.getTotal());
    }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcProductReportWordBuilderTest.java
@@ -62,13 +62,27 @@
            // 备注默认文案
            assertTrue(tables.get(3).getRow(0).getCell(1).getText().contains("检验结果仅对样品本身或本批次负责"));
            // 签字行留空(打印后手写),不得出现姓名
            // 签字行带出姓名:检验=检测人、复核=复核人、批准=审核人
            String signLine = doc.getParagraphs().stream()
                    .filter(p -> p.getText().contains("批准:"))
                    .findFirst().orElseThrow().getText();
            assertTrue(signLine.contains("检验:") && signLine.contains("复核:"));
            assertTrue(!signLine.contains("李博") && !signLine.contains("周亚洁") && !signLine.contains("包翠萍"),
                    "签字行应留空,不输出检验/复核/批准姓名");
            assertTrue(signLine.contains("检验:李博"), "检验应带出检测人姓名:" + signLine);
            assertTrue(signLine.contains("复核:周亚洁"), "复核应带出复核人姓名:" + signLine);
            assertTrue(signLine.contains("批准:包翠萍"), "批准应带出审核人姓名:" + signLine);
        }
    }
    @Test
    public void testBuildWithoutSigners() throws IOException {
        MesQcQcReportRespVO report = randomReport();
        report.setInspectorName(null).setAuditName("").setApproveName(null);
        byte[] docx = MesQcProductReportWordBuilder.build(report);
        try (InputStream in = new ByteArrayInputStream(docx); XWPFDocument doc = new XWPFDocument(in)) {
            String signLine = doc.getParagraphs().stream()
                    .filter(p -> p.getText().contains("批准:"))
                    .findFirst().orElseThrow().getText();
            assertTrue(signLine.contains("检验:") && signLine.contains("复核:") && signLine.contains("批准:"),
                    "未指定人员时仍保留三个栏位供手写:" + signLine);
        }
    }