8 小时以前 b630f868ae5cb4bc2e92c48d275341d36d260499
feat(qc): 新增OQC出货检验功能及优化质量流程

- 新增MES出货检验检测结果字典类型(mes_oqc_check_result)
- 添加OQC检验报告打印功能,支持产品等级判定(1型/2型/废品)
- 更新质量流程图,增加出货判定节点和产品等级分类
- 修改IPQC表头字段,移除生产订单/工位等字段,保留批号必填
- 优化AI生产风险预测,移除IPQC缺陷率相关逻辑
- 更新SQL配置文件,添加OQC检验相关数据字典
- 调整检验报告打印格式,增加质检专用章和签字区域
- 修订销售出库单逻辑,废品等级检验将取消整单出库
已添加2个文件
已修改33个文件
926 ■■■■■ 文件已修改
.playwright-mcp/产品检验单-1.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/business.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/flows.json 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/modules.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/objects.json 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/index.html 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/05-quality-flow.mmd 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/11-ai-business-flow.mmd 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_chemical_report_integration.md 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260904.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicatorresult/MesQcIndicatorResultController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicatorresult/vo/MesQcQcReportRespVO.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcPageReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | 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/dal/dataobject/qc/ipqc/MesQcIpqcDO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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/enums/DictTypeConstants.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcOqcCheckResultEnum.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java 37 ●●●● 补丁 | 查看 | 原始文档 | 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/indicatorresult/MesQcProductReportWordBuilder.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcReportServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | 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/indicatorresult/MesQcProductReportWordBuilderTest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java 411 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/²úÆ·¼ìÑéµ¥-1.docx
Binary files differ
docs/project-business/data/business.json
@@ -110,7 +110,7 @@
    },
    {
      "name": "生产风险预测",
      "flow": "IPQC ç¼ºé™·çއ+设备保养点检+物料可用率<20%+关键工序 â†’ chat â†’ ç»¼åˆé£Žé™©+因素",
      "flow": "设备保养点检+物料可用率<20%+关键工序 â†’ chat â†’ ç»¼åˆé£Žé™©+因素",
      "callsApis": [
        "MdmItemApi"
      ]
docs/project-business/data/flows.json
@@ -29,13 +29,13 @@
       {"stage": "联动", "action": "审批通过按工序标记: å€’冲→自动物料消耗扣线边库(允许负库存);质检→产出待检行等 IPQC;关键非检→直接产出入库", "roles": ["系统自动"]},
       {"stage": "完工", "action": "仅末道工序回写工单已生产数量,达标自动完成;MPS ç´¯è®¡åˆ¤æ–­å®Œæˆå¹¶å‘事件给销售订单", "roles": ["系统自动"]}]},
    {"id": "quality", "name": "质量流程", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + åˆ¤å®šå››æžœ(合格/特采/退货/报废) + NCR è¯„审处置关闭;AI æä¾›åˆ¤å®šå»ºè®®ï¼›NCR å½“前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论/质检章/检验·复核·批准签字留空手填,Word æ‰“印)。",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + åˆ¤å®šå››æžœ(合格/特采/退货/报废;OQC æ”¹æŒ‰äº§å“ç­‰çº§ 1型/2型/废品,仅废品判不合格) + NCR è¯„审处置关闭;AI æä¾›åˆ¤å®šå»ºè®®ï¼›NCR å½“前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论/质检章/检验·复核·批准签字留空手填,Word æ‰“印)。",
     "steps": [
       {"stage": "来料", "action": "到货通知/外协入库→IQC,结论回写推进待入库或触发退货", "roles": ["IQC è´¨æ£€å‘˜"]},
       {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC è´¨æ£€å‘˜"]},
       {"stage": "出货", "action": "销售出库提交→OQC,完成后出库单进入待拣货", "roles": ["OQC è´¨æ£€å‘˜"]},
       {"stage": "出货", "action": "销售出库提交→OQC(检测结果按产品等级 1型/2型/废品必填,1型/2型判行合格,废品判行不合格),全部合格后出库单进入待拣货,出现废品则整单取消", "roles": ["OQC è´¨æ£€å‘˜"]},
       {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC è´¨æ£€å‘˜"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report èšåˆåŽå‰ç«¯æ‰“印检验报告(含采样地点/证书编号/执行标准/复核·审核签字区);/indicator-result/export-product-word å¯¼å‡ºäº§å“æ£€éªŒå•Word(无技术指标列·判定结论/质检专用章/检验·复核·批准签字留空手填·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report èšåˆåŽå‰ç«¯æ‰“印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word å¯¼å‡ºäº§å“æ£€éªŒå•Word(无技术指标列·判定结论带出检验结果·质检章/检验·复核·批准签字留空手填·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "不合格", "action": "NCR: å¾…评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}]},
    {"id": "warehouse", "name": "仓储流程", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6",
     "summary": "15 ç±»å•据驱动库存台账(六元组、四量)变动,全部经事务流水留痕;支持调拨在途、盘点调整、批次/SN è¿½æº¯ã€åº“存占用与四级冻结、线边虚拟库负库存;对外 WmsStockApi ä¾› ERP æ”¶å‘。",
docs/project-business/data/modules.json
@@ -51,7 +51,7 @@
    {"key": "qc", "name": "质量管理", "color": "#e06c6c", "tag": "MES-QC", "flow": "quality",
     "positioning": "四道检验闸口 + ä¸åˆæ ¼å“é—­çŽ¯å¤„ç½®",
     "problem": "来料/过程/出货/退货检验标准化,不合格可追溯处置",
     "features": ["IQC æ¥æ–™(到货/外协)", "IPQC è¿‡ç¨‹(联动报工)", "OQC å‡ºè´§(联动销售出库)", "RQC é€€è´§(联动退料/售后)", "质检方案·指标·缺陷库", "检测结果:合格/特采/退货/报废", "NCR:评审→处置(退货/报废/返工/让步/降级)→关闭", "AI åˆ¤å®šå»ºè®®"],
     "features": ["IQC æ¥æ–™(到货/外协)", "IPQC è¿‡ç¨‹(联动报工)", "OQC å‡ºè´§(联动销售出库)", "RQC é€€è´§(联动退料/售后)", "质检方案·指标·缺陷库", "检测结果:IQC/IPQC/RQC åˆæ ¼Â·ç‰¹é‡‡Â·é€€è´§Â·æŠ¥åºŸ / OQC 1型·2型·废品", "NCR:评审→处置(退货/报废/返工/让步/降级)→关闭", "AI åˆ¤å®šå»ºè®®"],
     "objects": ["IQC", "IPQC", "OQC", "RQC", "质检方案", "NCR", "缺陷记录"],
     "roles": ["质检员(IQC/IPQC/OQC)", "质量工程师"],
     "upstream": ["仓储(到货)", "生产(报工)", "销售(出库)", "售后(退货)"], "downstream": ["仓储(合格入库/报废出库)", "生产(NCR返工)"],
docs/project-business/data/objects.json
@@ -28,7 +28,7 @@
    {"name": "到货通知单", "module": "仓储", "table": "mes_wm_arrival_notice", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4", "upstream": ["采购订单"], "downstream": ["IQC", "采购入库"], "keywords": "到货 æ”¶è´§ arrival"},
    {"name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus", "upstream": ["到货通知"], "downstream": ["库存台账", "采购订单(回写)"], "keywords": "采购入库 æ”¶è´§å…¥åº“ receipt"},
    {"name": "生产领料单", "module": "仓储", "table": "mes_wm_product_issue", "doc": "草稿0→待拣货2→待执行3→完成4,库存-", "upstream": ["生产工单"], "downstream": ["库存台账"], "keywords": "领料 issue"},
    {"name": "销售出库单", "module": "仓储", "table": "mes_wm_product_sales", "doc": "发货通知审核后生成,提交→OQC检测→拣货→发运10→待执行3→完成4,回写销售订单出库数与状态", "upstream": ["发货通知", "销售订单"], "downstream": ["库存台账", "OQC"], "keywords": "销售出库 å‘è´§ å‡ºåº“单 sales"},
    {"name": "销售出库单", "module": "仓储", "table": "mes_wm_product_sales", "doc": "发货通知审核后生成,提交→OQC检测→拣货→发运10→待执行3→完成4,回写销售订单出库数与状态;任一 OQC è¡Œåˆ¤åºŸå“åˆ™æ•´å•取消", "upstream": ["发货通知", "销售订单"], "downstream": ["库存台账", "OQC"], "keywords": "销售出库 å‘è´§ å‡ºåº“单 sales"},
    {"name": "销售退货单", "module": "仓储", "table": "mes_wm_return_sales", "doc": "草稿→待检验(RQC)→待上架→待入库→完成,售后与手工两来源", "upstream": ["售后退货申请"], "downstream": ["库存台账", "RQC"], "keywords": "销售退货 é€€è´§å…¥åº“"},
    {"name": "供应商退货单", "module": "仓储", "table": "mes_wm_return_vendor", "doc": "IQC ä¸åˆæ ¼é€€è´§å‘起,待拣货→待执行→完成,库存-", "upstream": ["IQC"], "downstream": ["库存台账", "采购订单(returnCount)"], "keywords": "供应商退货 é€€æ–™ é€€è´§ vendor"},
    {"name": "调拨单", "module": "仓储", "table": "mes_wm_transfer", "doc": "内部/外部调拨,成对 MOVE_OUT+MOVE_IN äº‹åŠ¡ï¼Œåœ¨é€”æ•°é‡", "upstream": [], "downstream": ["库存台账"], "keywords": "调拨 ç§»åº“ transfer"},
@@ -36,8 +36,8 @@
    {"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": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进)"], "keywords": "OQC å‡ºè´§æ£€éªŒ å‡ºåŽ‚æ£€éªŒ"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;表头=来源单据(只读)+产品物料(MDM,必填)+批号(必填)+数量+检验信息,已去掉生产订单/工位/生产任务/工序", "upstream": ["报工"], "downstream": ["产出入库", "NCR"], "keywords": "IPQC è¿‡ç¨‹æ£€éªŒ å·¡æ£€ æ‰¹å·"},
    {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,草稿0→完成4;检测结果按产品等级判定(1型/2型/废品,必填),1型/2型判行合格并推进出库单待拣货,废品判行不合格并取消销售出库单;发货数量/出货日期可空", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进或取消)"], "keywords": "OQC å‡ºè´§æ£€éªŒ å‡ºåŽ‚æ£€éªŒ 1型 2型 åºŸå“"},
    {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;四类检验单均支持采样地点/证书编号/执行标准表头与复核·审核人,并可打印检验报告", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "检验报告打印"], "keywords": "RQC é€€è´§æ£€éªŒ"},
    {"name": "检验指标", "module": "质量", "table": "mes_qc_indicator", "doc": "支持两级分组树(itemType录入/分组+parentId)、公式文本展示、单位文本、同级排序;分组项不录值;有子项禁删;同层级名称可重复", "upstream": [], "downstream": ["质检方案", "检验报告打印"], "keywords": "检验指标 indicator åˆ†ç»„ å…¬å¼ åŒ–å·¥"},
    {"name": "检验报告打印", "module": "质量", "table": "mes_qc_indicator_result_detail", "doc": "化工报告范式:平行测量批次(measure_no)+分组矩阵+公式列+签字区;/mes/qc/indicator-result/report èšåˆï¼Œå‰ç«¯æµè§ˆå™¨æ‰“印", "upstream": ["IQC来料检验", "IPQC过程检验", "OQC出货检验", "RQC退货检验", "检验指标"], "downstream": [], "keywords": "打印 æŠ¥å‘Š æµ‹é‡æ‰¹æ¬¡ ç­¾å­— æ­£ä¸é†‡ GB/T6027"},
docs/project-business/index.html
@@ -290,7 +290,7 @@
    },
    {
      "name": "生产风险预测",
      "flow": "IPQC ç¼ºé™·çއ+设备保养点检+物料可用率<20%+关键工序 â†’ chat â†’ ç»¼åˆé£Žé™©+因素",
      "flow": "设备保养点检+物料可用率<20%+关键工序 â†’ chat â†’ ç»¼åˆé£Žé™©+因素",
      "callsApis": [
        "MdmItemApi"
      ]
@@ -381,7 +381,7 @@
    {"key": "qc", "name": "质量管理", "color": "#e06c6c", "tag": "MES-QC", "flow": "quality",
     "positioning": "四道检验闸口 + ä¸åˆæ ¼å“é—­çŽ¯å¤„ç½®",
     "problem": "来料/过程/出货/退货检验标准化,不合格可追溯处置",
     "features": ["IQC æ¥æ–™(到货/外协)", "IPQC è¿‡ç¨‹(联动报工)", "OQC å‡ºè´§(联动销售出库)", "RQC é€€è´§(联动退料/售后)", "质检方案·指标·缺陷库", "检测结果:合格/特采/退货/报废", "NCR:评审→处置(退货/报废/返工/让步/降级)→关闭", "AI åˆ¤å®šå»ºè®®"],
     "features": ["IQC æ¥æ–™(到货/外协)", "IPQC è¿‡ç¨‹(联动报工)", "OQC å‡ºè´§(联动销售出库)", "RQC é€€è´§(联动退料/售后)", "质检方案·指标·缺陷库", "检测结果:IQC/IPQC/RQC åˆæ ¼Â·ç‰¹é‡‡Â·é€€è´§Â·æŠ¥åºŸ / OQC 1型·2型·废品", "NCR:评审→处置(退货/报废/返工/让步/降级)→关闭", "AI åˆ¤å®šå»ºè®®"],
     "objects": ["IQC", "IPQC", "OQC", "RQC", "质检方案", "NCR", "缺陷记录"],
     "roles": ["质检员(IQC/IPQC/OQC)", "质量工程师"],
     "upstream": ["仓储(到货)", "生产(报工)", "销售(出库)", "售后(退货)"], "downstream": ["仓储(合格入库/报废出库)", "生产(NCR返工)"],
@@ -463,7 +463,7 @@
    {"name": "到货通知单", "module": "仓储", "table": "mes_wm_arrival_notice", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4", "upstream": ["采购订单"], "downstream": ["IQC", "采购入库"], "keywords": "到货 æ”¶è´§ arrival"},
    {"name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus", "upstream": ["到货通知"], "downstream": ["库存台账", "采购订单(回写)"], "keywords": "采购入库 æ”¶è´§å…¥åº“ receipt"},
    {"name": "生产领料单", "module": "仓储", "table": "mes_wm_product_issue", "doc": "草稿0→待拣货2→待执行3→完成4,库存-", "upstream": ["生产工单"], "downstream": ["库存台账"], "keywords": "领料 issue"},
    {"name": "销售出库单", "module": "仓储", "table": "mes_wm_product_sales", "doc": "发货通知审核后生成,提交→OQC检测→拣货→发运10→待执行3→完成4,回写销售订单出库数与状态", "upstream": ["发货通知", "销售订单"], "downstream": ["库存台账", "OQC"], "keywords": "销售出库 å‘è´§ å‡ºåº“单 sales"},
    {"name": "销售出库单", "module": "仓储", "table": "mes_wm_product_sales", "doc": "发货通知审核后生成,提交→OQC检测→拣货→发运10→待执行3→完成4,回写销售订单出库数与状态;任一 OQC è¡Œåˆ¤åºŸå“åˆ™æ•´å•取消", "upstream": ["发货通知", "销售订单"], "downstream": ["库存台账", "OQC"], "keywords": "销售出库 å‘è´§ å‡ºåº“单 sales"},
    {"name": "销售退货单", "module": "仓储", "table": "mes_wm_return_sales", "doc": "草稿→待检验(RQC)→待上架→待入库→完成,售后与手工两来源", "upstream": ["售后退货申请"], "downstream": ["库存台账", "RQC"], "keywords": "销售退货 é€€è´§å…¥åº“"},
    {"name": "供应商退货单", "module": "仓储", "table": "mes_wm_return_vendor", "doc": "IQC ä¸åˆæ ¼é€€è´§å‘起,待拣货→待执行→完成,库存-", "upstream": ["IQC"], "downstream": ["库存台账", "采购订单(returnCount)"], "keywords": "供应商退货 é€€æ–™ é€€è´§ vendor"},
    {"name": "调拨单", "module": "仓储", "table": "mes_wm_transfer", "doc": "内部/外部调拨,成对 MOVE_OUT+MOVE_IN äº‹åŠ¡ï¼Œåœ¨é€”æ•°é‡", "upstream": [], "downstream": ["库存台账"], "keywords": "调拨 ç§»åº“ transfer"},
@@ -471,11 +471,11 @@
    {"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": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进)"], "keywords": "OQC å‡ºè´§æ£€éªŒ å‡ºåŽ‚æ£€éªŒ"},
    {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;表头=来源单据(只读)+产品物料(MDM,必填)+批号(必填)+数量+检验信息,已去掉生产订单/工位/生产任务/工序", "upstream": ["报工"], "downstream": ["产出入库", "NCR"], "keywords": "IPQC è¿‡ç¨‹æ£€éªŒ å·¡æ£€ æ‰¹å·"},
    {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,草稿0→完成4;检测结果按产品等级判定(1型/2型/废品,必填),1型/2型判行合格并推进出库单待拣货,废品判行不合格并取消销售出库单;发货数量/出货日期可空", "upstream": ["销售出库单"], "downstream": ["销售出库单(推进或取消)"], "keywords": "OQC å‡ºè´§æ£€éªŒ å‡ºåŽ‚æ£€éªŒ 1型 2型 åºŸå“"},
    {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;四类检验单均支持采样地点/证书编号/执行标准表头与复核·审核人,并可打印检验报告", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "检验报告打印"], "keywords": "RQC é€€è´§æ£€éªŒ"},
    {"name": "检验指标", "module": "质量", "table": "mes_qc_indicator", "doc": "支持两级分组树(itemType录入/分组+parentId)、公式文本展示、单位文本、同级排序;分组项不录值;有子项禁删;同层级名称可重复", "upstream": [], "downstream": ["质检方案", "检验报告打印"], "keywords": "检验指标 indicator åˆ†ç»„ å…¬å¼ åŒ–å·¥"},
    {"name": "检验报告打印", "module": "质量", "table": "mes_qc_indicator_result_detail", "doc": "化工报告范式:平行测量批次(measure_no)+分组矩阵+公式列+签字区;/mes/qc/indicator-result/report èšåˆï¼Œå‰ç«¯æµè§ˆå™¨æ‰“印;另有产品检验单Word导出 /mes/qc/indicator-result/export-product-word(纸质检验单版式:无技术指标列,判定结论/质检专用章留空手填,平行测量顿号合并,生产单位写死、生产时间=单据创建时间,下载后Word打印)", "upstream": ["IQC来料检验", "IPQC过程检验", "OQC出货检验", "RQC退货检验", "检验指标"], "downstream": [], "keywords": "打印 æŠ¥å‘Š æµ‹é‡æ‰¹æ¬¡ ç­¾å­— æ­£ä¸é†‡ GB/T6027 Word äº§å“æ£€éªŒå•"},
    {"name": "检验报告打印", "module": "质量", "table": "mes_qc_indicator_result_detail", "doc": "化工报告范式:平行测量批次(measure_no)+分组矩阵+公式列+签字区;/mes/qc/indicator-result/report èšåˆï¼Œå‰ç«¯æµè§ˆå™¨æ‰“印", "upstream": ["IQC来料检验", "IPQC过程检验", "OQC出货检验", "RQC退货检验", "检验指标"], "downstream": [], "keywords": "打印 æŠ¥å‘Š æµ‹é‡æ‰¹æ¬¡ ç­¾å­— æ­£ä¸é†‡ GB/T6027"},
    {"name": "NCR不合格品", "module": "质量", "table": "mes_qc_ncr", "doc": "待评审0→已评审6→已处置7→已关闭8,处置:退货/报废/返工/让步接收/降级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["退货/报废/返工(台账)"], "keywords": "NCR ä¸åˆæ ¼ å¤„ç½® ç‰¹é‡‡ æŠ¥åºŸ"},
    {"name": "设备", "module": "设备", "table": "mes_dv_machinery", "doc": "台账状态:停机/生产中/保养中;点检·保养·维修工单;AI é£Žé™©é¢„测消费其数据", "upstream": [], "downstream": ["点检", "ç»´ä¿®", "生产风险预测"], "keywords": "设备 ç‚¹æ£€ ä¿å…» ç»´ä¿® machinery"},
    {"name": "售后工单", "module": "售后", "table": "after_sale_ticket", "doc": "草稿0→处理中10→待入库30→待退款40→完结50→关闭60/取消99,判定:一般/ç»´ä¿®/退货", "upstream": ["客户反馈(人工录入)"], "downstream": ["维修记录", "退货申请"], "keywords": "售后 å·¥å• ticket ç»´ä¿®"},
@@ -516,13 +516,13 @@
       {"stage": "联动", "action": "审批通过按工序标记: å€’冲→自动物料消耗扣线边库(允许负库存);质检→产出待检行等 IPQC;关键非检→直接产出入库", "roles": ["系统自动"]},
       {"stage": "完工", "action": "仅末道工序回写工单已生产数量,达标自动完成;MPS ç´¯è®¡åˆ¤æ–­å®Œæˆå¹¶å‘事件给销售订单", "roles": ["系统自动"]}]},
    {"id": "quality", "name": "质量流程", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + åˆ¤å®šå››æžœ(合格/特采/退货/报废) + NCR è¯„审处置关闭;AI æä¾›åˆ¤å®šå»ºè®®ï¼›NCR å½“前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质版式,判定结论/质检章/检验·复核·批准签字留空手填,Word æ‰“印)。",
     "summary": "四道检验闸口(IQC/IPQC/OQC/RQC,二态 0→4) + åˆ¤å®šå››æžœ(合格/特采/退货/报废;OQC æ”¹æŒ‰äº§å“ç­‰çº§ 1型/2型/废品,仅废品判不合格) + NCR è¯„审处置关闭;AI æä¾›åˆ¤å®šå»ºè®®ï¼›NCR å½“前仅台账不触发返工单。支持化工报告范式:分组指标树+公式文本+平行测量批次+采样表头+签字区,浏览器打印检验报告;并可导出产品检验单 Word(纸质检验单版式,判定结论/质检章/检验·复核·批准签字留空手填,Word æ‰“印)。",
     "steps": [
       {"stage": "来料", "action": "到货通知/外协入库→IQC,结论回写推进待入库或触发退货", "roles": ["IQC è´¨æ£€å‘˜"]},
       {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC è´¨æ£€å‘˜"]},
       {"stage": "出货", "action": "销售出库提交→OQC,完成后出库单进入待拣货", "roles": ["OQC è´¨æ£€å‘˜"]},
       {"stage": "出货", "action": "销售出库提交→OQC(检测结果按产品等级 1型/2型/废品必填,1型/2型判行合格,废品判行不合格),全部合格后出库单进入待拣货,出现废品则整单取消", "roles": ["OQC è´¨æ£€å‘˜"]},
       {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC è´¨æ£€å‘˜"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report èšåˆåŽå‰ç«¯æ‰“印检验报告(含采样地点/证书编号/执行标准/复核·审核签字区);/indicator-result/export-product-word å¯¼å‡ºäº§å“æ£€éªŒå•Word(无技术指标列·判定结论/质检章/检验·复核·批准签字留空手填·Word打印;详情检测结果Tab可按样品查看只读明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "报告", "action": "检验结果按分组树+测量批次录入,/indicator-result/report èšåˆåŽå‰ç«¯æ‰“印检验报告(含采样地点/证书编号/执行标准/判定结论·质检专用章/复核·审核签字区;判定结论带出检验结果·质检章留空盖章·无平行测量时列头称测量值);/indicator-result/export-product-word å¯¼å‡ºäº§å“æ£€éªŒå•Word(无技术指标列·判定结论带出检验结果·质检章/检验·复核·批准签字留空手填·平行测量顿号合并·下载后Word打印;质检单详情检测结果Tab可按样品查看只读检测值明细)", "roles": ["质检员", "复核人", "审核人"]},
       {"stage": "不合格", "action": "NCR: å¾…评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}]},
    {"id": "warehouse", "name": "仓储流程", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6",
     "summary": "15 ç±»å•据驱动库存台账(六元组、四量)变动,全部经事务流水留痕;支持调拨在途、盘点调整、批次/SN è¿½æº¯ã€åº“存占用与四级冻结、线边虚拟库负库存;对外 WmsStockApi ä¾› ERP æ”¶å‘。",
@@ -739,19 +739,25 @@
  T4[生产退料单 æˆ– é”€å”®é€€è´§å• æˆ– å”®åŽé€€è´§] --> A4[RQC é€€è´§æ£€éªŒÂ·è‰ç¨¿0→完成4]
  A1 --> R{检验判定 MesQcCheckResultEnum}
  A2 --> R
  A3 --> R
  A4 --> R
  R -->|1 åˆæ ¼| Q1[允许入库·回写来源单据推进状态]
  R -->|2 ç‰¹é‡‡| Q2[允许入库·并生成NCR留痕]
  R -->|3 ä¸åˆæ ¼é€€è´§| Q3[生成NCR·走供应商退货或销售退货]
  R -->|4 ä¸åˆæ ¼æŠ¥åºŸ| Q4[生成NCR·走报废出库]
  A3 --> R2{出货判定 MesQcOqcCheckResultEnum·必填}
  R2 -->|1型/2型| S1[出库行判合格·推进出库单待拣货]
  R2 -->|废品| S2[出库行判不合格·取消销售出库单]
  A3 -.-> N5[表头:发货数量/出货日期非必填·检测结果必填]
  A2 -.-> N6[表头:来源单据只读·产品物料(MDM)必填·批号必填·已去掉生产订单/工位/生产任务/工序]
  A2 -->|合格数量| Q5[报工转完成·产出拆行入库]
  A3 -->|完成| Q6[销售出库单推进待拣货]
  A4 -->|完成| Q7[退货单推进上架或入库·售后回调结论]
  N1[NCR å¾…评审0] -->|评审| N2[已评审6]
  N2 -->|处置方式:退货1报废2返工3让步接收4降级使用5| N3[已处置7]
  N3 -->|关闭| N4[已关闭8]
  Q8[支撑数据:质检方案·检验指标·缺陷库·指标结果] --- A1
  Q9[化工报告增强:指标分组树+公式文本·平行测量批次·采样地点/证书编号/执行标准表头·复核人/审核人] --- Q8
  A1 & A2 & A3 & A4 --> P1[检验报告单打印·前端浏览器打印·判定结论带出检验结果+质检专用章留空盖章+签字区·无平行测量时列头称测量值]
  A1 & A2 & A3 & A4 --> P2[产品检验单Word导出·export-product-word·无技术指标列·判定结论带出检验结果/质检章留空盖章·Word打印]
</pre>
<pre style="display:none" class="mmd-src" data-mmd="06-warehouse-flow">
flowchart LR
@@ -932,7 +938,7 @@
  J4 --> J5[质检员参考判定·AI不可用时提示人工判定]
  R1[工单BOM+库存可用量+已投料] --> R2[物料短缺预测·风险等级与缺口清单]
  R3[工序标准工时+设备每小时产量+传送倍率] --> R4[生产时长预测]
  R5[IPQC缺陷率+设备保养点检+物料可用率+关键工序] --> R6[生产风险预测]
  R5[设备保养点检+物料可用率+关键工序] --> R6[生产风险预测]
  R7[进度+剩余天数+任务完成状态+客户交期] --> R8[交付预测·能否按时+置信度+延误因素]
  KB1[知识库文档上传URL] --> KB2[POI/Tika提取文本] --> KB3[切片约800tokens→向量化→Milvus]
  KB3 --> KB4[分段检索接口 topK+相似度阈值]
docs/project-business/mermaid/05-quality-flow.mmd
@@ -5,19 +5,22 @@
  T4[生产退料单 æˆ– é”€å”®é€€è´§å• æˆ– å”®åŽé€€è´§] --> A4[RQC é€€è´§æ£€éªŒÂ·è‰ç¨¿0→完成4]
  A1 --> R{检验判定 MesQcCheckResultEnum}
  A2 --> R
  A3 --> R
  A4 --> R
  R -->|1 åˆæ ¼| Q1[允许入库·回写来源单据推进状态]
  R -->|2 ç‰¹é‡‡| Q2[允许入库·并生成NCR留痕]
  R -->|3 ä¸åˆæ ¼é€€è´§| Q3[生成NCR·走供应商退货或销售退货]
  R -->|4 ä¸åˆæ ¼æŠ¥åºŸ| Q4[生成NCR·走报废出库]
  A3 --> R2{出货判定 MesQcOqcCheckResultEnum·必填}
  R2 -->|1型/2型| S1[出库行判合格·推进出库单待拣货]
  R2 -->|废品| S2[出库行判不合格·取消销售出库单]
  A3 -.-> N5[表头:发货数量/出货日期非必填·检测结果必填]
  A2 -.-> N6[表头:来源单据只读·产品物料(MDM)必填·批号必填·已去掉生产订单/工位/生产任务/工序]
  A2 -->|合格数量| Q5[报工转完成·产出拆行入库]
  A3 -->|完成| Q6[销售出库单推进待拣货]
  A4 -->|完成| Q7[退货单推进上架或入库·售后回调结论]
  N1[NCR å¾…评审0] -->|评审| N2[已评审6]
  N2 -->|处置方式:退货1报废2返工3让步接收4降级使用5| N3[已处置7]
  N3 -->|关闭| N4[已关闭8]
  Q8[支撑数据:质检方案·检验指标·缺陷库·指标结果] --- A1
  Q9[化工报告增强:指标分组树+公式文本·平行测量批次·采样地点/证书编号/执行标准表头·复核人/审核人] --- Q8
  A1 & A2 & A3 & A4 --> P1[检验报告单打印·前端浏览器打印·含签字区]
  A1 & A2 & A3 & A4 --> P2[产品检验单Word导出·export-product-word·无技术指标列·判定结论/质检章留空手填·Word打印]
  A1 & A2 & A3 & A4 --> P1[检验报告单打印·前端浏览器打印·判定结论带出检验结果+质检专用章留空盖章+签字区·无平行测量时列头称测量值]
  A1 & A2 & A3 & A4 --> P2[产品检验单Word导出·export-product-word·无技术指标列·判定结论带出检验结果/质检章留空盖章·Word打印]
docs/project-business/mermaid/11-ai-business-flow.mmd
@@ -15,7 +15,7 @@
  J4 --> J5[质检员参考判定·AI不可用时提示人工判定]
  R1[工单BOM+库存可用量+已投料] --> R2[物料短缺预测·风险等级与缺口清单]
  R3[工序标准工时+设备每小时产量+传送倍率] --> R4[生产时长预测]
  R5[IPQC缺陷率+设备保养点检+物料可用率+关键工序] --> R6[生产风险预测]
  R5[设备保养点检+物料可用率+关键工序] --> R6[生产风险预测]
  R7[进度+剩余天数+任务完成状态+客户交期] --> R8[交付预测·能否按时+置信度+延误因素]
  KB1[知识库文档上传URL] --> KB2[POI/Tika提取文本] --> KB3[切片约800tokens→向量化→Milvus]
  KB3 --> KB4[分段检索接口 topK+相似度阈值]
docs/qc_chemical_report_integration.md
@@ -50,6 +50,7 @@
  "samplingTime": "2026-08-30T01:00:00",
  "batchQuantity": 30,
  "checkResult": 1,
  "checkResultText": "校验通过",
  "inspectorName": "新艳",
  "auditName": "海丽",
  "approveName": "王五",
@@ -83,8 +84,10 @@
| è¯ä¹¦ç¼–号 / é‡‡æ ·åœ°ç‚¹ / æ‰¹é‡ / é‡‡æ ·æ—¶é—´ / æ‰§è¡Œæ ‡å‡† | report åŒåå­—段 |
| åˆ†ç»„行(水分/酸含量…) | itemType=2 èŠ‚ç‚¹ï¼šåç§°è¡Œ + å…¬å¼æ–‡æœ¬(formulaText) |
| å‚数行/结果行 | åˆ†ç»„ children å½•入项,按 sortOrder æŽ’序 |
| å¹³è¡Œæµ‹é‡ä¸¤åˆ— | measureNos æ•°ç»„驱动列头,每 measureNo ä¸€åˆ— |
| å¹³è¡Œæµ‹é‡åˆ—头 | measureNos æ•°ç»„驱动;仅一列时列头显示「测量值」,多列时显示「测量1」「测量2」… |
| å¤–观等定性项 | resultType=文本/字典 çš„录入项直接显示 value |
| åˆ¤å®šç»“论 | `checkResultText`(后端按 qcType å¯¹åº”字典解析) |
| è´¨æ£€ä¸“用章 | æ— æ•°æ®æ¥æºï¼Œç•™å‡ºç©ºç™½å•元格供打印后人工盖章 |
| åˆ†æžè€…/复核人/审核人 | inspectorName / auditName / approveName |
## ä¸šåŠ¡è§„åˆ™
@@ -101,6 +104,7 @@
- æ‰“印为浏览器 `window.print()`,建议在 Chrome ä¸‹é¢„览;隐藏打印外其他元素由组件内 `@media print` æŽ§åˆ¶ã€‚
- `/report` çš„ `qcType`:1=IQC、2=IPQC、3=OQC、4=RQC,与 `MesQcTypeEnum` ä¸€è‡´ã€‚
- åˆ¤å®šç»“论文案由后端按 `qcType` é€‰å­—典解析:OQC èµ° `mes_oqc_check_result`(1型/2型/废品),IQC/IPQC/RQC èµ° `mes_qc_check_result`(校验通过/校验不通过)。未判定(`checkResult` ä¸ºç©ºï¼‰æ—¶æ‰“印为「-」,前端可回退按 `checkResult` è‡ªè¡Œå–字典标签。
- å¤æ ¸äºº/审核人仅作为记录与打印签字区,不引入审批流转。
- æ–‡ä»¶é™„件类指标(resultType=文件)沿用原机制,未在本次范围。
- å›žå½’点:既有扁平指标的老检验单(录入/完成/Excel å¯¼å‡ºï¼‰è¡Œä¸ºä¸å˜ï¼›`measure_no` é»˜è®¤ 1 å¯¹è€æ•°æ®é€æ˜Žã€‚
docs/sql/config_export_all_20260904.sql
@@ -988,6 +988,7 @@
('2046', 'MES ç»´ä¿®å·¥å•状态', 'mes_dv_repair_status', '0', 'MES è®¾å¤‡ç®¡ç† - ç»´ä¿®å·¥å•状态(10=待维修,20=维修中,30=已完成,40=已验收)', '1', '2026-02-20 10:56:24', '1', '2026-02-20 10:56:24', X'00', NULL),
('2047', 'MES ç»´ä¿®ç»“æžœ', 'mes_dv_repair_result', '0', 'MES è®¾å¤‡ç®¡ç† - ç»´ä¿®ç»“果(1=修复成功,2=报废)', '1', '2026-02-20 10:56:24', '1', '2026-02-20 10:56:24', X'00', NULL),
('2049', 'MES æ£€æµ‹ç»“æžœ', 'mes_qc_check_result', '0', '来料检验的最终结果判定', '1', '2026-02-20 11:23:35', '1', '2026-02-20 11:23:35', X'00', NULL),
('1065400', 'MES å‡ºè´§æ£€éªŒæ£€æµ‹ç»“æžœ', 'mes_oqc_check_result', '0', '出货检验单(OQC)判定结果:1型/2型为可发货等级,废品不放行', '1', '2026-09-22 00:00:00', '1', '2026-09-22 00:00:00', X'00', NULL),
('2050', 'MES æ¥æºå•据类型', 'mes_qc_source_doc_type', '0', 'IQC æ¥æ–™æ£€éªŒçš„æ¥æºå•据类型', '1', '2026-02-20 11:23:35', '1', '2026-02-20 11:23:35', X'00', NULL),
('2051', 'MES å®‰ç¯å¤„置状态', 'mes_pro_andon_status', '0', 'MES ç”Ÿäº§ç®¡ç† - å®‰ç¯å¤„置状态(0=未处置,1=已处置)', '1', '2026-02-21 00:08:38', '1', '2026-02-21 00:08:38', X'00', NULL),
('2052', 'MES å®‰ç¯çº§åˆ«', 'mes_pro_andon_level', '0', 'MES ç”Ÿäº§ç®¡ç† - å®‰ç¯çº§åˆ«ï¼ˆ1=一级,2=二级,3=三级)', '1', '2026-02-21 00:08:38', '1', '2026-02-21 00:08:38', X'00', NULL),
@@ -1822,6 +1823,9 @@
('3158', '2', '报废', '2', 'mes_dv_repair_result', '0', 'danger', '', '', '1', '2026-02-20 10:56:24', '1', '2026-02-20 10:56:24', X'00'),
('3161', '1', '校验通过', '1', 'mes_qc_check_result', '0', 'success', '', '', '1', '2026-02-20 11:23:35', '1', '2026-02-20 16:15:54', X'00'),
('3162', '2', '校验不通过', '2', 'mes_qc_check_result', '0', 'danger', '', '', '1', '2026-02-20 11:23:35', '1', '2026-02-20 16:15:52', X'00'),
('1065401', '1', '1型', '1', 'mes_oqc_check_result', '0', 'success', '', '', '1', '2026-09-22 00:00:00', '1', '2026-09-22 00:00:00', X'00'),
('1065402', '2', '2型', '2', 'mes_oqc_check_result', '0', 'primary', '', '', '1', '2026-09-22 00:00:00', '1', '2026-09-22 00:00:00', X'00'),
('1065403', '3', '废品', '3', 'mes_oqc_check_result', '0', 'danger', '', '', '1', '2026-09-22 00:00:00', '1', '2026-09-22 00:00:00', X'00'),
('3166', '0', '未处置', '0', 'mes_pro_andon_status', '0', 'danger', '', '', '1', '2026-02-21 00:08:38', '1', '2026-02-21 00:08:38', X'00'),
('3167', '1', '已处置', '1', 'mes_pro_andon_status', '0', 'success', '', '', '1', '2026-02-21 00:08:38', '1', '2026-02-21 00:08:38', X'00'),
('3168', '1', '一级', '1', 'mes_pro_andon_level', '0', 'danger', '', '', '1', '2026-02-21 00:08:38', '1', '2026-02-21 00:08:38', X'00'),
@@ -7995,10 +7999,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/controller/admin/qc/indicatorresult/MesQcIndicatorResultController.java
@@ -143,7 +143,7 @@
    }
    @GetMapping("/export-product-word")
    @Operation(summary = "导出产品检验单 Word(纸质检验单版式:无技术指标列,判定结论/质检专用章留空手填,打印用 Word æ‰“开)")
    @Operation(summary = "导出产品检验单 Word(纸质检验单版式:无技术指标列,判定结论自动带出检验结果、质检专用章留空盖章,打印用 Word æ‰“开)")
    @Parameters({
            @Parameter(name = "qcId", description = "质检单ID", required = true, example = "1"),
            @Parameter(name = "qcType", description = "质检类型", required = true, example = "1"),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicatorresult/vo/MesQcQcReportRespVO.java
@@ -49,9 +49,12 @@
    @Schema(description = "批量(来料=接收数量,过程/退货=检测数量,出货=出货数量)", example = "30")
    private BigDecimal batchQuantity;
    @Schema(description = "检测结果(字典 mes_qc_check_result)", example = "1")
    @Schema(description = "检测结果(IQC/IPQC/RQC=字典 mes_qc_check_result;OQC=字典 mes_oqc_check_result)", example = "1")
    private Integer checkResult;
    @Schema(description = "检测结果文案(按质检类型对应字典解析,供判定结论展示;未判定时为空)", example = "校验通过")
    private String checkResultText;
    @Schema(description = "检验日期", example = "2026-08-30T01:00:00")
    private LocalDateTime inspectDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java
@@ -15,9 +15,6 @@
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;
@@ -28,9 +25,6 @@
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;
@@ -69,12 +63,6 @@
    @Resource
    private MesQcIpqcService ipqcService;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesMdWorkstationService workstationService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesMdItemService itemService;
    @Resource
@@ -157,15 +145,6 @@
        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(
                convertSet(list, MesQcIpqcDO::getItemId));
@@ -182,12 +161,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(),
@@ -289,8 +262,7 @@
            vo.setDocName(ipqc.getName());
            MesMdItemDO 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/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 = "BC20250101")
    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,34 +50,9 @@
    @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 = "批号", example = "BC20250101")
    @ExcelProperty("批号")
    private String batchCode;
    // ========== ç‰©æ–™ ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java
@@ -42,25 +42,14 @@
    @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 = "批号", requiredMode = Schema.RequiredMode.REQUIRED, example = "BC20250101")
    @NotEmpty(message = "批号不能为空")
    private String batchCode;
    // ========== ç‰©æ–™ ==========
    @Schema(description = "产品物料 ID", example = "50")
    @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/oqc/MesQcOqcController.java
@@ -285,7 +285,7 @@
            return "";
        }
        return DictFrameworkUtils.parseDictDataLabel(
                DictTypeConstants.MES_QC_CHECK_RESULT, String.valueOf(checkResult));
                DictTypeConstants.MES_OQC_CHECK_RESULT, String.valueOf(checkResult));
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -127,7 +127,7 @@
    @Schema(description = "检测结果", example = "1")
    @ExcelProperty(value = "检测结果", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_QC_CHECK_RESULT)
    @DictFormat(DictTypeConstants.MES_OQC_CHECK_RESULT)
    private Integer checkResult;
    // ========== æ£€éªŒé¡¹æŒ‡æ ‡ ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java
@@ -54,8 +54,7 @@
    // ========== æ•°é‡ ==========
    @Schema(description = "本次出货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "本次出货数量不能为空")
    @Schema(description = "本次出货数量", example = "100")
    @DecimalMin(value = "0", message = "本次出货数量不能小于 0")
    private BigDecimal outQuantity;
@@ -76,11 +75,11 @@
    // ========== æ£€éªŒ ==========
    @Schema(description = "检测结果", example = "1")
    @Schema(description = "检测结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结果不能为空")
    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/vo/MesQcPendingInspectRespVO.java
@@ -41,6 +41,9 @@
    @Schema(description = "单位名称", example = "个")
    private String unitName;
    @Schema(description = "批号(当前仅 IPQC æ¥æºæä¾›ï¼‰", example = "BC20250101")
    private String batchCode;
    // ========== æ•°é‡ ==========
    @Schema(description = "待检数量", example = "100.00")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java
@@ -2,9 +2,7 @@
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;
@@ -86,32 +84,10 @@
     */
    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;
    // ========== ç‰©æ–™ ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -156,8 +156,8 @@
    /**
     * æ£€æµ‹ç»“æžœ
     *
     * å­—å…¸ {@link DictTypeConstants#MES_QC_CHECK_RESULT}
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum}
     * å­—å…¸ {@link DictTypeConstants#MES_OQC_CHECK_RESULT}
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcOqcCheckResultEnum}
     */
    private Integer checkResult;
    /**
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/enums/DictTypeConstants.java
@@ -78,7 +78,8 @@
    String MES_IPQC_TYPE = "mes_ipqc_type"; // MES IPQC æ£€éªŒç±»åž‹
    String MES_RQC_TYPE = "mes_rqc_type"; // MES é€€è´§æ£€éªŒç±»åž‹
    String MES_ORDER_STATUS = "mes_order_status"; // MES å•据状态(IQC/IPQC/OQC/RQC é€šç”¨ï¼‰
    String MES_QC_CHECK_RESULT = "mes_qc_check_result"; // MES æ£€æµ‹ç»“æžœ
    String MES_QC_CHECK_RESULT = "mes_qc_check_result"; // MES æ£€æµ‹ç»“果(IQC/IPQC/RQC å…±ç”¨ï¼‰
    String MES_OQC_CHECK_RESULT = "mes_oqc_check_result"; // MES å‡ºè´§æ£€éªŒæ£€æµ‹ç»“果(OQC ä¸“用)
    String MES_QC_SOURCE_DOC_TYPE = "mes_qc_source_doc_type"; // MES æ¥æºå•据类型
    String MES_NCR_STATUS = "mes_ncr_status"; // MES NCR çŠ¶æ€
    String MES_NCR_DISPOSITION = "mes_ncr_disposition"; // MES NCR å¤„置方式
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcOqcCheckResultEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.enums.qc;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES å‡ºè´§æ£€éªŒå•检测结果枚举(OQC ä¸“用)
 *
 * <p>与 IQC/IPQC/RQC å…±ç”¨çš„ {@link MesQcCheckResultEnum} ä¸åŒï¼Œå‡ºè´§æ£€éªŒæŒ‰äº§å“ç­‰çº§åˆ¤å®šï¼Œ
 * å­—å…¸ {@link cn.iocoder.yudao.module.mes.enums.DictTypeConstants#MES_OQC_CHECK_RESULT}</p>
 */
@Getter
@AllArgsConstructor
public enum MesQcOqcCheckResultEnum implements ArrayValuable<Integer> {
    TYPE_1(1, "1型"),
    TYPE_2(2, "2型"),
    SCRAP(3, "废品");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcOqcCheckResultEnum::getType).toArray(Integer[]::new);
    /**
     * ç»“果值
     */
    private final Integer type;
    /**
     * ç»“果名
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å¯ä»¥æ”¾è¡Œå‘è´§
     *
     * <p>1 åž‹ã€2 åž‹å‡ä¸ºå¯å‘货等级,仅废品不放行</p>
     *
     * @param type æ£€æµ‹ç»“æžœ
     * @return true å¯ä»¥æ”¾è¡Œ
     */
    public static boolean canShip(Integer type) {
        return type != null && !SCRAP.getType().equals(type);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
@@ -19,7 +19,6 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkstationMachineMapper;
@@ -28,7 +27,6 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.service.pro.mps.MesProMpsService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
@@ -101,14 +99,13 @@
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的风险评估:
            ## åˆ†æžç»´åº¦
            1. è´¨é‡é£Žé™©ï¼šè¿‡ç¨‹æ£€éªŒ(IPQC)的缺陷率数据(致命/严重/轻微缺陷率)
            2. è®¾å¤‡é£Žé™©ï¼šè®¾å¤‡çŠ¶æ€ã€ä¸Šæ¬¡ä¿å…»æ—¶é—´ã€ä¸Šæ¬¡ç‚¹æ£€æ—¶é—´ã€ç”Ÿäº§æ•ˆçŽ‡
            3. ç‰©æ–™é£Žé™©ï¼šç‰©æ–™åº“存可用量与需求量的对比
            4. å·¥è‰ºç“¶é¢ˆï¼šå…³é”®å·¥åºçš„设备产能、任务排队情况
            1. è®¾å¤‡é£Žé™©ï¼šè®¾å¤‡çŠ¶æ€ã€ä¸Šæ¬¡ä¿å…»æ—¶é—´ã€ä¸Šæ¬¡ç‚¹æ£€æ—¶é—´ã€ç”Ÿäº§æ•ˆçŽ‡
            2. ç‰©æ–™é£Žé™©ï¼šç‰©æ–™åº“存可用量与需求量的对比
            3. å·¥è‰ºç“¶é¢ˆï¼šå…³é”®å·¥åºçš„设备产能、任务排队情况
            ## é£Žé™©ç­‰çº§å®šä¹‰
            - é«˜ï¼šå­˜åœ¨è‡´å‘½ç¼ºé™·çއ>5% æˆ– è®¾å¤‡æ•…éšœ æˆ– å…³é”®ç‰©æ–™çŸ­ç¼º>50% æˆ– å…³é”®å·¥åºä¸¥é‡ç§¯åŽ‹
            - ä¸­ï¼šå­˜åœ¨ä¸¥é‡ç¼ºé™·çއ>10% æˆ– è®¾å¤‡é•¿æœŸæœªä¿å…» æˆ– ç‰©æ–™ç´§å¼ 
            - é«˜ï¼šå­˜åœ¨è®¾å¤‡æ•…éšœ æˆ– å…³é”®ç‰©æ–™çŸ­ç¼º>50% æˆ– å…³é”®å·¥åºä¸¥é‡ç§¯åŽ‹
            - ä¸­ï¼šå­˜åœ¨è®¾å¤‡é•¿æœŸæœªä¿å…» æˆ– ç‰©æ–™ç´§å¼ 
            - ä½Žï¼šå„项指标正常
            ## è¾“出格式
@@ -118,7 +115,7 @@
            overallRiskLevel å–值:1=低风险 2=中风险 3=高风险
            riskLevel å–值:充足/ç´§å¼ /短缺
            severity å–值:高/中/低
            category å–值:质量/设备/物料/工艺瓶颈
            category å–值:设备/物料/工艺瓶颈
            """;
    private static final String SYSTEM_PROMPT_DELIVERY = """
@@ -159,8 +156,6 @@
    private MesProTaskIssueMapper taskIssueMapper;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
@@ -523,26 +518,6 @@
        data.put("计划数量", workOrder.getQuantity());
        data.put("已生产数量", workOrder.getQuantityProduced());
        fillItemInfo(data, workOrder.getProductId());
        // è´¨é‡é£Žé™© - IPQC
        List<MesQcIpqcDO> ipqcList = ipqcMapper.selectList(
                new LambdaQueryWrapper<MesQcIpqcDO>()
                        .eq(MesQcIpqcDO::getWorkOrderId, workOrderId));
        if (CollUtil.isNotEmpty(ipqcList)) {
            List<Map<String, Object>> qcData = ipqcList.stream().map(ipqc -> {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("检验单编号", ipqc.getCode());
                item.put("致命缺陷率", ipqc.getCriticalRate() != null ? ipqc.getCriticalRate() + "%" : "0%");
                item.put("严重缺陷率", ipqc.getMajorRate() != null ? ipqc.getMajorRate() + "%" : "0%");
                item.put("轻微缺陷率", ipqc.getMinorRate() != null ? ipqc.getMinorRate() + "%" : "0%");
                item.put("致命缺陷数", ipqc.getCriticalQuantity() != null ? ipqc.getCriticalQuantity() : 0);
                item.put("严重缺陷数", ipqc.getMajorQuantity() != null ? ipqc.getMajorQuantity() : 0);
                item.put("轻微缺陷数", ipqc.getMinorQuantity() != null ? ipqc.getMinorQuantity() : 0);
                item.put("检验结果", ipqc.getCheckResult());
                return item;
            }).collect(Collectors.toList());
            data.put("过程检验(IPQC)记录", qcData);
        }
        // è®¾å¤‡é£Žé™©
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
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/indicatorresult/MesQcProductReportWordBuilder.java
@@ -18,7 +18,7 @@
 * äº§å“æ£€éªŒå•(纸质"××产品质量检验单"版式)Word æž„建器
 *
 * ç‰ˆå¼ï¼šæ ‡é¢˜ï¼ˆå…¬å¸ + äº§å“æ£€éªŒå•)→ è¡¨å¤´ä¿¡æ¯ 5 è¡Œ â†’ æ£€éªŒé¡¹ç›®/检验结果 ä¸¤åˆ—矩阵 â†’
 * åˆ¤å®šç»“论/质检专用章(留空,打印后手填)→ å¤‡æ³¨ â†’ æ£€éªŒ/复核/批准签字行。
 * åˆ¤å®šç»“论(自动带出检验结果)/质检专用章(留空盖章)→ å¤‡æ³¨ â†’ æ£€éªŒ/复核/批准签字行。
 * ä¸å«æŠ€æœ¯æŒ‡æ ‡åˆ—;分组项不展示,录入项平铺;平行测量多值顿号合并。
 */
public class MesQcProductReportWordBuilder {
@@ -51,7 +51,7 @@
            writeTitle(doc, report);
            writeHeaderTable(doc, report);
            writeItemTable(doc, report);
            writeConclusionTable(doc);
            writeConclusionTable(doc, report);
            writeRemarkTable(doc, report);
            writeSignLine(doc);
            doc.write(out);
@@ -150,12 +150,14 @@
                .orElse(EMPTY);
    }
    // ========== åˆ¤å®šç»“论 / è´¨æ£€ä¸“用章(留空后填) ==========
    // ========== åˆ¤å®šç»“论 / è´¨æ£€ä¸“用章 ==========
    private static XWPFTable writeConclusionTable(XWPFDocument doc) {
    /** åˆ¤å®šç»“论自动带出检验结果文案;质检专用章留空,打印后人工盖章 */
    private static XWPFTable writeConclusionTable(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFTable table = createTable(doc);
        XWPFTableRow row = addRow(table, valueLeftSpan("判定结论:", 2), valueLeftSpan("质检专用章:", 2));
        // é¢„留手写/盖章高度
        String conclusion = notBlank(report.getCheckResultText()) ? report.getCheckResultText() : "";
        XWPFTableRow row = addRow(table, valueLeftSpan("判定结论:" + conclusion, 2), valueLeftSpan("质检专用章:", 2));
        // é¢„留盖章高度
        setRowHeight(row, 2550);
        return table;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcReportServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.qc.indicatorresult;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcQcReportRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
@@ -14,6 +15,7 @@
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.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
@@ -231,10 +233,23 @@
                report.setApproveName(userMap.get(report.getApproveUserId()).getNickname());
            }
        }
        report.setCheckResultText(resolveCheckResultText(qcType, report.getCheckResult()));
        return report;
    }
    /**
     * æ£€æµ‹ç»“果文案:OQC æŒ‰äº§å“ç­‰çº§åˆ¤å®šï¼ˆmes_oqc_check_result),其余走通用检测结果字典(mes_qc_check_result)
     */
    private String resolveCheckResultText(Integer qcType, Integer checkResult) {
        if (checkResult == null) {
            return null;
        }
        String dictType = Objects.equals(qcType, MesQcTypeEnum.OQC.getType())
                ? DictTypeConstants.MES_OQC_CHECK_RESULT : DictTypeConstants.MES_QC_CHECK_RESULT;
        return DictFrameworkUtils.parseDictDataLabel(dictType, String.valueOf(checkResult));
    }
    /**
     * ç»„装报告检验项树;顺带收集全局 measureNos åˆ—头
     */
    private List<MesQcQcReportRespVO.ReportItem> buildReportItems(
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -10,10 +10,6 @@
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;
@@ -23,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;
@@ -39,6 +28,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -65,14 +55,6 @@
@Slf4j
public class MesQcIpqcServiceImpl implements MesQcIpqcService {
    /**
     * IPQC ä¿å­˜æ•°æ®æ ¡éªŒä¸Šä¸‹æ–‡
     *
     * @param workstation å·¥ä½
     * @param task        ç”Ÿäº§ä»»åŠ¡ï¼ˆå¯é€‰ï¼ŒæŒ‡å®š taskId æ—¶éžç©ºï¼‰
     */
    private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task) {}
    @Resource
    private MesQcIpqcMapper ipqcMapper;
@@ -85,25 +67,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;
@@ -121,19 +85,18 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @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 ç±»åž‹æŸ¥æ‰¾å¯ç”¨è´¨æ£€æ–¹æ¡ˆï¼ˆæ”¯æŒå‰ç«¯æ˜¾å¼æŒ‡å®šï¼‰
        // å·¥å•产品/任务物料是 MES ç‰©æ–™ï¼Œç”±æ¨¡æ¿æœåŠ¡å†…éƒ¨è½¬æ¢ä¸º MDM ç‰©æ–™åŽåŒ¹é…ï¼Œæ•…传工单/任务 ID
        validateIpqcSaveData(createReqVO);
        // 1.2 æ ¹æ®äº§å“ç‰©æ–™ + IPQC ç±»åž‹æŸ¥æ‰¾å¯ç”¨è´¨æ£€æ–¹æ¡ˆï¼ˆæ”¯æŒå‰ç«¯æ˜¾å¼æŒ‡å®šï¼‰
        // ç‰©æ–™ ID æ¥è‡ª MDM,模板服务直接按 MDM ç‰©æ–™åŒ¹é…
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                null, MesQcTypeEnum.IPQC.getType(), createReqVO.getWorkOrderId(), createReqVO.getTaskId());
                createReqVO.getItemId(), MesQcTypeEnum.IPQC.getType(), null, null);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -147,14 +110,12 @@
            }
        }
        Long templateId = templateItem.getTemplateId();
        // 1.5 èŽ·å–æ¥æºå•æ®ç¼–å·
        // 1.3 èŽ·å–æ¥æºå•æ®ç¼–å·
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
        // 2. æ’入主表
        MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class)
                .setItemId(itemId)
                .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId()))
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
@@ -177,60 +138,23 @@
        // 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());
        // æ ¡éªŒç‰©æ–™ã€æ£€æµ‹äººå‘˜å­˜åœ¨
        // æ³¨ï¼šç‰©æ–™ 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
@@ -291,21 +215,8 @@
        // 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());
            log.warn("[writeBackSourceDoc] IPQC({}) æœªè®¾ç½®æ¥æºæŠ¥å·¥ï¼Œè·³è¿‡å›žå†™", ipqc.getId());
            return;
        }
@@ -389,33 +300,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/wm/productsales/MesWmProductSalesLineServiceImpl.java
@@ -10,7 +10,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcOqcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
@@ -232,8 +232,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) {
        // 1. æ›´æ–°è¡Œçš„ oqcId + qualityStatus
        Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.QUALIFIED.getType())
        // 1. æ›´æ–°è¡Œçš„ oqcId + qualityStatus(1型/2型放行,废品判不合格)
        Integer qualityStatus = MesQcOqcCheckResultEnum.canShip(checkResult)
                ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus();
        productSalesLineMapper.updateById(new MesWmProductSalesLineDO()
                .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus));
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
@@ -22,6 +22,7 @@
                n.arrival_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IQC.type} AS qcType,
                l.item_id AS itemId,
                NULL AS batchCode,
                l.arrival_quantity AS quantity,
                n.vendor_id AS vendorId,
                NULL AS workOrderId,
@@ -47,6 +48,7 @@
                r.receipt_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IQC.type} AS qcType,
                rl.item_id AS itemId,
                NULL AS batchCode,
                rl.quantity AS quantity,
                r.vendor_id AS vendorId,
                NULL AS workOrderId,
@@ -72,6 +74,7 @@
                pf.feedback_time AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IPQC.type} AS qcType,
                ppl.item_id AS itemId,
                ppl.batch_code AS batchCode,
                ppl.quantity AS quantity,
                NULL AS vendorId,
                pf.work_order_id AS workOrderId,
@@ -97,6 +100,7 @@
                s.sales_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@OQC.type} AS qcType,
                sl.item_id AS itemId,
                NULL AS batchCode,
                sl.quantity AS quantity,
                NULL AS vendorId,
                NULL AS workOrderId,
@@ -122,6 +126,7 @@
                ri.return_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
                ril.item_id AS itemId,
                NULL AS batchCode,
                ril.quantity AS quantity,
                NULL AS vendorId,
                ri.work_order_id AS workOrderId,
@@ -148,6 +153,7 @@
                r.return_date AS recordTime,
                ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
                rl.item_id AS itemId,
                NULL AS batchCode,
                rl.quantity AS quantity,
                NULL AS vendorId,
                NULL AS workOrderId,
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcProductReportWordBuilderTest.java
@@ -54,9 +54,9 @@
            assertEquals("10.023、10.115", itemTable.getRow(2).getCell(1).getText().trim());
            assertEquals("/", itemTable.getRow(3).getCell(1).getText().trim());
            // åˆ¤å®šç»“论/质检专用章留空
            // åˆ¤å®šç»“论带出检验结果;质检专用章留空待盖章
            assertTrue(itemTable.getRow(2).getCell(0).getText().contains("试样质量 m /g"));
            assertEquals("判定结论:", tables.get(2).getRow(0).getCell(0).getText().trim());
            assertEquals("判定结论:1型", tables.get(2).getRow(0).getCell(0).getText().trim());
            assertEquals("质检专用章:", tables.get(2).getRow(0).getCell(1).getText().trim());
            // å¤‡æ³¨é»˜è®¤æ–‡æ¡ˆ
@@ -80,7 +80,8 @@
                .setSamplingTime(LocalDateTime.of(2026, 1, 6, 9, 0))
                .setInspectDate(LocalDateTime.of(2026, 1, 6, 15, 0))
                .setDocCreateTime(LocalDateTime.of(2026, 1, 6, 8, 0))
                .setInspectorName("李博").setAuditName("周亚洁").setApproveName("包翠萍");
                .setInspectorName("李博").setAuditName("周亚洁").setApproveName("包翠萍")
                .setCheckResult(1).setCheckResultText("1型");
        MesQcQcReportRespVO.ReportItem appearance = new MesQcQcReportRespVO.ReportItem();
        appearance.setIndicatorName("外观").setItemType(1).setSortOrder(10);
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java
@@ -3,30 +3,23 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
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;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
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.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.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
@@ -34,11 +27,16 @@
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.util.Collections;
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;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
/**
@@ -58,13 +56,9 @@
    @MockitoBean
    private MesQcTemplateItemService templateItemService;
    @MockitoBean
    private MesQcTemplateService templateService;
    @MockitoBean
    private MesQcIpqcLineService ipqcLineService;
    @MockitoBean
    private MesProWorkOrderService workOrderService;
    @MockitoBean
    private MesMdWorkstationService workstationService;
    @MockitoBean
    private MesMdItemService itemService;
    @MockitoBean
    private MesQcDefectRecordService defectRecordService;
    @MockitoBean
@@ -72,15 +66,15 @@
    @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;
    @MockitoBean
    private MdmItemApi mdmItemApi;
    // ==================== finishIpqc å›žå†™æ¥æºå•据 ====================
    @Test
    public void testFinishIpqc_writeBack_feedback() {
@@ -89,9 +83,6 @@
        Long sourceLineId = randomLongId();
        BigDecimal qualifiedQty = BigDecimal.valueOf(80);
        BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
        BigDecimal laborScrapQty = BigDecimal.valueOf(5);
        BigDecimal materialScrapQty = BigDecimal.valueOf(10);
        BigDecimal otherScrapQty = BigDecimal.valueOf(5);
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
@@ -101,9 +92,9 @@
            o.setSourceLineId(sourceLineId);
            o.setQualifiedQuantity(qualifiedQty);
            o.setUnqualifiedQuantity(unqualifiedQty);
            o.setLaborScrapQuantity(laborScrapQty);
            o.setMaterialScrapQuantity(materialScrapQty);
            o.setOtherScrapQuantity(otherScrapQty);
            o.setLaborScrapQuantity(BigDecimal.valueOf(5));
            o.setMaterialScrapQuantity(BigDecimal.valueOf(10));
            o.setOtherScrapQuantity(BigDecimal.valueOf(5));
        });
        ipqcMapper.insert(ipqc);
@@ -124,6 +115,8 @@
                any(BigDecimal.class),
                any(BigDecimal.class),
                any(BigDecimal.class));
        // æ–­è¨€ 3:不合格数量 > 0,自动创建 NCR
        verify(ncrService).createNcr(any());
    }
    @Test
@@ -161,13 +154,14 @@
    @Test
    public void testFinishIpqc_writeBack_noSourceDoc() {
        // å‡†å¤‡æ•°æ®ï¼šsourceDocType ä¸ºç©ºï¼Œä¸éœ€è¦å›žå†™
        // å‡†å¤‡æ•°æ®ï¼šæ— æ¥æºå•据(手工创建的 IPQC),直接完成,不回写
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(null);
            o.setSourceDocId(null);
            o.setSourceLineId(null);
            o.setUnqualifiedQuantity(BigDecimal.ZERO);
        });
        ipqcMapper.insert(ipqc);
@@ -181,38 +175,6 @@
        // æ–­è¨€ 2:不应该调用 feedbackService
        verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
                anyLong(), anyLong(), any(), any(), any(), any(), any());
    }
    @Test
    public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
        // å‡†å¤‡æ•°æ®ï¼š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);
        });
        ipqcMapper.insert(ipqc);
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    @Test
    public void testFinishIpqc_writeBack_unknownSourceDocType() {
        // å‡†å¤‡æ•°æ®ï¼šsourceDocType æ— æ³•识别
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(99999); // æ— æ³•识别的类型
            o.setSourceDocId(randomLongId());
            o.setSourceLineId(null);
        });
        ipqcMapper.insert(ipqc);
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    @Test
@@ -242,162 +204,61 @@
        assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    // ==================== processId æŽ¨å¯¼è¡Œä¸ºæµ‹è¯• ====================
    // ==================== createIpqc ç‰©æ–™ / æ‰¹å· / æ¨¡æ¿åŒ¹é… ====================
    /**
     * å·¥ä½æœ‰å·¥åº + è¯¥å·¥åºåœ¨äº§å“å·¥è‰ºè·¯çº¿ä¸­ â†’ processId åº”该被设置为工位工序
     * æ‰‹å·¥åˆ›å»ºï¼šç‰©æ–™æ¥è‡ª MDM,模板按 MDM ç‰©æ–™ ID åŒ¹é…ï¼›æ‰¹å·ã€ç‰©æ–™å‡è½åº“
     */
    @Test
    public void testCreateIpqc_processId_workstationHasProcess() {
    public void testCreateIpqc_success() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long itemId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-001");
        reqVO.setItemId(itemId);
        reqVO.setBatchCode("BC20250101");
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-001");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        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());
        // mock:按 MDM ç‰©æ–™ ID åŒ¹é…åˆ°è´¨æ£€æ–¹æ¡ˆ
        mockTemplate(templateId);
        // è°ƒç”¨
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // æ–­è¨€ï¼šprocessId åº”该是工位工序
        // æ–­è¨€ 1:批号、物料、方案均落库
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertEquals(processId, ipqc.getProcessId());
        assertNotNull(ipqc);
        assertEquals("BC20250101", ipqc.getBatchCode());
        assertEquals(itemId, ipqc.getItemId());
        assertEquals(templateId, ipqc.getTemplateId());
        assertEquals(MesQcStatusEnum.DRAFT.getStatus(), ipqc.getStatus());
        assertNull(ipqc.getSourceDocCode());
        // æ–­è¨€ 2:模板按 MDM ç‰©æ–™ ID åŒ¹é…ï¼Œä¸å†ä¾èµ–工单/任务
        verify(templateItemService).getTemplateItemList(
                eq(itemId), eq(MesQcTypeEnum.IPQC.getType()), isNull(), isNull());
        // æ–­è¨€ 3:物料存在性校验走 MDM
        verify(mdmItemApi).validateItemExists(itemId);
    }
    /**
     * å·¥ä½æ²¡æœ‰å·¥åºï¼ˆprocessId=null)→ processId åº”该留 null(不回退到任务工序)
     * ç‰©æ–™æœªåŒ¹é…åˆ°ä»»ä½•质检方案 â†’ åˆ›å»ºå¤±è´¥
     */
    @Test
    public void testCreateIpqc_processId_workstationNoProcess_shouldBeNull() {
    public void testCreateIpqc_noTemplate() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long taskId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-002");
        reqVO.setItemId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-002");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setTaskId(taskId); // æ•…意传入 taskId,验证不会回退
        reqVO.setInspectorUserId(randomLongId());
        // mock:无匹配方案
        when(templateItemService.getTemplateItemList(any(), any(), any(), any()))
                .thenReturn(Collections.emptyList());
        // 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);
        // è°ƒç”¨
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // æ–­è¨€ï¼šprocessId åº”该是 null,即使传入了 taskId ä¹Ÿä¸åº”该回退
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId(),
                "工位无工序时 processId åº”为 null,不应回退到任务工序");
        // æ–­è¨€ï¼šä¸åº”该读取任务的 processId
        verify(routeProductService, never()).getRouteProductByItemId(any());
        // è°ƒç”¨ï¼Œæ–­è¨€å¼‚常
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode());
    }
    /**
     * å·¥ä½æœ‰å·¥åºä½†è¯¥å·¥åºä¸åœ¨äº§å“å·¥è‰ºè·¯çº¿ä¸­ â†’ processId åº”该留 null
     */
    @Test
    public void testCreateIpqc_processId_processNotInRoute() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-003");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        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(null);
        // è°ƒç”¨
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // æ–­è¨€ï¼šprocessId åº”该是 null
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId());
    }
    // ==================== sourceLineId æ ¡éªŒæµ‹è¯• ====================
    // ==================== createIpqc sourceLineId æ ¡éªŒ ====================
    /**
     * sourceDocType=PRO_FEEDBACK æ—¶ sourceLineId ä¸ºç©º â†’ åˆ›å»ºå¤±è´¥
@@ -405,34 +266,15 @@
    @Test
    public void testCreateIpqc_sourceLineId_required() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = 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());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-001");
        reqVO.setItemId(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);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // è°ƒç”¨ï¼Œæ–­è¨€å¼‚常
@@ -446,35 +288,16 @@
    @Test
    public void testCreateIpqc_sourceLineId_notBelong() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = 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());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-002");
        reqVO.setItemId(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);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在但 feedbackId ä¸åŒ¹é…
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -494,35 +317,16 @@
    @Test
    public void testCreateIpqc_sourceLineId_notPending() {
        // å‡†å¤‡å‚æ•°
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = 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());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-003");
        reqVO.setItemId(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);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在且归属正确,但不是 PENDING çŠ¶æ€
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -537,40 +341,24 @@
    }
    /**
     * sourceLineId æ ¡éªŒå…¨éƒ¨é€šè¿‡ â†’ åˆ›å»ºæˆåŠŸ
     * sourceLineId æ ¡éªŒå…¨éƒ¨é€šè¿‡ â†’ åˆ›å»ºæˆåŠŸï¼Œæ¥æºå•æ®ç¼–å·å›žå†™
     */
    @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());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-004");
        reqVO.setItemId(itemId);
        reqVO.setBatchCode("BC20250101");
        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);
        mockTemplate(templateId);
        MesProFeedbackDO feedback = new MesProFeedbackDO();
        feedback.setId(feedbackId);
        feedback.setCode("FB-001");
@@ -585,12 +373,65 @@
        // è°ƒç”¨
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // æ–­è¨€ï¼šåˆ›å»ºæˆåŠŸï¼ŒsourceDocCode å›žå†™
        // æ–­è¨€ï¼šåˆ›å»ºæˆåŠŸï¼Œæ¥æºå•æ®å­—æ®µè½åº“
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNotNull(ipqc);
        assertEquals(feedbackId, ipqc.getSourceDocId());
        assertEquals(sourceLineId, ipqc.getSourceLineId());
        assertEquals("FB-001", ipqc.getSourceDocCode());
        assertEquals("BC20250101", ipqc.getBatchCode());
        assertEquals(itemId, ipqc.getItemId());
    }
    /**
     * æœªçŸ¥æ¥æºå•据类型 â†’ åˆ›å»ºå¤±è´¥
     */
    @Test
    public void testCreateIpqc_sourceDocTypeUnknown() {
        // å‡†å¤‡å‚æ•°
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-005");
        reqVO.setItemId(randomLongId());
        reqVO.setSourceDocType(99999); // æ— æ³•识别的类型
        reqVO.setSourceDocId(randomLongId());
        // mock
        mockTemplate(randomLongId());
        // è°ƒç”¨ï¼Œæ–­è¨€å¼‚常
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN.getCode(), ex.getCode());
    }
    // ==================== è¾…助方法 ====================
    /**
     * æž„造一个满足必填项的过程检验单请求(不含来源单据)
     */
    private MesQcIpqcSaveReqVO buildReqVO(String code) {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode(code);
        reqVO.setName(code + "-名称");
        reqVO.setType(1);
        reqVO.setBatchCode("BC-" + code);
        reqVO.setCheckQuantity(BigDecimal.valueOf(100));
        reqVO.setQualifiedQuantity(BigDecimal.valueOf(95));
        reqVO.setUnqualifiedQuantity(BigDecimal.valueOf(5));
        reqVO.setLaborScrapQuantity(BigDecimal.ZERO);
        reqVO.setMaterialScrapQuantity(BigDecimal.ZERO);
        reqVO.setOtherScrapQuantity(BigDecimal.ZERO);
        reqVO.setInspectorUserId(randomLongId());
        return reqVO;
    }
    /**
     * mock:按 MDM ç‰©æ–™ ID + IPQC ç±»åž‹åŒ¹é…åˆ°æŒ‡å®šè´¨æ£€æ–¹æ¡ˆ
     */
    private void mockTemplate(Long templateId) {
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        List<MesQcTemplateItemDO> templateItems = Collections.singletonList(templateItem);
        when(templateItemService.getTemplateItemList(any(), eq(MesQcTypeEnum.IPQC.getType()), isNull(), isNull()))
                .thenReturn(templateItems);
    }
}
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,
@@ -263,6 +260,11 @@
    "inspect_date" timestamp DEFAULT NULL,
    "inspector_user_id" bigint DEFAULT NULL,
    "status" tinyint DEFAULT NULL,
    "sampling_location" varchar(255) DEFAULT NULL,
    "certificate_no" varchar(100) DEFAULT NULL,
    "execute_standard" varchar(100) DEFAULT NULL,
    "audit_user_id" bigint DEFAULT NULL,
    "approve_user_id" bigint DEFAULT NULL,
    "remark" varchar(500) DEFAULT NULL,
    "creator" varchar(64) DEFAULT '',
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,