云
2026-09-22 de0202274ed147e89fdbba0a953936a3b9997ec7
feat(mes): 更新IPQC过程检验功能

- 移除生产工单、生产任务、工位、工序相关字段
- 新增批号字段并设为必填
- 更新数据库表结构包括SQL导出文件和H2测试脚本
- 修改IPQC相关的控制器、服务和数据对象
- 调整AI服务中的查询逻辑适配新的字段结构
- 更新文档说明去除生产相关信息增加批号和MDM物料说明
- 添加批号必填的错误码常量
- 替换MES内部物料服务调用为MDM物料API调用
已修改38个文件
1105 ■■■■■ 文件已修改
docs/project-business/data/objects.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/index.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260918.sql 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/qc/MesQcReportApiImpl.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcLineController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcPageReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcLineController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ncr/MesQcNcrController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcLineController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/MesQcPendingInspectController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/vo/MesQcPendingInspectRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/MesQcRqcController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/MesQcRqcLineController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateIndicatorController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/rqc/MesQcRqcServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/instance/MesQcReportContextMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/objects.json
@@ -36,7 +36,7 @@
    {"name": "库存台账", "module": "仓储", "table": "mes_wm_material_stock", "doc": "物料×仓库×库区×库位×批次×供应商六元组,数量/冻结/占用/在途四量", "upstream": ["全部入库单"], "downstream": ["全部出库单", "事务流水"], "keywords": "库存 台账 stock 可用量"},
    {"name": "批次", "module": "仓储", "table": "mes_wm_batch", "doc": "关联物料/工单/供应商/客户/质量状态;SN 码含批次 uuid;条码支持18种业务对象", "upstream": ["入库单据"], "downstream": ["库存台账"], "keywords": "批次 batch SN 条码 追溯"},
    {"name": "IQC来料检验", "module": "质量", "table": "mes_qc_iqc", "doc": "到货通知/外协入库触发,草稿0→完成4,结论回写来源单据;行操作「出报告」仅在已完成时可见", "upstream": ["到货通知", "外协入库"], "downstream": ["采购入库", "供应商退货", "NCR", "报告实例(出报告)"], "keywords": "IQC 来料检验 出报告"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见", "upstream": ["报工"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;表单只留「来源单据 + 产品物料(前端手选 MDM 物料) + 批号(必填) + 数量 + 检验信息」,已去生产订单/生产任务/工位/工序,批号随报告落款;行操作「出报告」仅在已完成时可见", "upstream": ["报工"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批号 batchCode MDM物料"},
    {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货;行操作「出报告」仅在已完成时可见", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进)", "报告实例(出报告)"], "keywords": "OQC 出货检验 出厂检验 出报告"},
    {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;行操作「出报告」仅在已完成时可见。措辞已统一为「退货检验」(原菜单父项与 mes_qc_type 字典写「退料检验」,与后端枚举/表注释/菜单权限子项不一致;RQC 三者都覆盖,故取「退货检验」)", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "报告实例(出报告)"], "keywords": "RQC 退货检验 退料检验 出报告"},
    {"name": "NCR不合格品", "module": "质量", "table": "mes_qc_ncr", "doc": "待评审0→已评审6→已处置7→已关闭8,处置:退货/报废/返工/让步接收/降级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["退货/报废/返工(台账)"], "keywords": "NCR 不合格 处置 特采 报废"},
docs/project-business/index.html
@@ -479,7 +479,7 @@
    {"name": "库存台账", "module": "仓储", "table": "mes_wm_material_stock", "doc": "物料×仓库×库区×库位×批次×供应商六元组,数量/冻结/占用/在途四量", "upstream": ["全部入库单"], "downstream": ["全部出库单", "事务流水"], "keywords": "库存 台账 stock 可用量"},
    {"name": "批次", "module": "仓储", "table": "mes_wm_batch", "doc": "关联物料/工单/供应商/客户/质量状态;SN 码含批次 uuid;条码支持18种业务对象", "upstream": ["入库单据"], "downstream": ["库存台账"], "keywords": "批次 batch SN 条码 追溯"},
    {"name": "IQC来料检验", "module": "质量", "table": "mes_qc_iqc", "doc": "到货通知/外协入库触发,草稿0→完成4,结论回写来源单据;行操作「出报告」仅在已完成时可见", "upstream": ["到货通知", "外协入库"], "downstream": ["采购入库", "供应商退货", "NCR", "报告实例(出报告)"], "keywords": "IQC 来料检验 出报告"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见", "upstream": ["报工"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;表单只留「来源单据 + 产品物料(前端手选 MDM 物料) + 批号(必填) + 数量 + 检验信息」,已去生产订单/生产任务/工位/工序,批号随报告落款;行操作「出报告」仅在已完成时可见", "upstream": ["报工"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批号 batchCode MDM物料"},
    {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货;行操作「出报告」仅在已完成时可见", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进)", "报告实例(出报告)"], "keywords": "OQC 出货检验 出厂检验 出报告"},
    {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;行操作「出报告」仅在已完成时可见。措辞已统一为「退货检验」(原菜单父项与 mes_qc_type 字典写「退料检验」,与后端枚举/表注释/菜单权限子项不一致;RQC 三者都覆盖,故取「退货检验」)", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "报告实例(出报告)"], "keywords": "RQC 退货检验 退料检验 出报告"},
    {"name": "NCR不合格品", "module": "质量", "table": "mes_qc_ncr", "doc": "待评审0→已评审6→已处置7→已关闭8,处置:退货/报废/返工/让步接收/降级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["退货/报废/返工(台账)"], "keywords": "NCR 不合格 处置 特采 报废"},
docs/sql/config_export_all_20260918.sql
@@ -22,9 +22,73 @@
--      ruoyi-vue-pro.system_menu WHERE deleted=b'0'`,是执行期跨库全量拷贝,
--      只要源库 ruoyi-vue-pro 有这几行就会被带过去 —— 与阶段一的字面量 INSERT 不同。
--      在此登记只为说明「这版文件确实覆盖了该变更」,避免后来者以为漏了。
--   ⑥ 补 116 个按钮权限 + 删 1 个失效菜单(2026-09-22,前后端菜单/权限对齐):
--      116 个按钮权限是「后端 @PreAuthorize 注解里有、system_menu 里没有」的差集
--      (A 类 19 个:前后端都在用而库缺失,属真缺;B 类 97 个:仅后端定义),
--      按「模块:实体」前缀分别挂到对应 type=2 的菜单下(如 erp:supplier:* → 602525 供应商档案)。
--      同时软删 1063009 仓库坐标配置及其 2 个按钮 1063091/1063092 ——
--      该菜单 component 指向 bi/config/warehouse-coordinate/index,前端并无此页面(历史残留),
--      并物理清掉这 3 个菜单的 9 条 system_role_menu 孤儿授权。
--      **本文件同样无需为此改动**:阶段② 跨库全量拷贝 system_menu(deleted=b'0') 与
--      system_role_menu,源库 ruoyi-vue-pro 已写入,执行期自会带过去。理由同 ⑤。
--   ⑦ 薪酬按钮权限码改名对齐 + 删除「ERP 系统」整个菜单(2026-09-22):
--      ① 前端 hrm/salary/calculation/index.vue、payment/index.vue 原先用
--         hrm:salary-calculation:* / hrm:salary-payment:*,而后端
--         HrmSalaryCalculationController / HrmSalaryPaymentController 用的是 hrm:salary:*,
--         两边对不上(按钮要么压根不显示,要么显示了被后端 403)。已改前端对齐后端,
--         并软删库中 5 条旧码按钮(60212/60213/60214/60244/60245)及其 15 条 role_menu。
--         注:旧按钮原本挂在「超级管理员/高层管理/销售人员」3 个角色上(销售人员持有薪资发放权限,
--         疑为角色页全选误授),删除后未迁移授权,新按钮 hrm:salary:* 目前无任何角色授权,
--         需在角色管理页按实际岗位重新勾选。
--      ② 软删「ERP 系统」(id=2563, path=/erp) 整棵子树共 83 个节点 ——
--         产品管理 17 + 库存管理 43 + 财务管理 20 + ERP首页 1 + 目录节点 2,
--         并物理清掉其 83 条 system_role_menu(该子树仅超级管理员一个角色挂有授权)。
--         其中产品管理 / 库存管理在后端 yudao-module-erp 已无任何控制器
--         (erp:product* / erp:warehouse* / erp:stock* 全部不存在),属历史残留;
--         财务管理(结算账户/付款单/收款单)后端控制器仍在,删后这两个页面失去菜单入口
--         (付款单在「采购管理」下另有重复入口 1063116)。「采购管理」(2602)、
--         「营销管理」(2617) 两个根目录未动,其中的 ERP 页面照常保留。
--         ⚠ 这是**已知且已确认接受的代价**:删除后 13 个仍在使用的权限码失去 system_menu 载体 ——
--           erp:account:create/update/delete/export(A 类:前后端都在用)、
--           erp:finance-receipt:create/update/delete/export/query/update-status(A 类)、
--           erp:account:query、erp:statistics:query(B 类:仅后端 @PreAuthorize)。
--           即结算账户、收款单两页在库中已无权限行,所有角色按钮全隐藏且后端返回 403;
--           日后若要恢复这两个页面,需重建菜单并补回上述 13 个权限码。
--      **本文件同样无需为此改动**,理由同 ⑤⑥。
--   ⑧ 过程检验单 mes_qc_ipqc 字段调整(2026-09-22,IPQC 去生产信息 + 批号必填):
--      删 work_order_id / task_id / workstation_id / process_id 四列,
--      在 source_doc_code 之后新增 batch_code varchar(100) COMMENT '批号'。
--      同步改了本地 H2 单测建表脚本 create_tables.sql(否则 MesQcIpqcServiceImplTest 全挂)。
--      **本文件必须改**:阶段一是全库 CREATE TABLE 字面量导出,不重导出就会建出旧结构。
--      源库 ruoyi-vue-pro 已执行同样的 DDL,结构以本文件为准。
--   ⑨ 补 18 个按钮权限(2026-09-22,C2 类:「前端在用、后端 @PreAuthorize 未定义、库中亦无」):
--      1075533~1075550,分别为「模板导出」(system:notify-template:export → 2145)、
--      「离职交接」(hrm:resignation:handover → 6005)、「杂项入库单」6 个动作
--      (mes:wm-misc-receipt:create/update/delete/export/cancel/finish → 5888)、
--      「图纸审核」1 个、「设计任务」5 个、「版本变更」4 个动作 (mes:pd-* → 602572)。
--      补录时 C2 共 93 个(删「ERP 系统」后由 54 涨上来),其中仅这 18 个能定位到 type=2 父菜单;
--      已全部落库并逐个复核「父菜单 type=2、status=0」通过。
--      剩余 75 个**刻意未补**,分三类:
--        ① 67 个 erp:*(product / product-category / product-unit / warehouse / stock /
--           stock-in / stock-out / stock-move / stock-check / stock-record / purchase-in /
--           purchase-return / sale-out / sale-return)—— 对应页面**只有前端、没有后端**:
--           yudao-module-erp 无任何这些控制器,全仓 grep 也无对应 @PreAuthorize。
--           其中 42 个的父菜单(产品管理/库存管理子树)已随 ⑦ 一并删除,另 25 个
--           (采购入库/采购退货/销售出库/销售退货)自建库起就没有菜单。属历史残留,按既定口径跳过。
--        ② 7 个 crm:product:* / crm:product-category:* —— 主工作区 yudao-module-crm 下并无
--           CrmProductController / CrmProductCategoryController,只存在于两个并行 agent 的
--           worktree 副本里(尚未合并);且即便合并也缺「商品」菜单,单补按钮无意义,留给该分支自己补。
--        ③ 1 个 mes:wm-approval:save —— 前端 views/wls/approval/config/index.vue 的**码写错了**:
--           后端 /mes/wms/approval/config/save 实际要求 mes:wm-warehouse:update(库中已有,id=5784),
--           与 ⑦① 的薪酬漂移同类问题。正确修法是改前端这一行,因未获授权改前端故未动。
--           (另:该页所挂的父目录 6002「人事审批」是个没有子菜单的空 type=1,本就没有菜单挂到该页面)
--      **本文件同样无需为此改动**,理由同 ⑤⑥。
-- 验证:本文件已在全新空库(MySQL 8.4.6)整份执行通过 —— 419 张表、6 张 qc_report_*、
--       6 个字典类型 + 21 条字典数据、admin 账号 / 1258 条菜单 / 2349 条 role_menu /
--       默认 OAuth2 客户端均正确落库。
-- 2026-09-22 上述变更全部落地后,源库 ruoyi-vue-pro 现有有效菜单 1308 条(其中按钮 1021 条)、
-- 阶段② 将拷贝的 system_role_menu 1708 条(= admin 名下「超级管理员」852 + 「普通角色」856;
-- 该段子查询不带 deleted 过滤,实测这两角色下无软删行,故 1708 即最终行数),执行时应与此一致。
-- 范围:全库 BASE TABLE 共 419 张,全部输出 CREATE TABLE;
--       仅以下配置类表带数据(INSERT),其余业务/系统/运行历史表只保留空表结构:
--     ① 协同审批流程(引擎元数据+定义,仅激活流程,剔历史/运行/身份):act_ge_property、act_id_property、act_re_procdef、act_re_deployment、act_ge_bytearray、act_re_model、bpm_process_definition_info
@@ -8036,10 +8100,7 @@
  `source_doc_id` bigint DEFAULT NULL COMMENT '来源单据编号',
  `source_line_id` bigint DEFAULT NULL COMMENT '来源单据行编号',
  `source_doc_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源单据编号',
  `work_order_id` bigint DEFAULT NULL COMMENT '生产工单编号',
  `task_id` bigint DEFAULT NULL COMMENT '生产任务编号',
  `workstation_id` bigint DEFAULT NULL COMMENT '工位编号',
  `process_id` bigint DEFAULT NULL COMMENT '工序编号',
  `batch_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '批号',
  `item_id` bigint DEFAULT NULL COMMENT '产品物料编号',
  `check_quantity` decimal(24,6) DEFAULT NULL COMMENT '检测数量',
  `qualified_quantity` decimal(24,6) DEFAULT NULL COMMENT '合格品数量',
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/qc/MesQcReportApiImpl.java
@@ -4,10 +4,12 @@
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
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.api.qc.dto.MesQcReportItemRespDTO;
import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportRespDTO;
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;
@@ -19,15 +21,11 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
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.pro.workorder.MesProWorkOrderService;
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.iqc.MesQcIqcLineService;
@@ -102,11 +100,9 @@
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesProWorkOrderService workOrderService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
@@ -144,7 +140,7 @@
                    order.getSourceDocType(), order.getSourceDocCode(), order.getItemId(), order.getCheckQuantity(),
                    order.getQualifiedQuantity(), order.getUnqualifiedQuantity(), order.getCheckResult(),
                    order.getInspectDate(), order.getStatus(), order.getRemark());
            resp.setWorkOrderCode(workOrderCode(order.getWorkOrderId()));
            resp.setBatchCode(defaultStr(order.getBatchCode()));
            lines = loadIpqcLines(qcId);
            inspectorUserId = order.getInspectorUserId();
        } else if (Objects.equals(qcType, MesQcTypeEnum.OQC.getType())) {
@@ -176,17 +172,17 @@
            throw new IllegalArgumentException("暂不支持 qcType=" + qcType);
        }
        // 物料与单位:单位优先取行上的(检验时实际用的单位),缺失才回退物料的默认单位
        MesMdItemDO item = resp.getItemId() == null ? null
                : itemService.getItemMap(Collections.singletonList(resp.getItemId())).get(resp.getItemId());
        // 物料与单位:物料 ID 来自 MDM;单位优先取行上的(检验时实际用的单位),缺失才回退物料的默认单位
        MdmItemRespDTO item = resp.getItemId() == null ? null
                : mdmItemApi.getItemMap(Collections.singletonList(resp.getItemId())).get(resp.getItemId());
        if (item != null) {
            resp.setItemCode(defaultStr(item.getCode()));
            resp.setItemName(defaultStr(item.getName()));
            resp.setItemSpecification(defaultStr(item.getSpecification()));
        }
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMapOf(lines, item);
        Map<Long, MdmUnitMeasureRespDTO> unitMap = unitMapOf(lines, item);
        if (item != null) {
            MesMdUnitMeasureDO itemUnit = unitMap.get(item.getUnitMeasureId());
            MdmUnitMeasureRespDTO itemUnit = unitMap.get(item.getUnitMeasureId());
            resp.setUnitName(itemUnit == null ? "" : defaultStr(itemUnit.getName()));
        }
        resp.setInspectorName(userName(inspectorUserId));
@@ -235,7 +231,7 @@
     * (组名与公式是分组检验项最不该丢的两样东西)。
     */
    private BuiltItems buildItems(Long qcId, Integer qcType, List<LineRow> lines,
                                  Map<Long, MesMdUnitMeasureDO> unitMap, MesMdItemDO item) {
                                  Map<Long, MdmUnitMeasureRespDTO> unitMap, MdmItemRespDTO item) {
        if (CollUtil.isEmpty(lines)) {
            return new BuiltItems(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), 0);
        }
@@ -408,7 +404,7 @@
    private List<MesQcReportItemRespDTO> expandRows(MesQcIndicatorDO indicator, LineRow line,
                                                    List<MesQcIndicatorResultDO> samples,
                                                    Map<Long, List<MesQcIndicatorResultDetailDO>> detailMap,
                                                    Map<Long, MesMdUnitMeasureDO> unitMap, MesMdItemDO item) {
                                                    Map<Long, MdmUnitMeasureRespDTO> unitMap, MdmItemRespDTO item) {
        List<MesQcReportItemRespDTO> rows = new ArrayList<>();
        for (MesQcIndicatorResultDO sample : samples) {
            for (MesQcIndicatorResultDetailDO detail : detailMap.getOrDefault(sample.getId(),
@@ -441,9 +437,9 @@
    }
    private MesQcReportItemRespDTO toItem(MesQcIndicatorDO indicator, LineRow line, String sampleCode, String value,
                                          String detailRemark, Map<Long, MesMdUnitMeasureDO> unitMap,
                                          MesMdItemDO item) {
        MesMdUnitMeasureDO unit = unitMap.get(line.unitMeasureId());
                                          String detailRemark, Map<Long, MdmUnitMeasureRespDTO> unitMap,
                                          MdmItemRespDTO item) {
        MdmUnitMeasureRespDTO unit = unitMap.get(line.unitMeasureId());
        if (unit == null && item != null) {
            unit = unitMap.get(item.getUnitMeasureId());
        }
@@ -524,7 +520,7 @@
    // ==================== 关联名称 ====================
    private Map<Long, MesMdUnitMeasureDO> unitMapOf(List<LineRow> lines, MesMdItemDO item) {
    private Map<Long, MdmUnitMeasureRespDTO> unitMapOf(List<LineRow> lines, MdmItemRespDTO item) {
        List<Long> unitIds = new ArrayList<>();
        for (LineRow line : lines) {
            if (line.unitMeasureId() != null) {
@@ -534,7 +530,7 @@
        if (item != null && item.getUnitMeasureId() != null) {
            unitIds.add(item.getUnitMeasureId());
        }
        return unitIds.isEmpty() ? Collections.emptyMap() : unitMeasureService.getUnitMeasureMap(unitIds);
        return unitIds.isEmpty() ? Collections.emptyMap() : mdmUnitMeasureApi.getUnitMeasureMap(unitIds);
    }
    private String supplierName(Long vendorId) {
@@ -552,14 +548,6 @@
        }
        CrmCustomerRespDTO client = customerApi.getCustomerMap(Collections.singleton(clientId)).get(clientId);
        return client == null ? "" : defaultStr(client.getName());
    }
    private String workOrderCode(Long workOrderId) {
        if (workOrderId == null) {
            return "";
        }
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        return workOrder == null ? "" : defaultStr(workOrder.getCode());
    }
    private String userName(Long userId) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java
@@ -9,15 +9,14 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
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.ipqc.vo.MesQcIpqcPageReqVO;
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.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
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;
@@ -26,11 +25,6 @@
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.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
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;
@@ -70,15 +64,9 @@
    @Resource
    private MesQcIpqcService ipqcService;
    @Resource
    private MesProWorkOrderService workOrderService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdWorkstationService workstationService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -157,21 +145,12 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 批量查询工单
        Map<Long, MesProWorkOrderDO> workOrderMap = workOrderService.getWorkOrderMap(
                convertSet(list, MesQcIpqcDO::getWorkOrderId));
        // 批量查询工位
        Map<Long, MesMdWorkstationDO> workstationMap = workstationService.getWorkstationMap(
                convertSet(list, MesQcIpqcDO::getWorkstationId));
        // 批量查询工序
        Map<Long, MesProProcessDO> processMap = processService.getProcessMap(
                convertSet(list, MesQcIpqcDO::getProcessId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // 批量查询计量单位(
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIpqcDO::getInspectorUserId));
@@ -179,12 +158,6 @@
        Map<Long, String> indicatorSummaryMap = buildIndicatorSummaryMap(list);
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcIpqcRespVO.class, vo -> {
            findAndThen(workOrderMap, vo.getWorkOrderId(), workOrder -> vo
                    .setWorkOrderCode(workOrder.getCode()).setWorkOrderName(workOrder.getName()));
            findAndThen(workstationMap, vo.getWorkstationId(),
                    ws -> vo.setWorkstationName(ws.getName()));
            findAndThen(processMap, vo.getProcessId(),
                    process -> vo.setProcessName(process.getName()));
            findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification());
                findAndThen(unitMeasureMap, item.getUnitMeasureId(),
@@ -262,8 +235,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIpqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // 批量查询行明细
        List<MesQcIpqcLineDO> lines = ipqcLineMapper.selectListByIpqcIds(convertSet(list, MesQcIpqcDO::getId));
@@ -280,10 +253,9 @@
        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() : "");
            // IPQC 无批次号字段,暂用来源单据编号(一般为生产工单号)作为批号
            vo.setBatchNo(ipqc.getSourceDocCode());
            vo.setBatchNo(ipqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(ipqc.getInspectDate()));
            List<String> indicatorNames = new ArrayList<>();
            List<String> judgementSegments = new ArrayList<>();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcLineController.java
@@ -6,10 +6,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.line.MesQcIpqcLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.line.MesQcIpqcLineRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
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.ipqc.MesQcIpqcLineDO;
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.ipqc.MesQcIpqcLineService;
import io.swagger.v3.oas.annotations.Operation;
@@ -43,7 +43,7 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @GetMapping("/get")
    @Operation(summary = "获得过程检验单行")
@@ -71,8 +71,8 @@
        // 批量查询检测指标
        Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap(
                convertSet(list, MesQcIpqcLineDO::getIndicatorId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(list, MesQcIpqcLineDO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcIpqcLineRespVO.class, vo -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcPageReqVO.java
@@ -23,8 +23,8 @@
    @Schema(description = "IPQC 检验类型", example = "1")
    private Integer type;
    @Schema(description = "生产工单 ID", example = "10")
    private Long workOrderId;
    @Schema(description = "批号(模糊匹配)", example = "BC2025")
    private String batchCode;
    @Schema(description = "产品物料 ID", example = "50")
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java
@@ -50,37 +50,12 @@
    @Schema(description = "来源单据编号", example = "FB20250101001")
    private String sourceDocCode;
    // ========== 生产关联 ==========
    @Schema(description = "生产工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long workOrderId;
    @Schema(description = "工单编号", example = "WO20250101001")
    @ExcelProperty("工单编号")
    private String workOrderCode;
    @Schema(description = "工单名称", example = "生产工单A")
    private String workOrderName;
    @Schema(description = "生产任务 ID", example = "20")
    private Long taskId;
    @Schema(description = "工位 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "30")
    private Long workstationId;
    @Schema(description = "工位名称", example = "工位A")
    @ExcelProperty("工位名称")
    private String workstationName;
    @Schema(description = "工序 ID", example = "40")
    private Long processId;
    @Schema(description = "工序名称", example = "组装")
    @ExcelProperty("工序名称")
    private String processName;
    // ========== 物料 ==========
    @Schema(description = "批号", requiredMode = Schema.RequiredMode.REQUIRED, example = "BC20250101")
    @ExcelProperty("批号")
    private String batchCode;
    @Schema(description = "产品物料 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java
@@ -7,6 +7,7 @@
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
@@ -42,25 +43,15 @@
    @Schema(description = "来源单据行 ID", example = "300")
    private Long sourceLineId;
    // ========== 生产关联 ==========
    @Schema(description = "生产工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "生产工单不能为空")
    private Long workOrderId;
    @Schema(description = "生产任务 ID", example = "20")
    private Long taskId;
    @Schema(description = "工位 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "30")
    @NotNull(message = "工位不能为空")
    private Long workstationId;
    @Schema(description = "工序 ID", example = "40")
    private Long processId;
    // ========== 物料 ==========
    @Schema(description = "产品物料 ID", example = "50")
    @Schema(description = "批号", requiredMode = Schema.RequiredMode.REQUIRED, example = "BC20250101")
    @NotEmpty(message = "批号不能为空")
    @Size(max = 100, message = "批号长度不能超过 100 个字符")
    private String batchCode;
    @Schema(description = "产品物料 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
    @NotNull(message = "产品物料不能为空")
    private Long itemId;
    @Schema(description = "质检方案 ID", example = "5")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java
@@ -13,18 +13,18 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
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.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.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,12 @@
        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(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // 批量查询计量单位(通过物料的 unitMeasureId)
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位(来自 MDM,通过物料的 unitMeasureId)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIqcDO::getInspectorUserId));
@@ -248,8 +248,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // 批量查询行明细
        List<MesQcIqcLineDO> lines = iqcLineMapper.selectListByIqcIds(convertSet(list, MesQcIqcDO::getId));
@@ -266,7 +266,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/iqc/MesQcIqcLineController.java
@@ -6,10 +6,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.line.MesQcIqcLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.line.MesQcIqcLineRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
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.iqc.MesQcIqcLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcLineService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import io.swagger.v3.oas.annotations.Operation;
@@ -43,7 +43,7 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @GetMapping("/get")
    @Operation(summary = "获得来料检验单行")
@@ -71,8 +71,8 @@
        // 批量查询检测指标
        Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap(
                convertSet(list, MesQcIqcLineDO::getIndicatorId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(list, MesQcIqcLineDO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcIqcLineRespVO.class, vo -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ncr/MesQcNcrController.java
@@ -7,10 +7,10 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -44,7 +44,7 @@
    private MesQcNcrService ncrService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
@@ -138,8 +138,8 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcNcrDO::getItemId));
        // 批量查询评审人
        Set<Long> reviewerUserIds = convertSet(list, MesQcNcrDO::getReviewerUserId);
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,12 @@
        // 批量查询客户
        Map<Long, CrmCustomerRespDTO> clientMap = customerApi.getCustomerMap(
                convertSet(list, MesQcOqcDO::getClientId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcOqcDO::getInspectorUserId));
@@ -189,8 +188,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcOqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // 批量查询行明细
        List<MesQcOqcLineDO> lines = oqcLineMapper.selectListByOqcIds(convertSet(list, MesQcOqcDO::getId));
@@ -207,7 +206,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/oqc/MesQcOqcLineController.java
@@ -6,10 +6,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.line.MesQcOqcLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.line.MesQcOqcLineRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
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.oqc.MesQcOqcLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcLineService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import io.swagger.v3.oas.annotations.Operation;
@@ -43,7 +43,7 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @GetMapping("/get")
    @Operation(summary = "获得出货检验单行")
@@ -71,8 +71,8 @@
        // 批量查询检测指标
        Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap(
                convertSet(list, MesQcOqcLineDO::getIndicatorId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(list, MesQcOqcLineDO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcOqcLineRespVO.class, vo -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -47,7 +47,7 @@
    // ========== 客户 ==========
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @Schema(description = "客户 ID", example = "10")
    private Long clientId;
    @Schema(description = "客户简称", example = "客户A")
@@ -87,7 +87,7 @@
    @Schema(description = "最大不合格数", example = "0")
    private Integer maxUnqualifiedQuantity;
    @Schema(description = "本次出货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @Schema(description = "本次出货数量", example = "100")
    @ExcelProperty("出货数量")
    private BigDecimal outQuantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java
@@ -37,8 +37,7 @@
    // ========== 客户 ==========
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "客户不能为空")
    @Schema(description = "客户 ID", example = "10")
    private Long clientId;
    // ========== 物料 ==========
@@ -55,13 +54,11 @@
    // ========== 数量 ==========
    @Schema(description = "本次出货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "本次出货数量不能为空")
    @Schema(description = "本次出货数量", example = "100")
    @DecimalMin(value = "0", message = "本次出货数量不能小于 0")
    private BigDecimal outQuantity;
    @Schema(description = "本次检测数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "本次检测数量不能为空")
    @Schema(description = "本次检测数量", example = "10")
    @DecimalMin(value = "0", message = "本次检测数量不能小于 0")
    private BigDecimal checkQuantity;
@@ -80,8 +77,7 @@
    @Schema(description = "检测结果", example = "1")
    private Integer checkResult;
    @Schema(description = "出货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "出货日期不能为空")
    @Schema(description = "出货日期")
    private LocalDateTime outDate;
    @Schema(description = "检测日期", requiredMode = Schema.RequiredMode.REQUIRED)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/MesQcPendingInspectController.java
@@ -9,12 +9,12 @@
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
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.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
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.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.qc.pendinginspect.MesQcPendingInspectService;
@@ -44,9 +44,9 @@
    @Resource
    private MesQcPendingInspectService pendingInspectService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
@@ -73,12 +73,12 @@
        if (CollUtil.isEmpty(list)) {
            return;
        }
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcPendingInspectRespVO::getItemId));
        // 批量查询计量单位(通过物料的 unitMeasureId)
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位(来自 MDM,通过物料的 unitMeasureId)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 批量查询供应商(IQC 场景)
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesQcPendingInspectRespVO::getVendorId)).getCheckedData().stream()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/vo/MesQcPendingInspectRespVO.java
@@ -21,6 +21,9 @@
    @Schema(description = "来源单据编号", example = "AN2025001")
    private String sourceDocCode;
    @Schema(description = "批号(仅 IPQC/OQC/RQC 来源单据有值)", example = "BC20250101")
    private String batchCode;
    @Schema(description = "检验类型(MesQcTypeEnum)", example = "1")
    private Integer qcType;
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,12 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcRqcDO::getInspectorUserId));
@@ -235,8 +235,8 @@
        // 批量查询检测人员
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcRqcDO::getInspectorUserId));
        // 批量查询物料
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // 批量查询物料(物料 ID 来自 MDM)
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // 批量查询行明细
        List<MesQcRqcLineDO> lines = rqcLineMapper.selectListByRqcIds(convertSet(list, MesQcRqcDO::getId));
@@ -253,7 +253,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/controller/admin/qc/rqc/MesQcRqcLineController.java
@@ -6,10 +6,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.line.MesQcRqcLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.line.MesQcRqcLineRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
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.rqc.MesQcRqcLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcLineService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import io.swagger.v3.oas.annotations.Operation;
@@ -43,7 +43,7 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @GetMapping("/get")
    @Operation(summary = "获得退货检验单行")
@@ -71,8 +71,8 @@
        // 批量查询检测指标
        Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap(
                convertSet(list, MesQcRqcLineDO::getIndicatorId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
        // 批量查询计量单位(来自 MDM)
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(list, MesQcRqcLineDO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcRqcLineRespVO.class, vo -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateIndicatorController.java
@@ -7,12 +7,12 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.indicator.MesQcTemplateIndicatorPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.indicator.MesQcTemplateIndicatorRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.indicator.MesQcTemplateIndicatorSaveReqVO;
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.template.MesQcTemplateIndicatorDO;
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.template.MesQcTemplateIndicatorService;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -41,7 +41,7 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @PostMapping("/create")
    @Operation(summary = "创建质检方案-检测指标项")
@@ -95,7 +95,7 @@
        Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap(
                convertSet(list, MesQcTemplateIndicatorDO::getIndicatorId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(list, MesQcTemplateIndicatorDO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcTemplateIndicatorRespVO.class, vo -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java
@@ -8,13 +8,13 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
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.template.MesQcTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -44,9 +44,9 @@
    @Resource
    private MesQcTemplateService templateService;
    @Resource
    private MesMdmItemService mdmItemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @PostMapping("/create")
    @Operation(summary = "创建质检方案-产品关联")
@@ -91,15 +91,12 @@
    }
    @GetMapping("/list-by-item-and-type")
    @Operation(summary = "根据物料或工单/任务和检测种类获取可用质检方案")
    @Operation(summary = "根据物料和检测种类获取可用质检方案")
    @PreAuthorize("@ss.hasPermission('mes:qc-template:query')")
    public CommonResult<List<MesQcTemplateItemOptionRespVO>> getTemplateItemOptionList(
            @RequestParam(value = "qcType", required = false) Integer qcType,
            @RequestParam(value = "itemId", required = false) Long itemId,
            @RequestParam(value = "workOrderId", required = false) Long workOrderId,
            @RequestParam(value = "taskId", required = false) Long taskId) {
        List<MesQcTemplateItemDO> list = templateItemService.getTemplateItemList(
                itemId, qcType, workOrderId, taskId);
            @RequestParam(value = "itemId", required = false) Long itemId) {
        List<MesQcTemplateItemDO> list = templateItemService.getTemplateItemList(itemId, qcType);
        if (CollUtil.isEmpty(list)) {
            return success(Collections.emptyList());
        }
@@ -122,11 +119,11 @@
            return Collections.emptyList();
        }
        // 批量查询物料(itemId 存的是 mdm_item_id,关联 mdm_item 表)
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcTemplateItemDO::getItemId));
        // 批量查询计量单位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdmItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId));
        // 拼装 VO
        return BeanUtils.toBean(list, MesQcTemplateItemRespVO.class, vo -> {
            findAndThen(itemMap, vo.getItemId(), item -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java
@@ -1,10 +1,7 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcSourceDocTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
@@ -85,40 +82,17 @@
     * 来源单据编号(冗余)
     */
    private String sourceDocCode;
    // ========== 生产关联 ==========
    /**
     * 生产工单 ID
     *
     * 关联 {@link MesProWorkOrderDO#getId()}
     * 批号
     */
    private Long workOrderId;
    /**
     * 生产任务 ID
     *
     * 关联 {@link cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO#getId()}
     */
    private Long taskId;
    /**
     * 工位 ID
     *
     * 关联 {@link MesMdWorkstationDO#getId()}
     */
    private Long workstationId;
    /**
     * 工序 ID
     *
     * 关联 {@link cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO#getId()}
     */
    private Long processId;
    private String batchCode;
    // ========== 物料 ==========
    /**
     * 产品物料 ID
     *
     * 关联 {@link MesMdItemDO#getId()}
     * 关联 MDM 物料 {@link cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO#getId()}
     */
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java
@@ -21,7 +21,7 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesQcIpqcDO>()
                .likeIfPresent(MesQcIpqcDO::getCode, reqVO.getCode())
                .eqIfPresent(MesQcIpqcDO::getType, reqVO.getType())
                .eqIfPresent(MesQcIpqcDO::getWorkOrderId, reqVO.getWorkOrderId())
                .likeIfPresent(MesQcIpqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcIpqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcIpqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcIpqcDO::getStatus, reqVO.getStatus())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -42,10 +42,13 @@
                .betweenIfPresent(MesQcOqcDO::getInspectDate, reqVO.getInspectDate())
                .orderByDesc(MesQcOqcDO::getId);
        if (permittedClientIds != null) {
            // 未填客户的单据不属于任何客户,不受客户权限遮挡
            if (CollUtil.isEmpty(permittedClientIds)) {
                return PageResult.empty();
                wrapper.isNull(MesQcOqcDO::getClientId);
            } else {
                wrapper.and(w -> w.isNull(MesQcOqcDO::getClientId)
                        .or().in(MesQcOqcDO::getClientId, permittedClientIds));
            }
            wrapper.in(MesQcOqcDO::getClientId, permittedClientIds);
        }
        return selectPage(reqVO, wrapper);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -408,6 +408,7 @@
    ErrorCode QC_IPQC_SOURCE_LINE_NOT_BELONG = new ErrorCode(1_040_604_009, "来源单据行不属于该报工单");
    ErrorCode QC_IPQC_SOURCE_LINE_REQUIRED = new ErrorCode(1_040_604_010, "来源单据类型为报工时,来源产出行 ID 不能为空");
    ErrorCode QC_IPQC_SOURCE_LINE_NOT_PENDING = new ErrorCode(1_040_604_011, "来源产出行不是待检验状态");
    ErrorCode QC_IPQC_BATCH_CODE_REQUIRED = new ErrorCode(1_040_604_012, "批号不能为空");
    // ========== MES 质量管理-过程检验行(1-040-604-100) ==========
    ErrorCode QC_IPQC_LINE_NOT_EXISTS = new ErrorCode(1_040_604_100, "过程检验行不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
@@ -524,10 +524,14 @@
        data.put("已生产数量", workOrder.getQuantityProduced());
        fillItemInfo(data, workOrder.getProductId());
        // 质量风险 - IPQC
        List<MesQcIpqcDO> ipqcList = ipqcMapper.selectList(
                new LambdaQueryWrapper<MesQcIpqcDO>()
                        .eq(MesQcIpqcDO::getWorkOrderId, workOrderId));
        // 质量风险 - IPQC(过程检验单已无工单字段,经来源报工单反查本工单下的检验记录)
        List<Long> feedbackIds = feedbackMapper.selectList(
                new LambdaQueryWrapper<MesProFeedbackDO>()
                        .eq(MesProFeedbackDO::getWorkOrderId, workOrderId))
                .stream().map(MesProFeedbackDO::getId).collect(Collectors.toList());
        List<MesQcIpqcDO> ipqcList = CollUtil.isEmpty(feedbackIds) ? Collections.emptyList()
                : ipqcMapper.selectList(new LambdaQueryWrapper<MesQcIpqcDO>()
                        .in(MesQcIpqcDO::getSourceDocId, feedbackIds));
        if (CollUtil.isNotEmpty(ipqcList)) {
            List<Map<String, Object>> qcData = ipqcList.stream().map(ipqc -> {
                Map<String, Object> item = new LinkedHashMap<>();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiServiceImpl.java
@@ -141,8 +141,7 @@
            data.put("检测数量", ipqc.getCheckQuantity());
            data.put("合格数量", ipqc.getQualifiedQuantity());
            data.put("不合格数量", ipqc.getUnqualifiedQuantity());
            data.put("工单ID", ipqc.getWorkOrderId());
            data.put("工序ID", ipqc.getProcessId());
            data.put("批号", ipqc.getBatchCode());
            fillItemInfo(data, ipqc.getItemId());
        } else if (MesBizTypeConstants.QC_OQC == qcType) {
            data.put("质检类型", "出货检验");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -3,16 +3,14 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
@@ -21,15 +19,8 @@
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
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.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
@@ -62,14 +53,6 @@
@Slf4j
public class MesQcIpqcServiceImpl implements MesQcIpqcService {
    /**
     * IPQC 保存数据校验上下文
     *
     * @param workstation 工位
     * @param task        生产任务(可选,指定 taskId 时非空)
     */
    private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task) {}
    @Resource
    private MesQcIpqcMapper ipqcMapper;
@@ -79,25 +62,7 @@
    private MesQcIpqcLineService ipqcLineService;
    @Resource
    @Lazy
    private MesProWorkOrderService workOrderService;
    @Resource
    @Lazy
    private MesMdWorkstationService workstationService;
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MesQcDefectRecordService defectRecordService;
    @Resource
    @Lazy
    private MesProRouteProductService routeProductService;
    @Resource
    @Lazy
    private MesProRouteProcessService routeProcessService;
    @Resource
    @Lazy
    private MesProTaskService taskService;
    @Resource
    @Lazy
    private MesProFeedbackService feedbackService;
@@ -113,20 +78,21 @@
    private MesQcNcrService ncrService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIpqc(MesQcIpqcSaveReqVO createReqVO) {
        // 1.1 校验数据
        SaveDataContext ctx = validateIpqcSaveData(createReqVO);
        // 1.2 校验工单存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId());
        // 1.3 确定检验物料:有任务时取任务的物料,无任务时取工单的产品
        Long itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId();
        // 1.4 根据物料 + IPQC 类型查找可用质检方案(支持前端显式指定)
        validateIpqcSaveData(createReqVO);
        // 1.2 产品物料由前端指定(MDM 物料),校验其存在
        Long itemId = createReqVO.getItemId();
        // 1.3 根据物料 + IPQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                itemId, MesQcTypeEnum.IPQC.getType(), null, null);
                itemId, MesQcTypeEnum.IPQC.getType());
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -147,7 +113,6 @@
        // 2. 插入主表
        MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class)
                .setItemId(itemId)
                .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId()))
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
@@ -163,60 +128,27 @@
        // 1.1 校验存在 + 草稿状态
        validateIpqcStatusPrepare(updateReqVO.getId());
        // 1.2 校验数据
        SaveDataContext ctx = validateIpqcSaveData(updateReqVO);
        validateIpqcSaveData(updateReqVO);
        // 2. 更新主表
        MesQcIpqcDO existIpqc = validateIpqcExists(updateReqVO.getId());
        MesQcIpqcDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIpqcDO.class);
        updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // 不允许修改来源单据
        updateObj.setTemplateId(null); // 不允许修改模板
        updateObj.setItemId(null); // 不允许修改物料
        updateObj.setWorkOrderId(null); // 不允许修改工单
        updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId()));
        ipqcMapper.updateById(updateObj);
    }
    private SaveDataContext validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) {
    private void validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验工位、检测人员存在
        MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId());
        // 校验批号非空(VO 层已校验,此处兜底内部/RPC 调用绕过校验的情况)
        if (StrUtil.isBlank(reqVO.getBatchCode())) {
            throw exception(QC_IPQC_BATCH_CODE_REQUIRED);
        }
        // 校验产品物料存在(物料 ID 来自 MDM)
        mdmItemApi.validateItemExists(reqVO.getItemId());
        // 校验检测人员存在
        adminUserApi.validateUser(reqVO.getInspectorUserId());
        // 校验工单存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_WORKSTATION_PROCESS_MISMATCH);
        }
        // 校验任务关系(如果指定了任务)
        MesProTaskDO task = null;
        if (reqVO.getTaskId() != null) {
            task = taskService.validateTaskNotFinished(reqVO.getTaskId());
            validateTaskRelation(task, workstation, workOrder, reqVO);
        } else {
            // 无任务时,itemId 仍需与工单产品一致
            if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) {
                throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH);
            }
        }
        return new SaveDataContext(workstation, task);
    }
    private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation,
                                      MesProWorkOrderDO workOrder, MesQcIpqcSaveReqVO reqVO) {
        if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) {
            throw exception(PRO_TASK_WORK_ORDER_MISMATCH);
        }
        if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) {
            throw exception(PRO_TASK_WORKSTATION_MISMATCH);
        }
        Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() : workstation.getProcessId();
        if (expectedProcessId != null && ObjUtil.notEqual(task.getProcessId(), expectedProcessId)) {
            throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH);
        }
        // 校验检验物料与任务物料一致(有任务时,IPQC 检验的物料应与任务加工的物料一致)
        if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), task.getItemId())) {
            throw exception(PRO_TASK_ITEM_MISMATCH);
        }
    }
    @Override
@@ -277,18 +209,6 @@
        // 1. 确定 sourceDocType/sourceDocId
        Long sourceDocId = ipqc.getSourceDocId();
        Long sourceLineId = ipqc.getSourceLineId();
        // 如果 IPQC 单没有设置来源,尝试通过 taskId 查找关联的待检验报工
        if (sourceDocId == null && ipqc.getTaskId() != null) {
            MesProFeedbackDO feedback = feedbackService.getFeedbackListByTaskId(ipqc.getTaskId()).stream()
                    .filter(f -> MesProFeedbackStatusEnum.UNCHECK.getStatus().equals(f.getStatus()))
                    .findFirst().orElse(null);
            if (feedback != null) {
                sourceDocId = feedback.getId();
                log.info("[writeBackSourceDoc] IPQC({}) 未设置来源单据,通过 taskId({}) 找到待检验报工({})",
                        ipqc.getId(), ipqc.getTaskId(), feedback.getId());
            }
        }
        if (sourceDocId == null) {
            log.warn("[writeBackSourceDoc] IPQC({}) 无法确定来源报工,跳过回写", ipqc.getId());
@@ -375,33 +295,6 @@
        }
        // 未知来源类型应报错,而不是静默忽略
        throw exception(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN);
    }
    /**
     * 推导工序 ID
     *
     * <p>通过产品 → 工艺路线产品 → 工艺路线 → 校验工位工序是否在该路线中
     * <p>如果工位的工序在路线中,则返回该 processId;否则返回 null
     *
     * @param productId 产品 ID(工单产品)
     * @param workstationProcessId 工位关联的工序 ID
     * @return 推导出的工序 ID,无法推导时返回 null
     */
    private Long getProcessId(Long productId, Long workstationProcessId) {
        if (productId == null || workstationProcessId == null) {
            return null;
        }
        // 1. 产品 → 工艺路线
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        if (routeProduct == null) {
            return null;
        }
        // 2. 校验工位的工序是否在该路线中
        if (routeProcessService.getRouteProcessByRouteIdAndProcessId(
                routeProduct.getRouteId(), workstationProcessId) == null) {
            return null;
        }
        return workstationProcessId;
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java
@@ -16,7 +16,6 @@
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
@@ -68,8 +67,6 @@
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MesQcIndicatorResultService indicatorResultService;
@@ -93,7 +90,7 @@
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
        // 1.3 根据产品 + IQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.IQC.getType(), null, null);
                createReqVO.getItemId(), MesQcTypeEnum.IQC.getType());
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -91,7 +91,7 @@
                createReqVO.getSourceDocType(), createReqVO.getSourceLineId());
        // 1.3 根据产品 + OQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.OQC.getType(), null, null);
                createReqVO.getItemId(), MesQcTypeEnum.OQC.getType());
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/rqc/MesQcRqcServiceImpl.java
@@ -14,7 +14,7 @@
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.wm.returnissue.MesWmReturnIssueService;
@@ -58,7 +58,7 @@
    private MesQcRqcLineService rqcLineService;
    @Resource
    @Lazy
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    @Lazy
    private MesQcDefectRecordService defectRecordService;
@@ -92,7 +92,7 @@
        validateRqcSaveData(createReqVO);
        // 1.2 根据物料 + RQC 检测种类查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.RQC.getType(), null, null);
                createReqVO.getItemId(), MesQcTypeEnum.RQC.getType());
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -140,8 +140,8 @@
    private void validateRqcSaveData(MesQcRqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateRqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验物料、检测人员存在
        itemService.validateItemExists(reqVO.getItemId());
        // 校验物料(物料 ID 来自 MDM)、检测人员存在
        mdmItemApi.validateItemExists(reqVO.getItemId());
        adminUserApi.validateUser(reqVO.getInspectorUserId());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemService.java
@@ -71,14 +71,12 @@
    MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long itemId, Integer qcType);
    /**
     * 根据产品物料(或工单/任务)和 QC 类型,获取所有可用的质检方案产品关联列表
     * 根据产品物料和 QC 类型,获取所有可用的质检方案产品关联列表
     *
     * @param itemId      产品物料 ID(mes_md_item.id);与 workOrderId/taskId 至少其一
     * @param qcType      检测种类(枚举 {@link MesQcTypeEnum})
     * @param workOrderId 生产工单 ID(可选,IPQC 场景用于解析产品物料)
     * @param taskId      生产任务 ID(可选,优先用于解析产品物料)
     * @param itemId 产品物料 ID(MDM 物料 ID,即 mdm_item.id)
     * @param qcType 检测种类(枚举 {@link MesQcTypeEnum})
     * @return 匹配的产品关联列表(保持原顺序),无匹配返回空列表
     */
    List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType, Long workOrderId, Long taskId);
    List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java
@@ -7,13 +7,9 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.template.MesQcTemplateItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -45,14 +41,6 @@
    @Resource
    @Lazy
    private MesQcTemplateService templateService;
    @Resource
    private MesMdItemService mesMdItemService;
    @Resource
    @Lazy
    private MesProTaskService proTaskService;
    @Resource
    @Lazy
    private MesProWorkOrderService proWorkOrderService;
    @Override
    public Long createTemplateItem(MesQcTemplateItemSaveReqVO createReqVO) {
@@ -127,7 +115,7 @@
    @Override
    public MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long itemId, Integer qcType) {
        List<MesQcTemplateItemDO> templateItems = getTemplateItemList(itemId, qcType, null, null);
        List<MesQcTemplateItemDO> templateItems = getTemplateItemList(itemId, qcType);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -135,28 +123,16 @@
    }
    @Override
    public List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType, Long workOrderId, Long taskId) {
        // 1. 解析真实产品物料 ID:任务优先,其次工单,最后直接用入参
        Long resolvedItemId = itemId;
        if (taskId != null) {
            resolvedItemId = proTaskService.validateTaskExists(taskId).getItemId();
        } else if (workOrderId != null) {
            resolvedItemId = proWorkOrderService.validateWorkOrderExists(workOrderId).getProductId();
        }
        if (resolvedItemId == null) {
    public List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType) {
        // 1. 产品关联表 item_id 存的是 MDM 物料 ID(mdm_item.id),质检侧统一按 MDM 物料查询
        if (itemId == null) {
            return Collections.emptyList();
        }
        // 2. mes_md_item.id 转 mdm_item.id(产品关联表存的是 mdm 物料)
        MesMdItemDO item = mesMdItemService.getItem(resolvedItemId);
        if (item == null || item.getMdmItemId() == null) {
            return Collections.emptyList();
        }
        // 3. 查出该物料关联的所有产品关联
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(item.getMdmItemId());
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(itemId);
        if (CollUtil.isEmpty(templateItems)) {
            return Collections.emptyList();
        }
        // 4. 过滤:方案启用且检测种类包含 qcType,返回全部匹配项
        // 2. 过滤:方案启用且检测种类包含 qcType,返回全部匹配项
        List<MesQcTemplateDO> templates = templateService.getTemplateList(
                convertSet(templateItems, MesQcTemplateItemDO::getTemplateId));
        Set<Long> matchedTemplateIds = templates.stream()
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
@@ -18,6 +18,7 @@
                n.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@WM_ARRIVAL_NOTICE} AS sourceDocType,
                n.code AS sourceDocCode,
                NULL AS batchCode,
                l.id AS sourceLineId,
                n.arrival_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IQC.type} AS qcType,
@@ -43,6 +44,7 @@
                r.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@WM_OUTSOURCE_RECPT} AS sourceDocType,
                r.code AS sourceDocCode,
                NULL AS batchCode,
                rl.id AS sourceLineId,
                r.receipt_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IQC.type} AS qcType,
@@ -68,6 +70,7 @@
                pf.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@PRO_FEEDBACK} AS sourceDocType,
                pf.code AS sourceDocCode,
                ppl.batch_code AS batchCode,
                ppl.id AS sourceLineId,
                pf.feedback_time AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IPQC.type} AS qcType,
@@ -93,6 +96,7 @@
                s.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@WM_PRODUCT_SALES} AS sourceDocType,
                s.code AS sourceDocCode,
                sl.batch_code AS batchCode,
                sl.id AS sourceLineId,
                s.sales_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@OQC.type} AS qcType,
@@ -118,6 +122,7 @@
                ri.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@WM_RETURN_ISSUE} AS sourceDocType,
                ri.code AS sourceDocCode,
                ril.batch_code AS batchCode,
                ril.id AS sourceLineId,
                ri.return_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
@@ -144,6 +149,7 @@
                r.id AS sourceDocId,
                ${@cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants@WM_RETURN_SALES} AS sourceDocType,
                r.code AS sourceDocCode,
                rl.batch_code AS batchCode,
                rl.id AS sourceLineId,
                r.return_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java
@@ -2,13 +2,9 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
@@ -16,15 +12,10 @@
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -34,6 +25,7 @@
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -60,11 +52,7 @@
    @MockitoBean
    private MesQcIpqcLineService ipqcLineService;
    @MockitoBean
    private MesProWorkOrderService workOrderService;
    @MockitoBean
    private MesMdWorkstationService workstationService;
    @MockitoBean
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @MockitoBean
    private MesQcDefectRecordService defectRecordService;
    @MockitoBean
@@ -72,15 +60,11 @@
    @MockitoBean
    private MesProFeedbackService feedbackService;
    @MockitoBean
    private MesProRouteProductService routeProductService;
    @MockitoBean
    private MesProRouteProcessService routeProcessService;
    @MockitoBean
    private MesProTaskService taskService;
    @MockitoBean
    private MesQcIndicatorResultService indicatorResultService;
    @MockitoBean
    private MesWmProductProduceLineService productProduceLineService;
    @MockitoBean
    private MesQcNcrService ncrService;
    @Test
    public void testFinishIpqc_writeBack_feedback() {
@@ -185,34 +169,46 @@
    @Test
    public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
        // 准备数据:sourceDocType 非空但 sourceDocId 为空,应该抛异常
        // 准备数据:sourceDocType 非空但 sourceDocId 为空,跳过回写
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
            o.setSourceDocId(null); // 脏数据
            o.setSourceLineId(null);
            o.setUnqualifiedQuantity(BigDecimal.ZERO);
        });
        ipqcMapper.insert(ipqc);
        // 调用,并断言异常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
        // 调用:不抛异常,仅跳过回写
        ipqcService.finishIpqc(ipqc.getId());
        MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
        assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
        verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
                anyLong(), anyLong(), any(), any(), any(), any(), any());
    }
    @Test
    public void testFinishIpqc_writeBack_unknownSourceDocType() {
        // 准备数据:sourceDocType 无法识别
        // 准备数据:sourceDocType 无法识别,回写阶段不校验类型(仅按 sourceDocId 回写报工)
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(99999); // 无法识别的类型
            o.setSourceDocId(randomLongId());
            o.setSourceLineId(null);
            o.setSourceLineId(randomLongId());
            o.setUnqualifiedQuantity(BigDecimal.ZERO);
        });
        ipqcMapper.insert(ipqc);
        // 调用,并断言异常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
        // 调用:按 sourceDocId 回写报工
        ipqcService.finishIpqc(ipqc.getId());
        MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
        assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
        verify(feedbackService).updateProFeedbackWhenIpqcFinish(
                anyLong(), anyLong(), any(), any(), any(), any(), any());
    }
    @Test
@@ -242,159 +238,82 @@
        assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    // ==================== processId 推导行为测试 ====================
    // ==================== 批号必填校验 ====================
    /**
     * 工位有工序 + 该工序在产品工艺路线中 → processId 应该被设置为工位工序
     * 批号为 null → 创建失败
     */
    @Test
    public void testCreateIpqc_processId_workstationHasProcess() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long templateId = randomLongId();
    public void testCreateIpqc_batchCode_null() {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-001");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setCode("IPQC-BC-001");
        reqVO.setBatchCode(null);
        reqVO.setItemId(randomLongId());
        reqVO.setInspectorUserId(randomLongId());
        // mock 工位返回有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(processId);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        // mock 工艺路线
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
        when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
        // mock 工序在路线中
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(new MesProRouteProcessDO());
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是工位工序
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertEquals(processId, ipqc.getProcessId());
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode());
    }
    /**
     * 工位没有工序(processId=null)→ processId 应该留 null(不回退到任务工序)
     * 批号为空白串 → 创建失败(兜底内部/RPC 调用绕过 VO 校验的情况)
     */
    @Test
    public void testCreateIpqc_processId_workstationNoProcess_shouldBeNull() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long taskId = randomLongId();
        Long templateId = randomLongId();
    public void testCreateIpqc_batchCode_blank() {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-002");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setTaskId(taskId); // 故意传入 taskId,验证不会回退
        reqVO.setCode("IPQC-BC-002");
        reqVO.setBatchCode("  ");
        reqVO.setItemId(randomLongId());
        reqVO.setInspectorUserId(randomLongId());
        // mock 工位没有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null); // 无工序
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 任务
        MesProTaskDO task = new MesProTaskDO();
        task.setId(taskId);
        task.setWorkOrderId(workOrderId);
        task.setWorkstationId(workstationId);
        task.setItemId(productId);
        when(taskService.validateTaskNotFinished(taskId)).thenReturn(task);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode());
    }
    // ==================== 创建成功:批号 / 物料落库 ====================
    @Test
    public void testCreateIpqc_success() {
        // 准备参数
        Long itemId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-OK-001");
        reqVO.setName("测试过程检验");
        reqVO.setBatchCode("BC-2025-001");
        reqVO.setItemId(itemId);
        reqVO.setInspectorUserId(randomLongId());
        mockTemplate(itemId, templateId);
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是 null,即使传入了 taskId 也不应该回退
        // 断言:批号与物料落库,模板按物料+IPQC 类型匹配
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId(),
                "工位无工序时 processId 应为 null,不应回退到任务工序");
        // 断言:不应该读取任务的 processId
        verify(routeProductService, never()).getRouteProductByItemId(any());
        assertNotNull(ipqc);
        assertEquals("BC-2025-001", ipqc.getBatchCode());
        assertEquals(itemId, ipqc.getItemId());
        assertEquals(templateId, ipqc.getTemplateId());
        verify(mdmItemApi).validateItemExists(itemId);
    }
    /**
     * 工位有工序但该工序不在产品工艺路线中 → processId 应该留 null
     * 传入了不在匹配列表中的 templateId → 拒绝创建
     */
    @Test
    public void testCreateIpqc_processId_processNotInRoute() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long templateId = randomLongId();
    public void testCreateIpqc_templateId_notMatched() {
        Long itemId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-003");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setCode("IPQC-TPL-001");
        reqVO.setName("指定方案不匹配");
        reqVO.setBatchCode("BC-2025-002");
        reqVO.setItemId(itemId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setTemplateId(randomLongId()); // 与匹配结果不一致
        mockTemplate(itemId, randomLongId());
        // mock 工位有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(processId);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        // mock 工艺路线存在
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
        when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
        // mock 工序不在路线中
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(null);
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是 null
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId());
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode());
    }
    // ==================== sourceLineId 校验测试 ====================
@@ -404,35 +323,10 @@
     */
    @Test
    public void testCreateIpqc_sourceLineId_required() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long itemId = randomLongId();
        Long feedbackId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-001");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(null); // 不传 sourceLineId
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-001", itemId, feedbackId, null);
        mockTemplate(itemId, randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // 调用,断言异常
@@ -445,36 +339,11 @@
     */
    @Test
    public void testCreateIpqc_sourceLineId_notBelong() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long itemId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-002");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-002", itemId, feedbackId, sourceLineId);
        mockTemplate(itemId, randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在但 feedbackId 不匹配
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -493,36 +362,11 @@
     */
    @Test
    public void testCreateIpqc_sourceLineId_notPending() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long itemId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-003");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-003", itemId, feedbackId, sourceLineId);
        mockTemplate(itemId, randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在且归属正确,但不是 PENDING 状态
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -541,36 +385,11 @@
     */
    @Test
    public void testCreateIpqc_sourceLineId_success() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long itemId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-004");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-004", itemId, feedbackId, sourceLineId);
        mockTemplate(itemId, randomLongId());
        MesProFeedbackDO feedback = new MesProFeedbackDO();
        feedback.setId(feedbackId);
        feedback.setCode("FB-001");
@@ -593,4 +412,26 @@
        assertEquals("FB-001", ipqc.getSourceDocCode());
    }
    // ==================== 测试辅助 ====================
    private MesQcIpqcSaveReqVO buildFeedbackReqVO(String code, Long itemId, Long feedbackId, Long sourceLineId) {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode(code);
        reqVO.setName("来源校验过程检验");
        reqVO.setBatchCode("BC-SL-001");
        reqVO.setItemId(itemId);
        reqVO.setInspectorUserId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        return reqVO;
    }
    private void mockTemplate(Long itemId, Long templateId) {
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getTemplateItemList(eq(itemId), anyInt()))
                .thenReturn(List.of(templateItem));
    }
}
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -242,10 +242,7 @@
    "source_doc_id" bigint DEFAULT NULL,
    "source_line_id" bigint DEFAULT NULL,
    "source_doc_code" varchar(64) DEFAULT NULL,
    "work_order_id" bigint DEFAULT NULL,
    "task_id" bigint DEFAULT NULL,
    "workstation_id" bigint DEFAULT NULL,
    "process_id" bigint DEFAULT NULL,
    "batch_code" varchar(100) DEFAULT NULL,
    "item_id" bigint DEFAULT NULL,
    "check_quantity" decimal(14,2) DEFAULT NULL,
    "qualified_quantity" decimal(14,2) DEFAULT NULL,
yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/instance/MesQcReportContextMapper.java
@@ -67,7 +67,7 @@
                .setProductCode(StrUtil.nullToEmpty(data.getItemCode()))
                .setProductName(StrUtil.nullToEmpty(data.getItemName()))
                .setSpec(StrUtil.nullToEmpty(data.getItemSpecification()))
                // 来料检验的批次在供应商批号上,出货/退货在批次号上,过程检验两者都没有(只有工单)
                // 过程/出货/退货检验的批次取自单据的批号,来料检验没有批号则回落到供应商批号
                .setBatchNo(firstNonBlank(data.getBatchCode(), data.getVendorBatch()))
                .setWorkOrderNo(StrUtil.nullToEmpty(data.getWorkOrderCode()))
                .setInspectType(StrUtil.nullToEmpty(data.getQcTypeName()))