已修改43个文件
已添加16个文件
1546 ■■■■■ 文件已修改
.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 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/index.html 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/05-quality-flow.mmd 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/11-ai-business-flow.mmd 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType1_qcId9001.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType1_qcId9002.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType1_qcId9003.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType2_qcId9011.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType2_qcId9012.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType2_qcId9013.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType3_qcId9021.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType3_qcId9022.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType3_qcId9023.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType4_qcId9031.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType4_qcId9032.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc-report-samples/qcType4_qcId9033.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_chemical_report_integration.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_product_report_word_integration.md 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seed_qc_mock_data.sql 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260904.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | 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 60 ●●●●● 补丁 | 查看 | 原始文档 | 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/iqc/MesQcIqcController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 30 ●●●●● 补丁 | 查看 | 原始文档 | 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 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/vo/MesQcPendingInspectRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/MesQcRqcController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | 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/dataobject/qc/template/MesQcTemplateItemDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/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 34 ●●●●● 补丁 | 查看 | 原始文档 | 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 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/pendinginspect/MesQcPendingInspectServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java 45 ●●●●● 补丁 | 查看 | 原始文档 | 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 29 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -7,7 +7,7 @@
    {"name": "联系人", "module": "销售管理", "table": "crm_contact", "doc": "客户下联系人,与商机多对多", "upstream": ["客户"], "downstream": ["商机"], "keywords": "联系人 contact"},
    {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB é…ç½®å¼é˜¶æ®µæŽ¨è¿›ï¼Œç»“束态: èµ¢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business èµ¢å• è¾“单 é˜¶æ®µ"},
    {"name": "报价单", "module": "销售管理", "table": "crm_sale_quotation", "doc": "QT编号,选 MDM ç‰©æ–™æŠ¥ä»·ï¼ŒAI OCR å½•入,草稿→已转合同", "upstream": ["商机"], "downstream": ["合同"], "keywords": "报价 quotation QT"},
    {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号,BPM å®¡æ‰¹(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract å®¡æ‰¹"},
    {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号(新增/编辑均可自定义,留空则新增自动生成、编辑保持原编号),BPM å®¡æ‰¹(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract å®¡æ‰¹"},
    {"name": "发票", "module": "销售管理", "table": "crm_invoice", "doc": "KP编号,BPM å®¡æ‰¹ï¼Œç´¯è®¡å¼€ç¥¨â‰¤åˆåŒé¢ï¼Œé™„ä»¶å¿…ä¼ ", "upstream": ["合同"], "downstream": ["回款"], "keywords": "开票 å‘票 invoice"},
    {"name": "回款", "module": "销售管理", "table": "crm_receivable", "doc": "HK编号,创建即审批中(手动审核),强制先开票,金额双校验", "upstream": ["合同", "发票"], "downstream": ["收款单(关联)"], "keywords": "回款 æ”¶æ¬¾è®¡åˆ’ receivable"},
    {"name": "销售目标", "module": "销售管理", "table": "crm_sales_target", "doc": "å¹´/å­£/月×个人/部门/产品维度的合同回款等目标", "upstream": [], "downstream": ["业绩统计"], "keywords": "目标 target ä¸šç»©"},
@@ -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返工)"],
@@ -442,7 +442,7 @@
    {"name": "联系人", "module": "销售管理", "table": "crm_contact", "doc": "客户下联系人,与商机多对多", "upstream": ["客户"], "downstream": ["商机"], "keywords": "联系人 contact"},
    {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB é…ç½®å¼é˜¶æ®µæŽ¨è¿›ï¼Œç»“束态: èµ¢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business èµ¢å• è¾“单 é˜¶æ®µ"},
    {"name": "报价单", "module": "销售管理", "table": "crm_sale_quotation", "doc": "QT编号,选 MDM ç‰©æ–™æŠ¥ä»·ï¼ŒAI OCR å½•入,草稿→已转合同", "upstream": ["商机"], "downstream": ["合同"], "keywords": "报价 quotation QT"},
    {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号,BPM å®¡æ‰¹(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract å®¡æ‰¹"},
    {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号(新增/编辑均可自定义,留空则新增自动生成、编辑保持原编号),BPM å®¡æ‰¹(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract å®¡æ‰¹"},
    {"name": "发票", "module": "销售管理", "table": "crm_invoice", "doc": "KP编号,BPM å®¡æ‰¹ï¼Œç´¯è®¡å¼€ç¥¨â‰¤åˆåŒé¢ï¼Œé™„ä»¶å¿…ä¼ ", "upstream": ["合同"], "downstream": ["回款"], "keywords": "开票 å‘票 invoice"},
    {"name": "回款", "module": "销售管理", "table": "crm_receivable", "doc": "HK编号,创建即审批中(手动审核),强制先开票,金额双校验", "upstream": ["合同", "发票"], "downstream": ["收款单(关联)"], "keywords": "回款 æ”¶æ¬¾è®¡åˆ’ receivable"},
    {"name": "销售目标", "module": "销售管理", "table": "crm_sales_target", "doc": "å¹´/å­£/月×个人/部门/产品维度的合同回款等目标", "upstream": [], "downstream": ["业绩统计"], "keywords": "目标 target ä¸šç»©"},
@@ -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-report-samples/qcType1_qcId9001.docx
docs/qc-report-samples/qcType1_qcId9002.docx
docs/qc-report-samples/qcType1_qcId9003.docx
docs/qc-report-samples/qcType2_qcId9011.docx
docs/qc-report-samples/qcType2_qcId9012.docx
docs/qc-report-samples/qcType2_qcId9013.docx
docs/qc-report-samples/qcType3_qcId9021.docx
docs/qc-report-samples/qcType3_qcId9022.docx
docs/qc-report-samples/qcType3_qcId9023.docx
docs/qc-report-samples/qcType4_qcId9031.docx
docs/qc-report-samples/qcType4_qcId9032.docx
docs/qc-report-samples/qcType4_qcId9033.docx
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/qc_product_report_word_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
# è´¨æ£€äº§å“æ£€éªŒå• Word å¯¼å‡º - å‰ç«¯è”调方案
> å¯¹åº”纸质"××产品质量检验单"版式(如:宁夏九泓化工科技有限公司 å·¥ä¸šç”¨æ­£ä¸é†‡äº§å“è´¨é‡æ£€éªŒå•)。
> æ”¯æŒä¸‹è½½ Word(.docx);**打印通过下载后用 Word æ‰“开打印**,后端不再提供打印页面。
> IQC/IPQC/OQC/RQC å››ç±»æ£€éªŒå•通用,与现有 `/report` èšåˆæ•°æ®æŽ¥å£å…±ç”¨å‚数。
## æ¶‰åŠé¡µé¢
- å››ç±»æ£€éªŒå•列表/详情的操作列:新增「导出检验单(Word)」入口(与既有「打印报告」并列)
- å¤šæ ·å“ï¼ˆå¤šä¸ªæ£€éªŒç»“果)的单据:导出前允许选择样品(传 `resultId`,不传默认取最早一个样品)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/qc/indicator-result/export-product-word | å¯¼å‡ºäº§å“æ£€éªŒå• Word,响应为 docx äºŒè¿›åˆ¶æµ |
**请求参数(query):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| qcId | Long | æ˜¯ | è´¨æ£€å• ID |
| qcType | Integer | æ˜¯ | è´¨æ£€ç±»åž‹ï¼š1=IQC、2=IPQC、3=OQC、4=RQC |
| resultId | Long | å¦ | æ£€éªŒç»“果(样品)编号,多样品时用于选择 |
**响应:**
- Content-Type:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`
- Content-Disposition:`attachment;filename=<UTF8编码>`,文件名为 `{物料名称}产品质量检验单-{检验单号}.docx`
- æƒé™æ ‡è¯†ï¼š`mes:qc-iqc:query`(与 `/report` ä¸€è‡´ï¼‰
**前端调用方式:** ä¸Žæ—¢æœ‰ Excel å¯¼å‡ºä¸€è‡´ï¼Œaxios ä»¥ blob è¯·æ±‚后触发下载(需带 token),不可用 `<a href>` ç›´é“¾ã€‚
## æŠ¥å‘Šç‰ˆå¼ä¸Žå­—段带入
后端从 `/report` åŒæºæ•°æ®ç»„装,字段映射如下:
| çº¸è´¨æ ä½ | æ•°æ®æ¥æº | è¯´æ˜Ž |
|----------|----------|------|
| æŠ¥å‘Šæ ‡é¢˜å…¬å¸å / ç”Ÿäº§å•位 | å†™æ­»ã€Œå®å¤ä¹æ³“化工科技有限公司」 | ç¬¬äºŒè¡Œä¸ºã€Œ{物料名称}产品质量检验单」 |
| ç”Ÿäº§æ—¶é—´ | è´¨æ£€å•创建时间(`docCreateTime`,新增响应字段) | |
| æ‰¹å· | `batchNo` | IQC=供应商批次,OQC/RQC/IPQC=单据批次号 |
| é‡‡æ ·åœ°ç‚¹ | `samplingLocation` | |
| æ‰¹é‡ | `batchQuantity` | ç©ºå€¼æ‰“印 `/` |
| é‡‡æ ·è€… | `inspectorName`(检验人) | |
| å–样日期 | `samplingTime` | |
| æ‰§è¡Œæ ‡å‡† | `executeStandard` | |
| æ£€éªŒæ—¥æœŸ | `inspectDate`(新增响应字段) | |
| æ£€éªŒé¡¹ç›® | å½•入项名称 | **不含技术指标列**;分组行与公式文本不展示,分组下录入项平铺 |
| æ£€éªŒç»“æžœ | æµ‹é‡å€¼ | å¹³è¡Œæµ‹é‡å¤šå€¼é¡¿å·åˆå¹¶ï¼ˆå¦‚ `10.023、10.115`);无值 `/` |
| åˆ¤å®šç»“论 | `checkResultText` | è‡ªåŠ¨å¸¦å‡ºæ£€éªŒç»“æžœæ–‡æ¡ˆï¼ˆæŒ‰ `qcType` å–字典,OQC ç”¨ `mes_oqc_check_result`),未判定时留空 |
| è´¨æ£€ä¸“用章 | ç•™ç©º | æ‰“印后盖章 |
| å¤‡æ³¨ | å•据 `remark`,为空时默认「检验结果仅对样品本身或本批次负责。」 | |
| æ£€éªŒ / å¤æ ¸ / æ‰¹å‡† | `inspectorName` / `auditName` / `approveName` | åˆ†åˆ«å¯¹åº”单据的检测人员、复核人、审核人;未指定人员时该段留空、打印后手写 |
## æŽ¥å£å˜æ›´ç‚¹ï¼ˆ`/mes/qc/indicator-result/report` å“åº”)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| inspectDate | DateTime | æ£€éªŒæ—¥æœŸï¼ˆæ£€éªŒå• inspectDate å­—段) |
| docCreateTime | DateTime | è´¨æ£€å•创建时间,Word æŠ¥å‘Šæ‰“印为"生产时间" |
既有前端打印组件(qc-report-print.vue)不受影响,新字段向后兼容。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å¤šæ ·å“ | `resultId` ä¸ä¼ å–最早样品;与 `/report` è¡Œä¸ºä¸€è‡´ |
| æŠ€æœ¯æŒ‡æ ‡ | æœ¬ç‰ˆå¼å›ºå®šä¸è¾“出阈值/标准值列;需要技术指标对照时使用既有「打印报告」化验记录版式 |
| åˆ¤å®šç»“论 | è‡ªåŠ¨å¸¦å‡ºæ£€éªŒç»“æžœæ–‡æ¡ˆï¼›å•æ®æœªåˆ¤å®šï¼ˆ`checkResult` ä¸ºç©ºï¼‰æ—¶ç•™ç©ºæ‰‹å¡« |
| æ£€éªŒ/复核/批准 | åˆ†åˆ«å–检测人员、复核人、审核人姓名打印;三项均可为空(打印后手写),不因缺人员而报错 |
| ç©ºå€¼ | æ–‡æœ¬ç©ºæ‰“印 `/`;批量/日期为空打印 `/` |
| åˆ†ç»„指标 | åˆ†ç»„本身不占行,仅平铺其下录入项(含中间参数行,如"试样质量 m/g") |
## æ³¨æ„äº‹é¡¹
- æ–‡ä»¶åå«ä¸­æ–‡ï¼ŒåŽç«¯å·²æŒ‰ UTF-8 ç¼–码放 header,前端 blob ä¸‹è½½æ—¶å»ºè®®ç›´æŽ¥ç”¨å…¥å‚拼文件名或解析 Content-Disposition。
- å¯¼å‡ºå†…容为静态版式(4 åˆ—网格表),无页眉页脚、无 Logo å›¾ç‰‡ï¼›Logo å¦‚需展示由前端确认后再扩展。
- å›žå½’点:`/report` æ—¢æœ‰å­—段与打印组件行为不变;本次仅新增两个响应字段与新导出接口。
## åŽç«¯éªŒè¯
- å•测:`yudao-module-mes` æ¨¡å— `MesQcProductReportWordBuilderTest`(生成样例可读回断言),样例文件 `yudao-module-mes/target/MesQcProductReportWordBuilder-sample.docx` å¯ç”¨ Word æ‰“开核对版式。
docs/seed_qc_mock_data.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,237 @@
-- =====================================================================
-- MES è´¨æ£€æŠ¥å‘Šæ¼”示数据:IQC/IPQC/OQC/RQC å„ 3 æ¡å·²å®Œæˆå•据
-- å«åˆ†ç»„检验项(水分/酸含量/正丁醇含量平铺) + å¹³è¡Œæµ‹é‡(顿号合并) + ç©ºå€¼å ä½("/")
-- ç­¾å­—:检验=张三(231) å¤æ ¸=张静波(232) æ‰¹å‡†=超级管理员(1)
-- ä»…本地演示库执行;重复执行安全(先按固定编码清理)
-- =====================================================================
-- ---------- æ¸…理上次演示数据 ----------
DELETE FROM mes_qc_indicator_result_detail WHERE result_id IN (SELECT id FROM (SELECT id FROM mes_qc_indicator_result WHERE code LIKE 'MIR-DEMO%') t);
DELETE FROM mes_qc_indicator_result WHERE code LIKE 'MIR-DEMO%';
DELETE FROM mes_qc_iqc_line WHERE iqc_id IN (SELECT id FROM (SELECT id FROM mes_qc_iqc WHERE code LIKE 'IQC-DEMO%') t);
DELETE FROM mes_qc_iqc WHERE code LIKE 'IQC-DEMO%';
DELETE FROM mes_qc_ipqc_line WHERE ipqc_id IN (SELECT id FROM (SELECT id FROM mes_qc_ipqc WHERE code LIKE 'IPQC-DEMO%') t);
DELETE FROM mes_qc_ipqc WHERE code LIKE 'IPQC-DEMO%';
DELETE FROM mes_qc_oqc_line WHERE oqc_id IN (SELECT id FROM (SELECT id FROM mes_qc_oqc WHERE code LIKE 'OQC-DEMO%') t);
DELETE FROM mes_qc_oqc WHERE code LIKE 'OQC-DEMO%';
DELETE FROM mes_qc_rqc_line WHERE rqc_id IN (SELECT id FROM (SELECT id FROM mes_qc_rqc WHERE code LIKE 'RQC-DEMO%') t);
DELETE FROM mes_qc_rqc WHERE code LIKE 'RQC-DEMO%';
-- ---------- IQC æ¥æ–™æ£€éªŒå•(qc_type=1) ----------
INSERT INTO mes_qc_iqc (id,code,name,template_id,source_doc_type,source_doc_id,source_line_id,source_doc_code,vendor_id,vendor_batch,item_id,received_quantity,check_quantity,qualified_quantity,unqualified_quantity,check_result,receive_date,inspect_date,inspector_user_id,status,sampling_location,certificate_no,execute_standard,audit_user_id,approve_user_id,remark,creator,create_time,updater,update_time,deleted)
VALUES
(9001,'IQC-DEMO-01','精萘来料检验-01',NULL,NULL,NULL,NULL,NULL,NULL,'JN-20260918-A01',2,2000,200,198,2,1,'2026-09-17 08:00:00','2026-09-18 09:30:00',231,4,'原料库取样口','CERT-A001','GB/T 6027',232,1,'演示数据','1','2026-09-18 09:00:00','1','2026-09-18 09:30:00',0),
(9002,'IQC-DEMO-02','精萘来料检验-02',NULL,NULL,NULL,NULL,NULL,NULL,'JN-20260918-A02',2,1500,150,150,0,1,'2026-09-17 08:00:00','2026-09-18 10:15:00',231,4,'精馏塔取样口','CERT-A002','GB/T 6027',232,1,NULL,'1','2026-09-18 10:00:00','1','2026-09-18 10:15:00',0),
(9003,'IQC-DEMO-03','壬烯来料检验-03',NULL,NULL,NULL,NULL,NULL,NULL,'JN-20260918-A03',4,800,80,76,4,3,'2026-09-17 08:00:00','2026-09-18 11:00:00',231,4,'车间取样口 A-3','CERT-A003','GB/T 6028',232,1,'不合格批次,已走退货流程。','1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0);
-- ---------- IPQC è¿‡ç¨‹æ£€éªŒå•(qc_type=2) ----------
INSERT INTO mes_qc_ipqc (id,code,name,type,template_id,source_doc_id,source_doc_code,work_order_id,item_id,check_quantity,qualified_quantity,unqualified_quantity,check_result,inspect_date,inspector_user_id,status,sampling_location,certificate_no,execute_standard,audit_user_id,approve_user_id,remark,creator,create_time,updater,update_time,deleted)
VALUES
(9011,'IPQC-DEMO-01','磺化中控检验-01',NULL,NULL,NULL,'GD-20260918-01',NULL,4,300,298,2,1,'2026-09-18 13:00:00',231,4,'磺化工序取样阀','CERT-P001','Q/JH 02-2026',232,1,'演示数据','1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9012,'IPQC-DEMO-02','中和中控检验-02',NULL,NULL,NULL,'GD-20260918-02',NULL,6,260,260,0,1,'2026-09-18 14:00:00',231,4,'中和釜取样阀','CERT-P002','Q/JH 02-2026',232,1,'演示数据(密度/酸结果未测)','1','2026-09-18 14:00:00','1','2026-09-18 14:00:00',0),
(9013,'IPQC-DEMO-03','离心中控检验-03',NULL,NULL,NULL,'GD-20260918-03',NULL,2,120,120,0,1,'2026-09-18 15:30:00',231,4,'离心工序取样口','CERT-P003','Q/JH 02-2026',232,1,'演示数据','1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0);
-- ---------- OQC å‡ºè´§æ£€éªŒå•(qc_type=3) ----------
INSERT INTO mes_qc_oqc (id,code,name,template_id,source_doc_code,client_id,batch_code,item_id,out_quantity,check_quantity,qualified_quantity,unqualified_quantity,check_result,out_date,inspect_date,inspector_user_id,status,sampling_location,certificate_no,execute_standard,audit_user_id,approve_user_id,remark,creator,create_time,updater,update_time,deleted)
VALUES
(9021,'OQC-DEMO-01','精萘出货检验-01',NULL,'CH-20260918-01',NULL,'JH-CH0918-01',2,1000,100,99,1,1,'2026-09-18 16:00:00','2026-09-18 16:30:00',231,4,'成品库发货区','CERT-O001','GB/T 6027',232,1,'演示数据','1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9022,'OQC-DEMO-02','壬烯出货检验-02',NULL,'CH-20260918-02',NULL,'JH-CH0918-02',4,500,50,50,0,1,'2026-09-18 17:00:00','2026-09-18 17:30:00',231,4,'成品库发货区','CERT-O002','GB/T 6028',232,1,'客户指定复检批次。','1','2026-09-18 17:30:00','1','2026-09-18 17:30:00',0),
(9023,'OQC-DEMO-03','精萘出货检验-03',NULL,'CH-20260918-03',NULL,'JH-CH0918-03',2,300,30,30,0,1,'2026-09-18 18:00:00','2026-09-18 18:30:00',231,4,'成品库发货区','CERT-O003','GB/T 6027',232,1,NULL,'1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0);
-- ---------- RQC é€€è´§æ£€éªŒå•(qc_type=4) ----------
INSERT INTO mes_qc_rqc (id,code,name,type,template_id,source_doc_code,item_id,batch_code,check_quantity,qualified_quantity,unqualified_quantity,check_result,inspect_date,inspector_user_id,status,sampling_location,certificate_no,execute_standard,audit_user_id,approve_user_id,remark,creator,create_time,updater,update_time,deleted)
VALUES
(9031,'RQC-DEMO-01','精萘退货检验-01',NULL,NULL,'TH-20260918-01',2,'JH-TH0918-01',50,40,10,3,'2026-09-18 19:00:00',231,4,'退货暂存区','CERT-R001','GB/T 6027',232,1,'演示数据','1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9032,'RQC-DEMO-02','壬烯退货检验-02',NULL,NULL,'TH-20260918-02',4,'JH-TH0918-02',30,30,0,1,'2026-09-18 19:30:00',231,4,'退货暂存区','CERT-R002','GB/T 6028',232,1,'退货原因:运输污染,全批复检。','1','2026-09-18 19:30:00','1','2026-09-18 19:30:00',0),
(9033,'RQC-DEMO-03','液碱退货检验-03',NULL,NULL,'TH-20260918-03',6,'JH-TH0918-03',20,18,2,2,'2026-09-18 20:00:00',231,4,'退货暂存区','CERT-R003','GB/T 1119',232,1,NULL,'1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0);
-- ---------- æ£€éªŒå•行(指标含分组:21 æ°´åˆ† / 26 é…¸å«é‡ / 31 æ­£ä¸é†‡å«é‡ï¼›å­é¡¹è‡ªåŠ¨è¡¥å…¨ï¼‰ ----------
INSERT INTO mes_qc_iqc_line (iqc_id,indicator_id,tool,check_method,standard_value,creator,create_time,updater,update_time,deleted)
SELECT d.id, i.indicator_id, i.tool, i.check_method, i.standard_value, '1','2026-09-18 09:00:00','1','2026-09-18 09:00:00',0
FROM (SELECT 9001 id UNION ALL SELECT 9002 UNION ALL SELECT 9003) d
CROSS JOIN (
  SELECT 19 AS indicator_id,'目测' AS tool,'应澄清透明,无机械杂质' AS check_method,NULL AS standard_value
  UNION ALL SELECT 20,'铂钴比色仪','色度 Hazen â‰¤ 10',10.000000
  UNION ALL SELECT 21,'卡尔费休水分仪','水分测定分组',NULL
  UNION ALL SELECT 22,'分析天平','称量',NULL
  UNION ALL SELECT 23,'分析天平','称量',NULL
  UNION ALL SELECT 24,'分析天平','称量',NULL
  UNION ALL SELECT 25,NULL,'按公式计算',NULL
  UNION ALL SELECT 26,'电位滴定仪','酸含量测定分组',NULL
  UNION ALL SELECT 27,'自动电位滴定仪','标定',NULL
  UNION ALL SELECT 28,'自动电位滴定仪','滴定',NULL
  UNION ALL SELECT 29,'分析天平','称量',NULL
  UNION ALL SELECT 30,NULL,'按公式计算',NULL
  UNION ALL SELECT 31,'气相色谱仪','正丁醇含量测定分组',NULL
  UNION ALL SELECT 32,'气相色谱仪','外标法',NULL
  UNION ALL SELECT 33,'密度计','密度 (20℃) 809~812',810.000000
  UNION ALL SELECT 34,'阿贝折光仪','折光率 n²⁰D',1.399000
) i;
INSERT INTO mes_qc_ipqc_line (ipqc_id,indicator_id,tool,check_method,standard_value,creator,create_time,updater,update_time,deleted)
SELECT d.id, i.indicator_id, i.tool, i.check_method, i.standard_value, '1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0
FROM (SELECT 9011 id UNION ALL SELECT 9012 UNION ALL SELECT 9013) d
CROSS JOIN (
  SELECT 19 AS indicator_id,'目测' AS tool,'应澄清透明,无机械杂质' AS check_method,NULL AS standard_value
  UNION ALL SELECT 20,'铂钴比色仪','色度 Hazen â‰¤ 10',10.000000
  UNION ALL SELECT 21,'卡尔费休水分仪','水分测定分组',NULL
  UNION ALL SELECT 22,'分析天平','称量',NULL
  UNION ALL SELECT 23,'分析天平','称量',NULL
  UNION ALL SELECT 24,'分析天平','称量',NULL
  UNION ALL SELECT 25,NULL,'按公式计算',NULL
  UNION ALL SELECT 26,'电位滴定仪','酸含量测定分组',NULL
  UNION ALL SELECT 27,'自动电位滴定仪','标定',NULL
  UNION ALL SELECT 28,'自动电位滴定仪','滴定',NULL
  UNION ALL SELECT 29,'分析天平','称量',NULL
  UNION ALL SELECT 30,NULL,'按公式计算',NULL
  UNION ALL SELECT 31,'气相色谱仪','正丁醇含量测定分组',NULL
  UNION ALL SELECT 32,'气相色谱仪','外标法',NULL
  UNION ALL SELECT 33,'密度计','密度 (20℃) 809~812',810.000000
  UNION ALL SELECT 34,'阿贝折光仪','折光率 n²⁰D',1.399000
) i;
INSERT INTO mes_qc_oqc_line (oqc_id,indicator_id,tool,check_method,standard_value,creator,create_time,updater,update_time,deleted)
SELECT d.id, i.indicator_id, i.tool, i.check_method, i.standard_value, '1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0
FROM (SELECT 9021 id UNION ALL SELECT 9022 UNION ALL SELECT 9023) d
CROSS JOIN (
  SELECT 19 AS indicator_id,'目测' AS tool,'应澄清透明,无机械杂质' AS check_method,NULL AS standard_value
  UNION ALL SELECT 20,'铂钴比色仪','色度 Hazen â‰¤ 10',10.000000
  UNION ALL SELECT 21,'卡尔费休水分仪','水分测定分组',NULL
  UNION ALL SELECT 22,'分析天平','称量',NULL
  UNION ALL SELECT 23,'分析天平','称量',NULL
  UNION ALL SELECT 24,'分析天平','称量',NULL
  UNION ALL SELECT 25,NULL,'按公式计算',NULL
  UNION ALL SELECT 26,'电位滴定仪','酸含量测定分组',NULL
  UNION ALL SELECT 27,'自动电位滴定仪','标定',NULL
  UNION ALL SELECT 28,'自动电位滴定仪','滴定',NULL
  UNION ALL SELECT 29,'分析天平','称量',NULL
  UNION ALL SELECT 30,NULL,'按公式计算',NULL
  UNION ALL SELECT 31,'气相色谱仪','正丁醇含量测定分组',NULL
  UNION ALL SELECT 32,'气相色谱仪','外标法',NULL
  UNION ALL SELECT 33,'密度计','密度 (20℃) 809~812',810.000000
  UNION ALL SELECT 34,'阿贝折光仪','折光率 n²⁰D',1.399000
) i;
INSERT INTO mes_qc_rqc_line (rqc_id,indicator_id,tool,check_method,standard_value,creator,create_time,updater,update_time,deleted)
SELECT d.id, i.indicator_id, i.tool, i.check_method, i.standard_value, '1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0
FROM (SELECT 9031 id UNION ALL SELECT 9032 UNION ALL SELECT 9033) d
CROSS JOIN (
  SELECT 19 AS indicator_id,'目测' AS tool,'应澄清透明,无机械杂质' AS check_method,NULL AS standard_value
  UNION ALL SELECT 20,'铂钴比色仪','色度 Hazen â‰¤ 10',10.000000
  UNION ALL SELECT 21,'卡尔费休水分仪','水分测定分组',NULL
  UNION ALL SELECT 22,'分析天平','称量',NULL
  UNION ALL SELECT 23,'分析天平','称量',NULL
  UNION ALL SELECT 24,'分析天平','称量',NULL
  UNION ALL SELECT 25,NULL,'按公式计算',NULL
  UNION ALL SELECT 26,'电位滴定仪','酸含量测定分组',NULL
  UNION ALL SELECT 27,'自动电位滴定仪','标定',NULL
  UNION ALL SELECT 28,'自动电位滴定仪','滴定',NULL
  UNION ALL SELECT 29,'分析天平','称量',NULL
  UNION ALL SELECT 30,NULL,'按公式计算',NULL
  UNION ALL SELECT 31,'气相色谱仪','正丁醇含量测定分组',NULL
  UNION ALL SELECT 32,'气相色谱仪','外标法',NULL
  UNION ALL SELECT 33,'密度计','密度 (20℃) 809~812',810.000000
  UNION ALL SELECT 34,'阿贝折光仪','折光率 n²⁰D',1.399000
) i;
-- ---------- æ£€éªŒç»“果主表(每单据 1 ä¸ªæ ·å“ï¼‰ ----------
INSERT INTO mes_qc_indicator_result (id,code,qc_id,qc_type,item_id,sn,remark,creator,create_time,updater,update_time,deleted)
VALUES
(9001,'MIR-DEMO-01',9001,1,2,'YP-A001',NULL,'1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9002,'MIR-DEMO-02',9002,1,2,'YP-A002',NULL,'1','2026-09-18 10:15:00','1','2026-09-18 10:15:00',0),
(9003,'MIR-DEMO-03',9003,1,4,'YP-A003',NULL,'1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0),
(9004,'MIR-DEMO-04',9011,2,4,'YP-P001',NULL,'1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9005,'MIR-DEMO-05',9012,2,6,'YP-P002',NULL,'1','2026-09-18 14:00:00','1','2026-09-18 14:00:00',0),
(9006,'MIR-DEMO-06',9013,2,2,'YP-P003',NULL,'1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0),
(9007,'MIR-DEMO-07',9021,3,2,'YP-O001',NULL,'1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9008,'MIR-DEMO-08',9022,3,4,'YP-O002',NULL,'1','2026-09-18 17:30:00','1','2026-09-18 17:30:00',0),
(9009,'MIR-DEMO-09',9023,3,2,'YP-O003',NULL,'1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0),
(9010,'MIR-DEMO-10',9031,4,2,'YP-R001',NULL,'1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9011,'MIR-DEMO-11',9032,4,4,'YP-R002',NULL,'1','2026-09-18 19:30:00','1','2026-09-18 19:30:00',0),
(9012,'MIR-DEMO-12',9033,4,6,'YP-R003',NULL,'1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0);
-- ---------- æ£€éªŒç»“果明细:独立项 ----------
-- 9002 ä¸å½• å¯†åº¦33/折光率34;9005 ä¸å½• æ­£ä¸é†‡32;9008 ä¸å½• é…¸ç»“æžœ30 â€”— æŠ¥å‘Šä¸­æ˜¾ç¤º "/"
INSERT INTO mes_qc_indicator_result_detail (result_id,indicator_id,measure_no,`value`,creator,create_time,updater,update_time,deleted)
VALUES
(9001,19,1,'澄清透明液体,无机械杂质','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,20,1,'5','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,33,1,'810','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,34,1,'1.3993','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9002,19,1,'淡黄色透明液体','1','2026-09-18 10:15:00','1','2026-09-18 10:15:00',0),
(9002,20,1,'8','1','2026-09-18 10:15:00','1','2026-09-18 10:15:00',0),
(9003,19,1,'透明液体,无悬浮物','1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0),
(9003,20,1,'3','1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0),
(9003,33,1,'795','1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0),
(9003,34,1,'1.3985','1','2026-09-18 11:00:00','1','2026-09-18 11:00:00',0),
(9004,19,1,'微浊','1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9004,20,1,'12','1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9004,33,1,'812','1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9004,34,1,'1.4001','1','2026-09-18 13:00:00','1','2026-09-18 13:00:00',0),
(9005,19,1,'澄清透明液体','1','2026-09-18 14:00:00','1','2026-09-18 14:00:00',0),
(9005,20,1,'6','1','2026-09-18 14:00:00','1','2026-09-18 14:00:00',0),
(9006,19,1,'无色液体','1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0),
(9006,20,1,'4','1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0),
(9006,33,1,'809','1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0),
(9006,34,1,'1.3990','1','2026-09-18 15:30:00','1','2026-09-18 15:30:00',0),
(9007,19,1,'澄清透明液体,无机械杂质','1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9007,20,1,'5','1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9007,33,1,'811','1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9007,34,1,'1.3992','1','2026-09-18 16:30:00','1','2026-09-18 16:30:00',0),
(9008,19,1,'透明液体','1','2026-09-18 17:30:00','1','2026-09-18 17:30:00',0),
(9008,33,1,'808','1','2026-09-18 17:30:00','1','2026-09-18 17:30:00',0),
(9008,34,1,'1.3988','1','2026-09-18 17:30:00','1','2026-09-18 17:30:00',0),
(9009,19,1,'澄清透明液体','1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0),
(9009,20,1,'7','1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0),
(9009,33,1,'810','1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0),
(9009,34,1,'1.3995','1','2026-09-18 18:30:00','1','2026-09-18 18:30:00',0),
(9010,19,1,'浑浊,底部沉淀','1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9010,20,1,'18','1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9010,33,1,'815','1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9010,34,1,'1.4005','1','2026-09-18 19:00:00','1','2026-09-18 19:00:00',0),
(9011,19,1,'透明液体','1','2026-09-18 19:30:00','1','2026-09-18 19:30:00',0),
(9011,20,1,'6','1','2026-09-18 19:30:00','1','2026-09-18 19:30:00',0),
(9011,34,1,'1.3991','1','2026-09-18 19:30:00','1','2026-09-18 19:30:00',0),
(9012,19,1,'淡黄色液体','1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0),
(9012,20,1,'9','1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0),
(9012,33,1,'810','1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0),
(9012,34,1,'1.3993','1','2026-09-18 20:00:00','1','2026-09-18 20:00:00',0);
-- ---------- åˆ†ç»„子项明细:9001 å«å¹³è¡Œæµ‹é‡ï¼ˆé…¸ç»“æžœ2批、正丁醇含量3批顿号合并) ----------
INSERT INTO mes_qc_indicator_result_detail (result_id,indicator_id,measure_no,`value`,creator,create_time,updater,update_time,deleted)
VALUES
(9001,22,1,'10.52','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,23,1,'12.35','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,24,1,'1.83','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,25,1,'0.048','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,27,1,'0.1005','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,28,1,'0.15','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,29,1,'10.02','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,30,1,'0.090','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,30,2,'0.095','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,32,1,'99.82','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,32,2,'99.85','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0),
(9001,32,3,'99.87','1','2026-09-18 09:30:00','1','2026-09-18 09:30:00',0);
-- ---------- åˆ†ç»„子项明细:其余样品(单批),并按注释故意缺项 ----------
INSERT INTO mes_qc_indicator_result_detail (result_id,indicator_id,measure_no,`value`,creator,create_time,updater,update_time,deleted)
SELECT r.id, m.indicator_id, 1, m.val, '1','2026-09-18 12:00:00','1','2026-09-18 12:00:00',0
FROM (
  SELECT 9002 AS id UNION ALL SELECT 9003 UNION ALL SELECT 9004 UNION ALL SELECT 9005 UNION ALL
  SELECT 9006 UNION ALL SELECT 9007 UNION ALL SELECT 9008 UNION ALL SELECT 9009 UNION ALL
  SELECT 9010 UNION ALL SELECT 9011 UNION ALL SELECT 9012
) r
CROSS JOIN (
  SELECT 22 AS indicator_id,'10.50' AS val
  UNION ALL SELECT 23,'12.30'
  UNION ALL SELECT 24,'1.80'
  UNION ALL SELECT 25,'0.052'
  UNION ALL SELECT 27,'0.1005'
  UNION ALL SELECT 28,'0.14'
  UNION ALL SELECT 29,'10.00'
  UNION ALL SELECT 30,'0.095'
  UNION ALL SELECT 32,'99.78'
) m
WHERE NOT (r.id=9005 AND m.indicator_id=32)
  AND NOT (r.id=9008 AND m.indicator_id=30)
  AND NOT (r.id=9011 AND m.indicator_id=22);
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-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java
@@ -7,6 +7,7 @@
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -30,6 +31,11 @@
    @NotNull(message = "合同名称不能为空")
    private String name;
    @Schema(description = "合同编号,留空则自动生成;新增和更新均可自定义,更新时留空则保持原编号", example = "HT20260921001")
    @DiffLogField(name = "合同编号")
    @Size(max = 100, message = "合同编号长度不能超过 100 ä¸ªå­—符")
    private String no;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
    @NotNull(message = "客户编号不能为空")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -20,6 +20,7 @@
    ErrorCode CONTRACT_GENERATE_ORDER_FAIL_EXISTS_ORDER = new ErrorCode(1_020_000_007, "生成销售订单失败,原因:已关联销售订单【{}】");
    ErrorCode CONTRACT_GENERATE_ORDER_FAIL_NO_PRODUCT = new ErrorCode(1_020_000_008, "生成销售订单失败,原因:合同没有产品信息");
    ErrorCode CONTRACT_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_020_000_009, "合同审批流程不存在或未激活,请检查 BPM é…ç½®");
    ErrorCode CONTRACT_NO_DUPLICATE = new ErrorCode(1_020_000_010, "合同编号【{}】已存在,请更换后重试");
    // ========== çº¿ç´¢ç®¡ç† 1-020-001-000 ==========
    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -150,10 +150,15 @@
        List<CrmContractProductDO> contractProducts = validateContractProducts(createReqVO.getProducts());
        // 1.2 æ ¡éªŒå…³è”字段
        validateRelationDataExists(createReqVO);
        // 1.3 ç”Ÿæˆåºå·
        String no = noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX);
        if (contractMapper.selectByNo(no) != null) {
            throw exception(CONTRACT_NO_EXISTS);
        // 1.3 ç”Ÿæˆæˆ–校验合同编号:支持自定义编号,未填写时自动生成
        String no = StrUtil.trimToNull(createReqVO.getNo());
        if (no == null) {
            no = noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX);
            if (contractMapper.selectByNo(no) != null) {
                throw exception(CONTRACT_NO_EXISTS);
            }
        } else if (contractMapper.selectByNo(no) != null) {
            throw exception(CONTRACT_NO_DUPLICATE, no);
        }
        // 2.1 æ’入合同
@@ -188,14 +193,26 @@
        updateReqVO.setOwnerUserId(null); // ä¸å…è®¸æ›´æ–°çš„字段
        // 1.1 æ ¡éªŒå­˜åœ¨
        CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
        // 1.2 åªæœ‰è‰ç¨¿ã€å®¡æ‰¹ä¸­ï¼Œå¯ä»¥ç¼–辑;
        // 1.2 åˆåŒç¼–号:留空则保持原编号,填写则校验唯一性(排除自身)
        //     ç•™ç©ºæ—¶å›žå¡«åŽŸç¼–å·è€Œéžç½®ç©ºï¼Œé¿å…æ“ä½œæ—¥å¿— diff å‡ºçŽ°â€œç¼–å·è¢«æ¸…ç©ºâ€
        String no = StrUtil.trimToNull(updateReqVO.getNo());
        if (no == null) {
            no = oldContract.getNo();
        } else {
            CrmContractDO noContract = contractMapper.selectByNo(no);
            if (noContract != null && !ObjUtil.equal(noContract.getId(), updateReqVO.getId())) {
                throw exception(CONTRACT_NO_DUPLICATE, no);
            }
        }
        updateReqVO.setNo(no);
        // 1.3 åªæœ‰è‰ç¨¿ã€å®¡æ‰¹ä¸­ï¼Œå¯ä»¥ç¼–辑;
        if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT);
        }
        // 1.3 æ ¡éªŒäº§å“é¡¹çš„æœ‰æ•ˆæ€§
        // 1.4 æ ¡éªŒäº§å“é¡¹çš„æœ‰æ•ˆæ€§
        List<CrmContractProductDO> contractProducts = validateContractProducts(updateReqVO.getProducts());
        // 1.4 æ ¡éªŒå…³è”字段
        // 1.5 æ ¡éªŒå…³è”字段
        validateRelationDataExists(updateReqVO);
        // 2.1 æ›´æ–°åˆåŒ
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
@@ -13,11 +13,6 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -26,11 +21,10 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.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.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
@@ -61,6 +55,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@Tag(name = "管理后台 - MES è¿‡ç¨‹æ£€éªŒå•(IPQC)")
@RestController
@RequestMapping("/mes/qc/ipqc")
@@ -70,15 +65,9 @@
    @Resource
    private MesQcIpqcService ipqcService;
    @Resource
    private MesProWorkOrderService workOrderService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdWorkstationService workstationService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -157,21 +146,13 @@
        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(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位(
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcIpqcDO::getInspectorUserId));
@@ -182,12 +163,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(),
@@ -269,8 +244,8 @@
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹äººå‘˜
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIpqcDO::getInspectorUserId));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIpqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è¡Œæ˜Žç»†
        List<MesQcIpqcLineDO> lines = ipqcLineMapper.selectListByIpqcIds(convertSet(list, MesQcIpqcDO::getId));
@@ -287,10 +262,9 @@
        for (MesQcIpqcDO ipqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(ipqc.getName());
            MesMdItemDO item = itemMap.get(ipqc.getItemId());
            MdmItemRespDTO item = itemMap.get(ipqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            // IPQC æ— æ‰¹æ¬¡å·å­—段,暂用来源单据编号(一般为生产工单号)作为批号
            vo.setBatchNo(ipqc.getSourceDocCode());
            vo.setBatchNo(ipqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(ipqc.getInspectDate()));
            List<String> indicatorNames = new ArrayList<>();
            List<String> judgementSegments = new ArrayList<>();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/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/iqc/MesQcIqcController.java
@@ -15,16 +15,16 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.iqc.MesQcIqcLineMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
@@ -73,9 +73,9 @@
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIqcLineMapper iqcLineMapper;
@@ -154,12 +154,13 @@
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesQcIqcDO::getVendorId)).getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位(通过物料的 unitMeasureId)
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcIqcDO::getInspectorUserId));
@@ -255,8 +256,8 @@
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹äººå‘˜
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcIqcDO::getInspectorUserId));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcIqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è¡Œæ˜Žç»†
        List<MesQcIqcLineDO> lines = iqcLineMapper.selectListByIqcIds(convertSet(list, MesQcIqcDO::getId));
@@ -273,7 +274,7 @@
        for (MesQcIqcDO iqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(iqc.getName());
            MesMdItemDO item = itemMap.get(iqc.getItemId());
            MdmItemRespDTO item = itemMap.get(iqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(iqc.getVendorBatch());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(iqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -15,8 +15,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -25,9 +27,6 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
@@ -69,9 +68,9 @@
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -153,12 +152,13 @@
        // æ‰¹é‡æŸ¥è¯¢å®¢æˆ·
        Map<Long, CrmCustomerRespDTO> clientMap = customerApi.getCustomerMap(
                convertSet(list, MesQcOqcDO::getClientId));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcOqcDO::getInspectorUserId));
@@ -196,8 +196,8 @@
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹äººå‘˜
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcOqcDO::getInspectorUserId));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcOqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è¡Œæ˜Žç»†
        List<MesQcOqcLineDO> lines = oqcLineMapper.selectListByOqcIds(convertSet(list, MesQcOqcDO::getId));
@@ -214,7 +214,7 @@
        for (MesQcOqcDO oqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(oqc.getName());
            MesMdItemDO item = itemMap.get(oqc.getItemId());
            MdmItemRespDTO item = itemMap.get(oqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(oqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(oqc.getInspectDate()));
@@ -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
@@ -37,8 +37,7 @@
    // ========== å®¢æˆ· ==========
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "客户不能为空")
    @Schema(description = "客户 ID,可为空", example = "10")
    private Long clientId;
    // ========== ç‰©æ–™ ==========
@@ -55,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;
@@ -77,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/controller/admin/qc/rqc/MesQcRqcController.java
@@ -13,8 +13,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.MesQcRqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo.MesQcRqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
@@ -23,8 +25,6 @@
import cn.iocoder.yudao.module.mes.dal.mysql.qc.rqc.MesQcRqcLineMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
@@ -64,9 +64,9 @@
    @Resource
    private MesQcRqcService rqcService;
    @Resource
    private MesMdItemService itemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
@@ -145,12 +145,13 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹/复核/审核人员
        Set<Long> qcUserIds = new HashSet<>();
        qcUserIds.addAll(convertSet(list, MesQcRqcDO::getInspectorUserId));
@@ -242,8 +243,8 @@
        // æ‰¹é‡æŸ¥è¯¢æ£€æµ‹äººå‘˜
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcRqcDO::getInspectorUserId));
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆMDM ç‰©æ–™ï¼‰
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesQcRqcDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è¡Œæ˜Žç»†
        List<MesQcRqcLineDO> lines = rqcLineMapper.selectListByRqcIds(convertSet(list, MesQcRqcDO::getId));
@@ -260,7 +261,7 @@
        for (MesQcRqcDO rqc : list) {
            MesQcQualityExportVO vo = new MesQcQualityExportVO();
            vo.setDocName(rqc.getName());
            MesMdItemDO item = itemMap.get(rqc.getItemId());
            MdmItemRespDTO item = itemMap.get(rqc.getItemId());
            vo.setItemName(item != null ? item.getName() : "");
            vo.setBatchNo(rqc.getBatchCode());
            vo.setInspectDate(MesQcQualityExportVO.formatDateTime(rqc.getInspectDate()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/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/dataobject/qc/template/MesQcTemplateItemDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.template;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -38,7 +37,7 @@
    /**
     * äº§å“ç‰©æ–™ç¼–号
     *
     * å…³è” {@link MesMdItemDO#getId()}
     * å…³è” MDM ç‰©æ–™ç¼–号(mdm_item.id),非 MES ç‰©æ–™
     */
    private Long itemId;
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java
@@ -21,7 +21,7 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesQcIpqcDO>()
                .likeIfPresent(MesQcIpqcDO::getCode, reqVO.getCode())
                .eqIfPresent(MesQcIpqcDO::getType, reqVO.getType())
                .eqIfPresent(MesQcIpqcDO::getWorkOrderId, reqVO.getWorkOrderId())
                .likeIfPresent(MesQcIpqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcIpqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcIpqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcIpqcDO::getStatus, reqVO.getStatus())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -43,9 +43,12 @@
                .orderByDesc(MesQcOqcDO::getId);
        if (permittedClientIds != null) {
            if (CollUtil.isEmpty(permittedClientIds)) {
                return PageResult.empty();
                wrapper.isNull(MesQcOqcDO::getClientId);
            } else {
                // å…è®¸ client_id ä¸ºç©ºçš„记录(未填客户的检验单对所有有权限用户可见)
                wrapper.and(w -> w.in(MesQcOqcDO::getClientId, permittedClientIds)
                        .or().isNull(MesQcOqcDO::getClientId));
            }
            wrapper.in(MesQcOqcDO::getClientId, permittedClientIds);
        }
        return selectPage(reqVO, wrapper);
    }
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,8 @@
 * äº§å“æ£€éªŒå•(纸质"××产品质量检验单"版式)Word æž„建器
 *
 * ç‰ˆå¼ï¼šæ ‡é¢˜ï¼ˆå…¬å¸ + äº§å“æ£€éªŒå•)→ è¡¨å¤´ä¿¡æ¯ 5 è¡Œ â†’ æ£€éªŒé¡¹ç›®/检验结果 ä¸¤åˆ—矩阵 â†’
 * åˆ¤å®šç»“论/质检专用章(留空,打印后手填)→ å¤‡æ³¨ â†’ æ£€éªŒ/复核/批准签字行。
 * åˆ¤å®šç»“论(自动带出检验结果)/质检专用章(留空盖章)→ å¤‡æ³¨ â†’
 * æ£€éªŒ/复核/批准签字行(分别带出检测人、复核人、审核人姓名)。
 * ä¸å«æŠ€æœ¯æŒ‡æ ‡åˆ—;分组项不展示,录入项平铺;平行测量多值顿号合并。
 */
public class MesQcProductReportWordBuilder {
@@ -51,9 +52,9 @@
            writeTitle(doc, report);
            writeHeaderTable(doc, report);
            writeItemTable(doc, report);
            writeConclusionTable(doc);
            writeConclusionTable(doc, report);
            writeRemarkTable(doc, report);
            writeSignLine(doc);
            writeSignLine(doc, report);
            doc.write(out);
            return out.toByteArray();
        }
@@ -150,12 +151,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;
    }
@@ -170,12 +173,23 @@
    // ========== ç­¾å­—行 ==========
    /** ç­¾å­—行固定留空,与判定结论/质检专用章一致,打印后手写 */
    private static void writeSignLine(XWPFDocument doc) {
    /**
     * ç­¾å­—行:检验/复核/批准分别带出检测人(inspectorUserId)、复核人(auditUserId)、
     * å®¡æ ¸äººï¼ˆapproveUserId)姓名;未指定人员时该段留空,打印后手写
     */
    private static void writeSignLine(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFParagraph paragraph = doc.createParagraph();
        paragraph.setAlignment(ParagraphAlignment.LEFT);
        paragraph.setSpacingBefore(400);
        addRun(paragraph, "检验:         复核:         批准:        ", 12, true);
        String text = "检验:" + signName(report.getInspectorName())
                + "    复核:" + signName(report.getAuditName())
                + "    批准:" + signName(report.getApproveName());
        addRun(paragraph, text, 12, true);
    }
    /** ç­¾åæ®µï¼šæœ‰å§“名则打印姓名并留签名余量,无姓名则整段留空便于手写 */
    private static String signName(String name) {
        return notBlank(name) ? name + "   " : "     ";
    }
    // ========== è¡¨æ ¼åŸºç¡€ ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/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,18 +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 ç±»åž‹æŸ¥æ‰¾å¯ç”¨è´¨æ£€æ–¹æ¡ˆï¼ˆæ”¯æŒå‰ç«¯æ˜¾å¼æŒ‡å®šï¼‰
        validateIpqcSaveData(createReqVO);
        // 1.2 æ ¹æ®äº§å“ç‰©æ–™ + IPQC ç±»åž‹æŸ¥æ‰¾å¯ç”¨è´¨æ£€æ–¹æ¡ˆï¼ˆæ”¯æŒå‰ç«¯æ˜¾å¼æŒ‡å®šï¼‰
        // ç‰©æ–™ ID æ¥è‡ª MDM,模板服务直接按 MDM ç‰©æ–™åŒ¹é…
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                itemId, MesQcTypeEnum.IPQC.getType(), null, null);
                createReqVO.getItemId(), MesQcTypeEnum.IPQC.getType(), null, null);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -146,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());
@@ -176,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
@@ -290,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;
        }
@@ -388,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/qc/pendinginspect/MesQcPendingInspectServiceImpl.java
@@ -1,7 +1,12 @@
package cn.iocoder.yudao.module.mes.service.qc.pendinginspect;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.pendinginspect.MesQcPendingInspectMapper;
@@ -9,6 +14,13 @@
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
/**
 * MES å¾…检任务 Service å®žçŽ°ç±»
@@ -19,6 +31,10 @@
    @Resource
    private MesQcPendingInspectMapper pendingInspectMapper;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Override
    public PageResult<MesQcPendingInspectRespVO> getPendingInspectPage(MesQcPendingInspectPageReqVO pageReqVO) {
@@ -26,8 +42,22 @@
        IPage<MesQcPendingInspectRespVO> page = MyBatisUtils.buildPage(pageReqVO);
        pendingInspectMapper.selectQcPendingPage(page,
                pageReqVO.getSourceDocCode(), pageReqVO.getQcType(), pageReqVO.getItemId());
        List<MesQcPendingInspectRespVO> list = page.getRecords();
        // å¡«å……物料信息(来源单据的 item_id ä¸º MDM ç‰©æ–™ ID)
        if (CollUtil.isNotEmpty(list)) {
            Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                    convertSet(list, MesQcPendingInspectRespVO::getItemId));
            Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureList(
                    convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                    .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
            list.forEach(vo -> findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
                findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                        unitMeasure -> vo.setUnitName(unitMeasure.getName()));
            }));
        }
        // è¿”回结果
        return new PageResult<>(page.getRecords(), page.getTotal());
        return new PageResult<>(list, page.getTotal());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemService.java
@@ -64,21 +64,21 @@
    /**
     * æ ¹æ®äº§å“ç‰©æ–™ ID å’Œ QC ç±»åž‹æŸ¥æ‰¾æ£€éªŒæ¨¡æ¿çš„产品关联(必须存在,否则抛异常)
     *
     * @param itemId äº§å“ç‰©æ–™ ID
     * @param qcType æ£€æµ‹ç§ç±»ï¼ˆæžšä¸¾ {@link MesQcTypeEnum})
     * @param mdmItemId MDM äº§å“ç‰©æ–™ ID(mdm_item.id)
     * @param qcType    æ£€æµ‹ç§ç±»ï¼ˆæžšä¸¾ {@link MesQcTypeEnum})
     * @return åŒ¹é…çš„产品关联
     */
    MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long itemId, Integer qcType);
    MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long mdmItemId, Integer qcType);
    /**
     * æ ¹æ®äº§å“ç‰©æ–™ï¼ˆæˆ–工单/任务)和 QC ç±»åž‹ï¼ŒèŽ·å–æ‰€æœ‰å¯ç”¨çš„è´¨æ£€æ–¹æ¡ˆäº§å“å…³è”åˆ—è¡¨
     *
     * @param itemId      äº§å“ç‰©æ–™ ID(mes_md_item.id);与 workOrderId/taskId è‡³å°‘其一
     * @param mdmItemId   MDM äº§å“ç‰©æ–™ ID(mdm_item.id);与 workOrderId/taskId è‡³å°‘其一
     * @param qcType      æ£€æµ‹ç§ç±»ï¼ˆæžšä¸¾ {@link MesQcTypeEnum})
     * @param workOrderId ç”Ÿäº§å·¥å• ID(可选,IPQC åœºæ™¯ç”¨äºŽè§£æžäº§å“ç‰©æ–™ï¼‰
     * @param taskId      ç”Ÿäº§ä»»åŠ¡ ID(可选,优先用于解析产品物料)
     * @param workOrderId ç”Ÿäº§å·¥å• ID(可选,IPQC åœºæ™¯ç”¨äºŽè§£æžäº§å“ç‰©æ–™ï¼›å·¥å•产品为 MES ç‰©æ–™ï¼Œå†…部转换为 MDM ç‰©æ–™ï¼‰
     * @param taskId      ç”Ÿäº§ä»»åŠ¡ ID(可选,优先用于解析产品物料;任务物料为 MES ç‰©æ–™ï¼Œå†…部转换为 MDM ç‰©æ–™ï¼‰
     * @return åŒ¹é…çš„产品关联列表(保持原顺序),无匹配返回空列表
     */
    List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType, Long workOrderId, Long taskId);
    List<MesQcTemplateItemDO> getTemplateItemList(Long mdmItemId, Integer qcType, Long workOrderId, Long taskId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java
@@ -126,8 +126,8 @@
    }
    @Override
    public MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long itemId, Integer qcType) {
        List<MesQcTemplateItemDO> templateItems = getTemplateItemList(itemId, qcType, null, null);
    public MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long mdmItemId, Integer qcType) {
        List<MesQcTemplateItemDO> templateItems = getTemplateItemList(mdmItemId, qcType, null, null);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
@@ -135,28 +135,27 @@
    }
    @Override
    public List<MesQcTemplateItemDO> getTemplateItemList(Long itemId, Integer qcType, Long workOrderId, Long taskId) {
        // 1. è§£æžçœŸå®žäº§å“ç‰©æ–™ ID:任务优先,其次工单,最后直接用入参
        Long resolvedItemId = itemId;
    public List<MesQcTemplateItemDO> getTemplateItemList(Long mdmItemId, Integer qcType, Long workOrderId, Long taskId) {
        // 1. è§£æž MDM äº§å“ç‰©æ–™ ID:任务优先,其次工单;两者存的是 MES ç‰©æ–™ ID,需转换为 MDM ç‰©æ–™ ID。
        //    æœªä¼ å·¥å•/任务时,入参本身即 MDM ç‰©æ–™ ID(质检单前端选择的就是 MDM ç‰©æ–™ï¼‰
        Long resolvedMdmItemId;
        if (taskId != null) {
            resolvedItemId = proTaskService.validateTaskExists(taskId).getItemId();
            resolvedMdmItemId = convertMesItemIdToMdmItemId(proTaskService.validateTaskExists(taskId).getItemId());
        } else if (workOrderId != null) {
            resolvedItemId = proWorkOrderService.validateWorkOrderExists(workOrderId).getProductId();
            resolvedMdmItemId = convertMesItemIdToMdmItemId(
                    proWorkOrderService.validateWorkOrderExists(workOrderId).getProductId());
        } else {
            resolvedMdmItemId = mdmItemId;
        }
        if (resolvedItemId == null) {
        if (resolvedMdmItemId == null) {
            return Collections.emptyList();
        }
        // 2. mes_md_item.id è½¬ mdm_item.id(产品关联表存的是 mdm ç‰©æ–™ï¼‰
        MesMdItemDO item = mesMdItemService.getItem(resolvedItemId);
        if (item == null || item.getMdmItemId() == null) {
            return Collections.emptyList();
        }
        // 3. æŸ¥å‡ºè¯¥ç‰©æ–™å…³è”的所有产品关联
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(item.getMdmItemId());
        // 2. æŸ¥å‡ºè¯¥ç‰©æ–™å…³è”的所有产品关联
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(resolvedMdmItemId);
        if (CollUtil.isEmpty(templateItems)) {
            return Collections.emptyList();
        }
        // 4. è¿‡æ»¤ï¼šæ–¹æ¡ˆå¯ç”¨ä¸”检测种类包含 qcType,返回全部匹配项
        // 3. è¿‡æ»¤ï¼šæ–¹æ¡ˆå¯ç”¨ä¸”检测种类包含 qcType,返回全部匹配项
        List<MesQcTemplateDO> templates = templateService.getTemplateList(
                convertSet(templateItems, MesQcTemplateItemDO::getTemplateId));
        Set<Long> matchedTemplateIds = templates.stream()
@@ -169,4 +168,18 @@
                .collect(Collectors.toList());
    }
    /**
     * MES ç‰©æ–™ ID è½¬ MDM ç‰©æ–™ ID
     *
     * @param mesItemId MES ç‰©æ–™ ID(mes_md_item.id)
     * @return MDM ç‰©æ–™ ID(mdm_item.id);MES ç‰©æ–™ä¸å­˜åœ¨æˆ–未关联 MDM ç‰©æ–™æ—¶è¿”回 null
     */
    private Long convertMesItemIdToMdmItemId(Long mesItemId) {
        if (mesItemId == null) {
            return null;
        }
        MesMdItemDO item = mesMdItemService.getItem(mesItemId);
        return item != null ? item.getMdmItemId() : null;
    }
}
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,21 +54,35 @@
            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());
            // å¤‡æ³¨é»˜è®¤æ–‡æ¡ˆ
            assertTrue(tables.get(3).getRow(0).getCell(1).getText().contains("检验结果仅对样品本身或本批次负责"));
            // ç­¾å­—行留空(打印后手写),不得出现姓名
            // ç­¾å­—行带出姓名:检验=检测人、复核=复核人、批准=审核人
            String signLine = doc.getParagraphs().stream()
                    .filter(p -> p.getText().contains("批准:"))
                    .findFirst().orElseThrow().getText();
            assertTrue(signLine.contains("检验:") && signLine.contains("复核:"));
            assertTrue(!signLine.contains("李博") && !signLine.contains("周亚洁") && !signLine.contains("包翠萍"),
                    "签字行应留空,不输出检验/复核/批准姓名");
            assertTrue(signLine.contains("检验:李博"), "检验应带出检测人姓名:" + signLine);
            assertTrue(signLine.contains("复核:周亚洁"), "复核应带出复核人姓名:" + signLine);
            assertTrue(signLine.contains("批准:包翠萍"), "批准应带出审核人姓名:" + signLine);
        }
    }
    @Test
    public void testBuildWithoutSigners() throws IOException {
        MesQcQcReportRespVO report = randomReport();
        report.setInspectorName(null).setAuditName("").setApproveName(null);
        byte[] docx = MesQcProductReportWordBuilder.build(report);
        try (InputStream in = new ByteArrayInputStream(docx); XWPFDocument doc = new XWPFDocument(in)) {
            String signLine = doc.getParagraphs().stream()
                    .filter(p -> p.getText().contains("批准:"))
                    .findFirst().orElseThrow().getText();
            assertTrue(signLine.contains("检验:") && signLine.contains("复核:") && signLine.contains("批准:"),
                    "未指定人员时仍保留三个栏位供手写:" + signLine);
        }
    }
@@ -80,7 +94,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,