云
3 小时以前 5072520172e8f45e3ccb8afc26cf0c8186ba4cdd
refactor(mes): 重构质量管理和批次绑定功能

- 移除 mes_qc_oqc_action 表及相关实体类和映射器
- 删除 workflow_status 字段和相关查询条件
- 新增 mes_qc_approver_config 表用于质检审核人配置
- 添加新的错误码常量用于质检审核配置验证
- 更新种子质量工作流状态定义和菜单权限配置
- 移除物料名称唯一性校验相关代码和错误码
- 新增获取未绑定批次吨袋的功能接口
- 实现批次绑定模式检查和吨袋绑定逻辑
- 优化批次状态变更和绑定托盘吨袋处理流程
已添加24个文件
已删除4个文件
已修改33个文件
1925 ■■■■■ 文件已修改
.playwright-mcp/page-2026-10-19T06-21-53-919Z.yml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-10-19T06-22-06-902Z.yml 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-10-19T06-22-10-596Z.yml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-10-19T06-22-35-986Z.yml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_quality_flow_20261019.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_qc_oqc_retest_approver_20261019.sql 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_qc_remove_sales_quality_20261019.sql 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_qc_seed_indicator_range_20261019.sql 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_seed_quality_workflow_20261019.sql 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/MesQcApproverConfigController.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigPageReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigSaveReqVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedIndicatorRangeSaveReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchRetestReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityScopeRetestReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/approver/MesQcApproverConfigDO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedIndicatorRangeDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/approver/MesQcApproverConfigMapper.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedIndicatorRangeMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcApprovalTypeEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcApproveNodeEnum.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/approver/MesQcApproverService.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/approver/MesQcApproverServiceImpl.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedIndicatorRangeService.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedIndicatorRangeServiceTest.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-10-19T06-21-53-919Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-10-19T06-22-06-902Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
- generic [active] [ref=e4]:
  - generic [ref=e201]:
    - complementary [ref=e203]:
      - generic [ref=e204]:
        - generic [ref=e205] [cursor=pointer]
        - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=e210] [cursor=pointer]:
          - /url: javascript:void 0
          - img "工业AI智造平台" [ref=e213]
          - generic [ref=e214]: å·¥ä¸šAI智造平台
        - menu [ref=e217]:
          - menuitem "首页" [ref=e218] [cursor=pointer]
          - listitem [ref=e222]:
            - generic [ref=e223] [cursor=pointer]: åŸºç¡€æ•°æ®
          - listitem [ref=e228]:
            - generic [ref=e229] [cursor=pointer]: ç»é”€å•†ç®¡ç†
          - listitem [ref=e234]:
            - generic [ref=e235] [cursor=pointer]: ä»“储物流
          - listitem [ref=e240]:
            - generic [ref=e241] [cursor=pointer]: å‡ºåº“流通管理
          - listitem [ref=e246]:
            - generic [ref=e247] [cursor=pointer]: é˜²ä¼ªé˜²çªœè´§ç®¡ç†
          - listitem [ref=e252]:
            - generic [ref=e253] [cursor=pointer]: æº¯æºåˆ†æž
          - listitem [ref=e258]:
            - generic [ref=e259] [cursor=pointer]: ä¸€è¢‹ä¸€ç ç®¡ç†
          - listitem [ref=e264]:
            - generic [ref=e265] [cursor=pointer]: è´¨é‡ç®¡ç†
          - listitem [ref=e270]:
            - generic [ref=e271] [cursor=pointer]: ç»Ÿè®¡åˆ†æž
          - listitem [ref=e276]:
            - generic [ref=e277] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - generic [ref=e283] [cursor=pointer]
    - generic [ref=e288]:
      - generic [ref=e289]:
        - banner [ref=e290]:
          - button [ref=e291] [cursor=pointer]
          - button [ref=e293] [cursor=pointer]
          - navigation "breadcrumb" [ref=e298]:
            - list [ref=e299]:
              - listitem [ref=e300]:
                - link "首页" [disabled] [ref=e301]
          - generic [ref=e305]:
            - generic [ref=e307] [cursor=pointer]:
              - generic [ref=e311]: æœç´¢
              - generic [ref=e312]: Ctrl K
            - button [ref=e314] [cursor=pointer]
            - button "dark" [ref=e319] [cursor=pointer]
            - button [ref=e328] [cursor=pointer]
            - button [ref=e334] [cursor=pointer]
            - button [ref=e336] [cursor=pointer]
            - button [ref=e342] [cursor=pointer]:
              - button [ref=e344]
            - button "工作台" [ref=e348] [cursor=pointer]
            - button [ref=e351] [cursor=pointer]:
              - img "超级管理员" [ref=e356]
        - generic [ref=e358]:
          - generic [ref=e359]: é¦–页
          - generic [ref=e377]:
            - button [ref=e378] [cursor=pointer]
            - generic [ref=e385] [cursor=pointer]
            - generic [ref=e389] [cursor=pointer]
      - main [ref=e396]:
        - generic [ref=e400]:
          - generic [ref=e401]:
            - generic [ref=e402]: ä¸šåŠ¡å…¨é“¾è·¯
            - generic [ref=e408]:
              - generic [ref=e412] [cursor=pointer]:
                - generic [ref=e413]: å“ç§
                - generic [ref=e414]: "0"
              - generic [ref=e420] [cursor=pointer]:
                - generic [ref=e421]: äº§çº¿
                - generic [ref=e422]: "0"
              - generic [ref=e428] [cursor=pointer]:
                - generic [ref=e429]: ç”Ÿäº§æ‰¹æ¬¡
                - generic [ref=e430]: "0"
              - generic [ref=e436] [cursor=pointer]:
                - generic [ref=e437]: è¢‹ç 
                - generic [ref=e438]: "0"
              - generic [ref=e444] [cursor=pointer]:
                - generic [ref=e445]: æ‰˜ç›˜ç 
                - generic [ref=e446]: "0"
              - generic [ref=e452] [cursor=pointer]:
                - generic [ref=e453]: é”€å”®å‡ºåº“
                - generic [ref=e454]: "0"
              - generic [ref=e460] [cursor=pointer]:
                - generic [ref=e461]: æµé€šæ‰«ç 
                - generic [ref=e462]: "0"
              - generic [ref=e468] [cursor=pointer]:
                - generic [ref=e469]: é˜²çªœ/溯源
                - generic [ref=e470]: "0"
          - generic [ref=e471]:
            - generic [ref=e478] [cursor=pointer]:
              - generic [ref=e479]: å“ç§æ€»æ•°
              - generic [ref=e480]: "0"
              - generic [ref=e481]: åŸºç¡€æ•°æ®
            - generic [ref=e488] [cursor=pointer]:
              - generic [ref=e489]: ç”Ÿäº§æ‰¹æ¬¡
              - generic [ref=e490]: "0"
              - generic [ref=e491]: ä¸€è¢‹ä¸€ç 
            - generic [ref=e498] [cursor=pointer]:
              - generic [ref=e499]: è¢‹ç 
              - generic [ref=e500]: "0"
              - generic [ref=e501]: èµ‹ç 
            - generic [ref=e508] [cursor=pointer]:
              - generic [ref=e509]: æ‰˜ç›˜ç 
              - generic [ref=e510]: "0"
              - generic [ref=e511]: æ‰˜ç›˜
            - generic [ref=e518] [cursor=pointer]:
              - generic [ref=e519]: é”€å”®å‡ºåº“单
              - generic [ref=e520]: "0"
              - generic [ref=e521]: å‡ºåº“流通
            - generic [ref=e528] [cursor=pointer]:
              - generic [ref=e529]: ç´¯è®¡æ‰«ç 
              - generic [ref=e530]: "0"
              - generic [ref=e531]: æº¯æº
            - generic [ref=e538] [cursor=pointer]:
              - generic [ref=e539]: åº“存预警
              - generic [ref=e540]: "0"
              - generic [ref=e541]: ä»“储
            - generic [ref=e548] [cursor=pointer]:
              - generic [ref=e549]: å¾…评审不合格
              - generic [ref=e550]: "0"
              - generic [ref=e551]: è´¨é‡
          - generic [ref=e552]:
            - generic [ref=e556]:
              - generic [ref=e557]: è¢‹ç ç”Ÿæˆè¶‹åŠ¿
              - generic [ref=e559]:
                - generic [ref=e560] [cursor=pointer]:
                  - generic [ref=e561]:
                    - radio "近 7 å¤©" [checked]
                  - generic [ref=e562]: è¿‘ 7 å¤©
                - generic [ref=e563] [cursor=pointer]:
                  - generic [ref=e564]:
                    - radio "近 30 å¤©"
                  - generic [ref=e565]: è¿‘ 30 å¤©
            - generic [ref=e568]: æ¶ˆè´¹è€…扫码地区分布
          - generic [ref=e577]:
            - generic [ref=e578]: æ‰¹æ¬¡çŠ¶æ€åˆ†å¸ƒ
            - generic [ref=e585]: è¢‹ç çŠ¶æ€åˆ†å¸ƒ
            - generic [ref=e593]:
              - generic [ref=e594]: å¾…办与预警
              - generic [ref=e598]:
                - generic [ref=e602] [cursor=pointer]:
                  - generic [ref=e603]: çªœè´§é¢„è­¦
                  - generic [ref=e604]: å¾…处理的窜货预警
                - generic [ref=e608] [cursor=pointer]:
                  - generic [ref=e609]: åº“存预警
                  - generic [ref=e610]: æœªå¤„理的库存预警
                - generic [ref=e614] [cursor=pointer]:
                  - generic [ref=e615]: ä¸åˆæ ¼å“
                  - generic [ref=e616]: å¾…评审的不合格品单
          - generic [ref=e617]:
            - generic [ref=e618]: å¿«æ·å…¥å£
            - generic [ref=e622]:
              - generic [ref=e624]:
                - generic [ref=e625]: åŸºç¡€æ•°æ®
                - generic [ref=e626]:
                  - generic [ref=e627] [cursor=pointer]: å“ç§ç®¡ç†
                  - generic [ref=e630] [cursor=pointer]: å“ç§åˆ†ç±»
                  - generic [ref=e633] [cursor=pointer]: è®¡é‡å•位
              - generic [ref=e637]:
                - generic [ref=e638]: ä¸€è¢‹ä¸€ç ç”Ÿäº§
                - generic [ref=e639]:
                  - generic [ref=e640] [cursor=pointer]: äº§çº¿ç»´æŠ¤
                  - generic [ref=e643] [cursor=pointer]: æ‰¹æ¬¡ç”Ÿäº§
                  - generic [ref=e646] [cursor=pointer]: èµ‹ç ç®¡ç†
                  - generic [ref=e649] [cursor=pointer]: æ‰˜ç›˜ç 
                  - generic [ref=e652] [cursor=pointer]: æ‰«ç å…¥åº“
              - generic [ref=e656]:
                - generic [ref=e657]: å‡ºåº“流通
                - generic [ref=e658]:
                  - generic [ref=e659] [cursor=pointer]: å‘货通知
                  - generic [ref=e662] [cursor=pointer]: é”€å”®å‡ºåº“
                  - generic [ref=e665] [cursor=pointer]: é”€å”®é€€è´§
              - generic [ref=e669]:
                - generic [ref=e670]: é˜²ä¼ªé˜²çªœè´§
                - generic [ref=e671]:
                  - generic [ref=e672] [cursor=pointer]: çªœè´§é¢„è­¦
                  - generic [ref=e675] [cursor=pointer]: æ‰«ç åœ°ç†åˆ†å¸ƒ
                  - generic [ref=e678] [cursor=pointer]: å®Œæ•´æ€§æ ¡éªŒ
              - generic [ref=e682]:
                - generic [ref=e683]: æº¯æºåˆ†æž
                - generic [ref=e684]: æ¶ˆè´¹è€…扫码统计
              - generic [ref=e689]:
                - generic [ref=e690]: ä»“储物流
                - generic [ref=e691]:
                  - generic [ref=e692] [cursor=pointer]: ä»“库设置
                  - generic [ref=e695] [cursor=pointer]: åº“存现有量
                  - generic [ref=e698] [cursor=pointer]: åº“存预警
                  - generic [ref=e701] [cursor=pointer]: è½¬ç§»è°ƒæ‹¨
              - generic [ref=e705]:
                - generic [ref=e706]: è´¨é‡ç®¡ç†
                - generic [ref=e707]:
                  - generic [ref=e708] [cursor=pointer]: æ‰¹æ¬¡è¿½æº¯
                  - generic [ref=e711] [cursor=pointer]: ä¸åˆæ ¼å“
                  - generic [ref=e714] [cursor=pointer]: ç§å­è´¨æ£€
  - generic [ref=e719]:
    - generic [ref=e721]:
      - img "check-circle" [ref=e722]
      - generic [ref=e726]: ç™»å½•成功
      - generic [ref=e727]: æ¬¢è¿Žå›žæ¥:超级管理员
    - img "close" [ref=e730] [cursor=pointer]
.playwright-mcp/page-2026-10-19T06-22-10-596Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-10-19T06-22-35-986Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
docs/production_quality_flow_20261019.md
@@ -1,37 +1,27 @@
# ç”Ÿäº§è´¨æ£€æµç¨‹ä¸Žèœå•映射
# ç§å­è´¨æ£€æµç¨‹
依据用户提供的生产流程图和当前 `system_menu`,保留已有质量管理菜单,新增“销售质检”入口。
按袋码保存质检记录;托盘码、批次码提交时,每袋生成独立记录。纯度、净度、发芽率、水分四项指标在初检、重新初检和复检中一致。每次检验保存指标快照、操作人、时间和不合格原因。
| æµç¨‹å›¾èŠ‚ç‚¹ | ç³»ç»Ÿå…¥å£ | å®žçް |
质检项合格范围配置页维护四项指标的默认百分比上下限(含边界)。这是填写模板;未配置时可手动填写,初检、重新初检和复检均可修改本次范围。服务端按本次提交的实测值和范围判定,并保存范围快照;配置更新不改变历史记录。桌面端和浏览器移动端均可扫描托盘码或生产批次码,批量处理该范围内同一待检状态的全部袋码;提交时重新核对清单,任一袋失败则整批回滚。
| çŠ¶æ€ | è¿›å…¥æ¡ä»¶ | ä¸‹ä¸€æ­¥ |
| --- | --- | --- |
| 48/100 è¢‹æ‰«ç å…¥åº“、生成托盘码 | ä¸€è¢‹ä¸€ç  / ä»“储物流 | æ—¢æœ‰è¢‹ç ã€æ‰˜ç›˜ç ã€æ‰¹æ¬¡ç  |
| åŠ å·¥éƒ¨åˆæ£€ | è´¨é‡ç®¡ç† / ç§å­è´¨æ£€ | å¯æŒ‰è¢‹ã€æ‰˜ç›˜ã€æ‰¹æ¬¡æäº¤ï¼›æ¯è¢‹ç‹¬ç«‹è®°å½• |
| åˆæ£€ä¸åˆæ ¼ã€å¤æ£€ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | ä¿ç•™æš‚存库存,记录复检结果与历次指标 |
| åŠ å·¥éƒ¨é¢†å¯¼å®¡æ‰¹ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | å•条或同状态批量审批,可驳回重检 |
| å…¬å¸è´¨æ£€å®¡æ‰¹ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | å•条或同状态批量审批;通过后才迁移或报废库存 |
| é”€å”®éƒ¨è´¨æ£€ | è´¨é‡ç®¡ç† / é”€å”®è´¨æ£€ | ç‹¬ç«‹èœå•,复用既有 OQC é”€å”®å‡ºåº“单检验单 |
| é”€å”®å¤æ£€ä¸Žå®¡æ‰¹ | è´¨é‡ç®¡ç† / é”€å”®è´¨æ£€ | ä¸åˆæ ¼å…ˆå¤æ£€ï¼Œé”€å”®éƒ¨ä¸Žå…¬å¸è´¨æ£€å®¡æ‰¹åŽå›žå†™å‡ºåº“质量状态 |
| ä¸åˆæ ¼å“ç•™ç—•评审 | è´¨é‡ç®¡ç† / ä¸åˆæ ¼å“å¤„理 | æ—¢æœ‰ OQC/IPQC è‡ªåŠ¨åˆ›å»º NCR |
| æ­£å¼å…¥åº“、出库、扫码追溯 | ä»“储物流 / å‡ºåº“流通 / è¿½æº¯ | å®¡æ‰¹æ”¾è¡ŒåŽè¢‹ç  `qc_status=20`;未放行不能扫码出库 |
| 20 å¾…初检审核 | åˆæ£€åˆæ ¼ | é€šè¿‡ â†’ 10;驳回 â†’ 50 |
| 25 å¾…初检不合格处理 | åˆæ£€ä¸åˆæ ¼ | ç¡®è®¤å¹¶å®‰æŽ’复检 â†’ 10;驳回原检验 â†’ 50 |
| 50 åˆæ£€é©³å›žå¾…重检 | åˆæ£€å®¡æ ¸é©³å›ž | é‡æ–°å½•入四项指标 â†’ 20 æˆ– 25 |
| 10 å¾…复检 | åˆæ£€å®¡æ ¸é€šè¿‡ï¼Œæˆ–复检审核驳回 | å½•入四项指标 â†’ 30 æˆ– 60 |
| 30 å¾…复检审核 | å¤æ£€åˆæ ¼ | é€šè¿‡ â†’ 40;驳回 â†’ 10 |
| 60 å¾…复检不合格处理 | å¤æ£€ä¸åˆæ ¼ | ç¡®è®¤æŠ¥åºŸ â†’ 70;驳回重新复检 â†’ 10 |
| 40 å·²æ”¾è¡Œ | å¤æ£€åˆæ ¼ä¸”审核通过 | è¢‹ç è½¬å…¥é€‰å®šåˆæ ¼åº“ |
| 70 å·²æŠ¥åºŸ | å¤æ£€ä¸åˆæ ¼ä¸”审核确认 | æš‚存库存扣减并报废袋码 |
## ç§å­è´¨æ£€çŠ¶æ€
初检与复检期间库存留在暂存库。只有最终审核通过才发生库存事务。不合格检验必须填写原因;处置人需填写审核意见。复检不合格确认时明确执行报废,驳回则退回复检。批量审核在同一事务内完成,任一袋库存流转失败则全部回滚。
- 10 å¾…复检:初检失败或审批驳回;库存仍在暂存库。
- 20 å¾…加工部审批:初检或复检合格。
- 30 å¾…公司质检审批:加工部同意放行。
- 40 å·²æ”¾è¡Œï¼šå…¬å¸è´¨æ£€åŒæ„ï¼Œäº‹åŠ¡æ€§åœ°å°†è¯¥è¢‹ç”±æš‚å­˜åº“ç§»å…¥åˆæ ¼åº“ã€‚
- 50 å¾…加工部不合格评审:复检不合格。
- 60 å¾…公司不合格审批:加工部同意报废。
- 70 å·²æŠ¥åºŸï¼šå…¬å¸è´¨æ£€åŒæ„ï¼Œäº‹åŠ¡æ€§åœ°ç§»é™¤è¯¥è¢‹æš‚å­˜åº“å­˜ã€‚
“质检审核人配置”仅维护种子质检的初检审核(节点 1)和复检审核(节点 3),支持多人和停用。服务端同时校验按钮权限与当前审核人配置;超级管理员可跳过审核人配置校验。原 process-approve / company-approve æŽ¥å£åŠæƒé™ç ä¿ç•™ï¼Œå…¼å®¹å·²åˆ†é…è§’色。
审批驳回回到 10。每次初检、复检和审批写入 `mes_qc_seed_quality_action`,包含操作人、时间、意见和检验指标快照。批量审批在同一数据库事务内执行,任一袋库存流转失败则全部回滚。
## éƒ¨ç½²
销售质检的流程状态与种子质检相同,其中 20/50 ç”±é”€å”®éƒ¨å®¡æ‰¹ï¼›40 è¡¨ç¤ºå‡ºè´§æ£€éªŒé€šè¿‡å¹¶å›žå†™é”€å”®å‡ºåº“单行,70 è¡¨ç¤ºä¸åˆæ ¼å®¡æ‰¹å®Œæˆå¹¶å›žå†™ä¸ºä¸åˆæ ¼ï¼ŒåŒæ—¶åˆ›å»º NCR。初检与复检不回写出库单质量结果,因此审批前仍处于待检状态。复检前后数量及审批意见写入 `mes_qc_oqc_action`。
## éƒ¨ç½²é¡ºåº
1. å¤‡ä»½æ•°æ®åº“,执行 `sql/mysql/mes_seed_quality_workflow_20261019.sql`。脚本是一次性迁移;历史已流转的合格/不合格记录分别标记为 40/70,避免重复库存事务。
2. éƒ¨ç½²åŽç«¯ã€å‰ç«¯ã€‚前端构建产物在 `mom-pro2-before/dist.zip`。
3. ä¸ºåŠ å·¥éƒ¨ä¸Žå…¬å¸è´¨æ£€è§’è‰²åˆ†åˆ«æŽˆäºˆ `mes:qc-seed-quality:process-approve`、`mes:qc-seed-quality:company-approve`;复检人员授予 `mes:qc-seed-quality:retest`。为销售部与公司质检角色分别授予 `mes:qc-oqc:sales-approve`、`mes:qc-oqc:company-approve`;销售复检人员授予 `mes:qc-oqc:retest`。核对“销售质检”菜单角色权限。
“销售质检”与原“出货检验”共用 OQC æ£€éªŒå•及既有出货检验表单,但有独立菜单与专门的复检、审批操作。原“过程检验(IPQC)”仍服务生产任务和报工,未并入按袋码流转的种子质检审批链。
1. å¤‡ä»½æ•°æ®åº“,执行 sql/mysql/mes_seed_quality_workflow_20261019.sql。历史已流转记录标为已放行或已报废,避免重复库存事务。
2. æ‰§è¡Œ sql/mysql/mes_qc_oqc_retest_approver_20261019.sql,创建审核人配置与菜单。该脚本仅涉及种子质检,不修改其他质检表。
3. æ‰§è¡Œ sql/mysql/mes_qc_seed_indicator_range_20261019.sql,创建范围模板表和页面菜单;为管理员分配 mes:qc-seed-range:query、mes:qc-seed-range:update æƒé™ã€‚建议配置完整四项默认范围。
4. éƒ¨ç½²åŽç«¯å’Œ mom-pro2-before å‰ç«¯ã€‚分配 mes:qc-seed-quality:retest、mes:qc-seed-quality:process-approve、mes:qc-seed-quality:company-approve æƒé™ï¼Œå¹¶åœ¨å®¡æ ¸äººé…ç½®ä¸­æŒ‡å®šä¸¤èŠ‚ç‚¹å®¡æ ¸äººã€‚
sql/mysql/mes_qc_oqc_retest_approver_20261019.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
-- è´¨æ£€å®¡æ ¸äººé…ç½®ï¼šä»…种子质检:按初检审核/复检审核节点维护,可配置多人。
CREATE TABLE IF NOT EXISTS mes_qc_approver_config (
  id bigint NOT NULL AUTO_INCREMENT,
  qc_type tinyint NOT NULL COMMENT '质检类型:1种子质检',
  approve_node tinyint NOT NULL COMMENT '审核节点:1初检审核 3复检审核',
  user_id bigint NOT NULL COMMENT '审核人用户ID',
  status tinyint NOT NULL DEFAULT 0 COMMENT '状态:0启用 1停用',
  remark varchar(255) DEFAULT NULL COMMENT '备注',
  creator varchar(64) DEFAULT '',
  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updater varchar(64) DEFAULT '',
  update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (id),
  KEY idx_qc_type_node (qc_type, approve_node)
) ENGINE=InnoDB COMMENT='质检审核人配置';
-- è´¨é‡ç®¡ç† / è´¨æ£€å®¡æ ¸äºº èœå•及按钮权限
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '质检审核人', '', 2, 9, 5500, 'approver-config', 'ep:user', 'mes/qc/approverConfig/index', 'MesQcApproverConfig', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'approver-config' AND deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '查询', 'mes:qc-approver:query', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'approver-config' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-approver:query' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '新增', 'mes:qc-approver:create', 3, 2, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'approver-config' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-approver:create' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '修改', 'mes:qc-approver:update', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'approver-config' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-approver:update' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '删除', 'mes:qc-approver:delete', 3, 4, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'approver-config' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-approver:delete' AND m.deleted = b'0');
sql/mysql/mes_qc_remove_sales_quality_20261019.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
-- ä¸‹çº¿é”€å”®è´¨æ£€ä¸šåŠ¡çº¿ï¼šé€»è¾‘åˆ é™¤â€œé”€å”®è´¨æ£€â€èœå•åŠæŒ‰é’®æƒé™ï¼Œæ¸…ç†é”€å”®è´¨æ£€å®¡æ ¸äººé…ç½®ã€‚
-- å‡ºè´§æ£€éªŒï¼ˆOQC)基础功能保留。
-- 1. åˆ é™¤é”€å”®è´¨æ£€æŒ‰é’®æƒé™
UPDATE system_menu SET deleted = b'1', update_time = NOW()
WHERE deleted = b'0'
  AND permission IN ('mes:qc-oqc:retest', 'mes:qc-oqc:sales-approve', 'mes:qc-oqc:company-approve');
-- 2. é€»è¾‘删除“销售质检”菜单
UPDATE system_menu SET deleted = b'1', update_time = NOW()
WHERE deleted = b'0' AND parent_id = 5500 AND path = 'sales-quality';
-- 3. æ¸…理角色与销售质检菜单的关联(system_role_menu æ—  deleted åˆ—,物理删除)
DELETE rm FROM system_role_menu rm
JOIN system_menu m ON rm.menu_id = m.id
WHERE m.permission IN ('mes:qc-oqc:retest', 'mes:qc-oqc:sales-approve', 'mes:qc-oqc:company-approve')
   OR (m.parent_id = 5500 AND m.path = 'sales-quality');
-- 4. é€»è¾‘删除销售质检审核人配置
UPDATE mes_qc_approver_config SET deleted = b'1', update_time = NOW()
WHERE deleted = b'0' AND qc_type = 2;
-- å¯é€‰ï¼šç¡®è®¤æ— åŽ†å²æ•°æ®åŽï¼Œå†æ¸…ç†é”€å”®è´¨æ£€é—ç•™çš„è¡¨ç»“æž„ä¸Žå­—æ®µ
-- DROP TABLE IF EXISTS mes_qc_oqc_action;
-- ALTER TABLE mes_qc_oqc
--   DROP COLUMN workflow_status,
--   DROP COLUMN inspection_round,
--   DROP COLUMN retest_of_id;
sql/mysql/mes_qc_seed_indicator_range_20261019.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
-- ç§å­è´¨æ£€å››é¡¹åˆæ ¼èŒƒå›´æ¨¡æ¿ã€‚初检与复检可修改本次范围,按实际提交值判定。
CREATE TABLE IF NOT EXISTS mes_qc_seed_indicator_range (
  indicator_code varchar(32) NOT NULL COMMENT 'purity/cleanliness/germination_rate/moisture',
  min_value decimal(18,6) NOT NULL COMMENT '合格下限,含边界',
  max_value decimal(18,6) NOT NULL COMMENT '合格上限,含边界',
  creator varchar(64) DEFAULT '',
  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updater varchar(64) DEFAULT '',
  update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (indicator_code)
) ENGINE=InnoDB COMMENT='种子质检项合格范围配置';
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '质检项合格范围配置', '', 2, 10, 5500, 'seed-range', 'ep:data-analysis', 'mes/qc/seedRange/index', 'MesQcSeedRange', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'seed-range' AND deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '查询', 'mes:qc-seed-range:query', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-range' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-range:query' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '修改', 'mes:qc-seed-range:update', 3, 2, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-range' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-range:update' AND m.deleted = b'0');
sql/mysql/mes_seed_quality_workflow_20261019.sql
@@ -1,7 +1,7 @@
-- ç”Ÿäº§è´¨æ£€ï¼šåˆæ£€ã€å¤æ£€ã€åŠ å·¥éƒ¨å®¡æ‰¹ã€å…¬å¸è´¨æ£€å®¡æ‰¹åŽæ‰æ”¾è¡Œåº“å­˜ã€‚
-- ç”Ÿäº§è´¨æ£€ï¼šåˆæ£€å®¡æ ¸ã€å¤æ£€ã€å¤æ£€å®¡æ ¸åŽæ‰æ”¾è¡Œæˆ–处置库存。
-- å…ˆéƒ¨ç½²æ•°æ®åº“变更,再部署对应后端与前端;历史合格记录已完成库存流转。
ALTER TABLE mes_qc_seed_quality
  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 40 COMMENT '10待复检 20待加工部审批 30待公司质检审批 40已放行 50待加工部不合格评审 60待公司不合格审批 70已报废',
  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 40 COMMENT '10待复检 20待初检审核 25待初检不合格处理 30待复检审核 40已放行 50初检驳回待重检 60待复检不合格处理 70已报废',
  ADD COLUMN qualified_warehouse_id bigint DEFAULT NULL COMMENT '审批通过后的目标合格库',
  ADD COLUMN retest_result tinyint DEFAULT NULL COMMENT '复检结果 1合格 2不合格',
  ADD COLUMN retested_at datetime DEFAULT NULL COMMENT '复检时间',
@@ -30,49 +30,10 @@
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:retest' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '加工部审批', 'mes:qc-seed-quality:process-approve', 3, 4, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
SELECT '初检审核', 'mes:qc-seed-quality:process-approve', 3, 4, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:process-approve' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '公司质检审批', 'mes:qc-seed-quality:company-approve', 3, 5, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
SELECT '复检审核', 'mes:qc-seed-quality:company-approve', 3, 5, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:company-approve' AND m.deleted = b'0');
-- ç‹¬ç«‹é”€å”®è´¨æ£€å…¥å£ï¼›ä¿ç•™åŽŸâ€œå‡ºè´§æ£€éªŒâ€èœå•ï¼Œå¤ç”¨å…¶å·²å®žçŽ°çš„é”€å”®å‡ºåº“å•æ£€éªŒèƒ½åŠ›ã€‚
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售质检', '', 2, 8, 5500, 'sales-quality', 'ep:document-checked', 'mes/qc/salesQuality/index', 'MesQcSalesQuality', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0');
-- é”€å”®è´¨æ£€å¤æ£€åŠä¸¤çº§å®¡æ‰¹ï¼›åŽ†å²å·²å®Œæˆå‡ºè´§æ£€éªŒå•ç»´æŒæ—¢æœ‰æ”¾è¡Œç»“æžœã€‚
ALTER TABLE mes_qc_oqc
  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 10待复检 20待销售部审批 30待公司质检审批 40已放行 50待销售部不合格评审 60待公司不合格审批 70已拒绝出货';
CREATE INDEX idx_mes_oqc_workflow ON mes_qc_oqc (workflow_status, id);
UPDATE mes_qc_oqc SET workflow_status = CASE
  WHEN status = 0 THEN 0
  WHEN check_result = 1 THEN 40
  ELSE 70 END;
CREATE TABLE IF NOT EXISTS mes_qc_oqc_action (
  id bigint NOT NULL AUTO_INCREMENT,
  oqc_id bigint NOT NULL,
  action varchar(32) NOT NULL,
  result tinyint DEFAULT NULL,
  opinion varchar(500) DEFAULT NULL,
  quantities_json text DEFAULT NULL,
  operator_id bigint DEFAULT NULL,
  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_oqc_time (oqc_id, create_time)
) ENGINE=InnoDB COMMENT='销售质检复检和审批留痕';
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售复检', 'mes:qc-oqc:retest', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:retest' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售部审批', 'mes:qc-oqc:sales-approve', 3, 2, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:sales-approve' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售公司质检审批', 'mes:qc-oqc:company-approve', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:company-approve' AND m.deleted = b'0');
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
@@ -164,6 +164,13 @@
        return success(new PageResult<>(buildBagCodeRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/unbound-ton-bags")
    @Operation(summary = "获得可绑定生产批次的吨袋")
    @PreAuthorize("@ss.hasAnyPermissions('mes:pro-batch:create', 'mes:pro-batch:update', 'mes:pro-bag-code:query')")
    public CommonResult<List<MesProBagCodeRespVO>> getUnboundTonBags() {
        return success(buildBagCodeRespVOList(bagCodeService.getUnboundTonBags()));
    }
    @GetMapping("/list-by-uuid")
    @Operation(summary = "获得生成组的袋码列表")
    @Parameter(name = "uuid", description = "生成组 UUID", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
@@ -148,6 +148,7 @@
            return success(null);
        }
        MesProBatchRespVO respVO = buildBatchRespVOList(Collections.singletonList(batch)).get(0);
        respVO.setBindingMode(batchService.getBindingMode(id));
        // å¡«å……人员配置
        respVO.setWorkers(buildWorkerRespVOList(batchService.getBatchWorkerListByBatchId(id)));
        return success(respVO);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java
@@ -34,6 +34,9 @@
    private String tempCode;
    @ExcelIgnore
    private Long itemId;
    @Schema(description = "已有绑定方式:pallet æˆ– tonBag")
    @ExcelIgnore
    private String bindingMode;
    @ExcelProperty("品种编码")
    private String itemCode;
    @ExcelProperty("品种名称")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java
@@ -20,7 +20,7 @@
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "品种编号,由绑定托盘带入并经后端校验")
    @Schema(description = "品种编号,由绑定托盘或吨袋带入并经后端校验")
    private Long itemId;
    @Schema(description = "产线编号(页面展示为「加工单位」)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -54,6 +54,9 @@
    @Schema(description = "绑定托盘编号列表(可选;绑定时批次直接进入生产中,托盘下袋码级联录入本批次;新增/补录/编辑均可传入,编辑时仅需传本次新增勾选的托盘)")
    private List<Long> palletIds;
    @Schema(description = "绑定吨袋编号列表(仅计量单位为吨且无托盘的袋码;与绑定托盘互斥)")
    private List<Long> tonBagIds;
    @Schema(description = "生产人员配置")
    @Data
    public static class Worker {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/MesQcApproverConfigController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.approver;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.approver.MesQcApproverConfigDO;
import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
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 è´¨æ£€å®¡æ ¸äººé…ç½®")
@RestController
@RequestMapping("/mes/qc/approver-config")
@Validated
public class MesQcApproverConfigController {
    @Resource
    private MesQcApproverService approverService;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建质检审核人配置")
    @PreAuthorize("@ss.hasPermission('mes:qc-approver:create')")
    public CommonResult<Long> createConfig(@Valid @RequestBody MesQcApproverConfigSaveReqVO reqVO) {
        return success(approverService.createConfig(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新质检审核人配置")
    @PreAuthorize("@ss.hasPermission('mes:qc-approver:update')")
    public CommonResult<Boolean> updateConfig(@Valid @RequestBody MesQcApproverConfigSaveReqVO reqVO) {
        approverService.updateConfig(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除质检审核人配置")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:qc-approver:delete')")
    public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
        approverService.deleteConfig(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得质检审核人配置")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:qc-approver:query')")
    public CommonResult<MesQcApproverConfigRespVO> getConfig(@RequestParam("id") Long id) {
        MesQcApproverConfigDO config = approverService.getConfig(id);
        return success(buildRespVOList(List.of(config)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得质检审核人配置分页")
    @PreAuthorize("@ss.hasPermission('mes:qc-approver:query')")
    public CommonResult<PageResult<MesQcApproverConfigRespVO>> getConfigPage(@Valid MesQcApproverConfigPageReqVO pageReqVO) {
        PageResult<MesQcApproverConfigDO> pageResult = approverService.getConfigPage(pageReqVO);
        return success(new PageResult<>(buildRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    private List<MesQcApproverConfigRespVO> buildRespVOList(List<MesQcApproverConfigDO> list) {
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcApproverConfigDO::getUserId));
        return BeanUtils.toBean(list, MesQcApproverConfigRespVO.class, vo ->
                findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname())));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - è´¨æ£€å®¡æ ¸äººé…ç½®åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesQcApproverConfigPageReqVO extends PageParam {
    @Schema(description = "质检类型:1种子质检", example = "1")
    private Integer qcType;
    @Schema(description = "审核节点:1加工部审批 3公司质检审批", example = "3")
    private Integer approveNode;
    @Schema(description = "审核人用户 ID", example = "1024")
    private Long userId;
    @Schema(description = "状态:0启用 1停用", example = "0")
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - è´¨æ£€å®¡æ ¸äººé…ç½® Response VO")
@Data
public class MesQcApproverConfigRespVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "质检类型:1种子质检", example = "1")
    private Integer qcType;
    @Schema(description = "审核节点:1加工部审批 3公司质检审批", example = "3")
    private Integer approveNode;
    @Schema(description = "审核人用户 ID", example = "1024")
    private Long userId;
    @Schema(description = "审核人昵称", example = "张三")
    private String userNickname;
    @Schema(description = "状态:0启用 1停用", example = "0")
    private Integer status;
    @Schema(description = "备注", example = "负责种子质检公司审批")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/approver/vo/MesQcApproverConfigSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - è´¨æ£€å®¡æ ¸äººé…ç½®æ–°å¢ž/修改 Request VO")
@Data
public class MesQcApproverConfigSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "质检类型:1种子质检", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "质检类型不能为空")
    private Integer qcType;
    @Schema(description = "审核节点:1加工部审批 3公司质检审批", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    @NotNull(message = "审核节点不能为空")
    private Integer approveNode;
    @Schema(description = "审核人用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "审核人不能为空")
    private Long userId;
    @Schema(description = "状态:0启用 1停用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注", example = "负责种子质检公司审批")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -10,8 +10,6 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcApprovalReqVO;
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;
@@ -83,30 +81,6 @@
    public CommonResult<Boolean> finishOqc(@RequestParam("id") Long id) {
        oqcService.finishOqc(id);
        return success(true);
    }
    @PostMapping("/retest") @Operation(summary = "销售质检复检")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:retest')")
    public CommonResult<Boolean> retestOqc(@Valid @RequestBody MesQcOqcRetestReqVO reqVO) {
        oqcService.retestOqc(reqVO);
        return success(true);
    }
    @PostMapping("/sales-approve") @Operation(summary = "销售部审批")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:sales-approve')")
    public CommonResult<Boolean> salesApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
        oqcService.salesApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve") @Operation(summary = "公司质检审批销售出货")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:company-approve')")
    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
        oqcService.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @GetMapping("/actions") @Operation(summary = "销售质检流程记录")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:query')")
    public CommonResult<List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO>> actions(@RequestParam Long id) {
        return success(oqcService.getActions(id));
    }
    @DeleteMapping("/delete")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
@@ -26,7 +26,5 @@
    @Schema(description = "检测结果", example = "1")
    private Integer checkResult;
    @Schema(description = "来源单据类型")
    private Integer sourceDocType;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -142,7 +142,6 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty("状态")
    private Integer status;
    private Integer workflowStatus;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -9,7 +9,13 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityApprovalReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchApprovalReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedIndicatorRangeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedIndicatorRangeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -17,12 +23,31 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES ç§å­è´¨æ£€")
@RestController @RequestMapping("/mes/qc/seed-quality") @Validated
public class MesQcSeedQualityController {
    @Resource private MesQcSeedQualityService service;
    @Resource private MesQcSeedIndicatorRangeService rangeService;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @GetMapping("/ranges") @Operation(summary = "获得种子质检合格范围")
    @PreAuthorize("@ss.hasAnyPermissions('mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', 'mes:qc-seed-range:query')")
    public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedIndicatorRangeDO>> ranges() {
        return success(rangeService.getRanges());
    }
    @PutMapping("/ranges") @Operation(summary = "配置种子质检合格范围")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-range:update')")
    public CommonResult<Boolean> saveRanges(@Valid @RequestBody MesQcSeedIndicatorRangeSaveReqVO reqVO) {
        rangeService.saveRanges(reqVO);
        return success(true);
    }
    @GetMapping("/resolve")
    @Operation(summary = "解析待质检袋码及确认该袋质检结果")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
@@ -33,7 +58,19 @@
    @Operation(summary = "获得种子质检记录分页")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
    public CommonResult<PageResult<MesQcSeedQualityRespVO>> getQualityPage(@Valid MesQcSeedQualityPageReqVO pageReqVO) {
        return success(BeanUtils.toBean(service.getQualityPage(pageReqVO), MesQcSeedQualityRespVO.class));
        PageResult<MesQcSeedQualityRespVO> page = BeanUtils.toBean(
                service.getQualityPage(pageReqVO), MesQcSeedQualityRespVO.class);
        List<String> codes = page.getList().stream().map(MesQcSeedQualityRespVO::getCode)
                .filter(java.util.Objects::nonNull).distinct().toList();
        if (!codes.isEmpty()) {
            Map<String, MesProBagCodeDO> bagMap = bagCodeMapper.selectList(MesProBagCodeDO::getCode, codes).stream()
                    .collect(Collectors.toMap(MesProBagCodeDO::getCode, Function.identity(), (first, ignored) -> first));
            page.getList().forEach(record -> {
                MesProBagCodeDO bag = bagMap.get(record.getCode());
                if (bag != null) record.setPalletCode(bag.getPalletCode());
            });
        }
        return success(page);
    }
    @GetMapping("/indicators") @Operation(summary = "获得种子质检最新指标")
@@ -64,26 +101,45 @@
        return success(true);
    }
    @PostMapping("/process-approve") @Operation(summary = "加工部审批")
    @PostMapping("/retest-batch") @Operation(summary = "同批次种子批量复检或重新初检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
    public CommonResult<Boolean> retestBatch(@Valid @RequestBody MesQcSeedQualityBatchRetestReqVO reqVO) {
        service.retestBatch(reqVO);
        return success(true);
    }
    @GetMapping("/retest-scope/resolve") @Operation(summary = "按托盘码或生产批次码解析待批量质检记录")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
    public CommonResult<java.util.Map<String, Object>> resolveRetestScope(@RequestParam String code, @RequestParam String scanType) {
        return success(service.resolveRetestScope(code, scanType));
    }
    @PostMapping("/retest-scope") @Operation(summary = "按托盘码或生产批次码批量复检或重新初检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
    public CommonResult<java.util.Map<String, Object>> retestScope(@Valid @RequestBody MesQcSeedQualityScopeRetestReqVO reqVO) {
        return success(service.retestScope(reqVO));
    }
    @PostMapping("/process-approve") @Operation(summary = "初检审核")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
    public CommonResult<Boolean> processApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
        service.processApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve") @Operation(summary = "公司质检审批并流转库存")
    @PostMapping("/company-approve") @Operation(summary = "复检审核并流转库存")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
        service.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/process-approve-batch") @Operation(summary = "批量加工部审批")
    @PostMapping("/process-approve-batch") @Operation(summary = "批量初检审核")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
    public CommonResult<Boolean> processApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
        service.processApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve-batch") @Operation(summary = "批量公司质检审批并流转库存")
    @PostMapping("/company-approve-batch") @Operation(summary = "批量复检审核并流转库存")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
    public CommonResult<Boolean> companyApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
        service.companyApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedIndicatorRangeSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class MesQcSeedIndicatorRangeSaveReqVO {
    @NotEmpty
    private List<@Valid Item> ranges;
    @Data
    public static class Item {
        @NotBlank private String indicatorCode;
        @NotNull @DecimalMin("0") @DecimalMax("100") private BigDecimal minValue;
        @NotNull @DecimalMin("0") @DecimalMax("100") private BigDecimal maxValue;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -8,5 +7,5 @@
public class MesQcSeedQualityApprovalReqVO {
    @NotNull private Long id;
    @NotNull private Boolean approved;
    @NotBlank private String opinion;
    private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -11,5 +10,5 @@
public class MesQcSeedQualityBatchApprovalReqVO {
    @NotEmpty private List<@NotNull Long> ids;
    @NotNull private Boolean approved;
    @NotBlank private String opinion;
    private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchRetestReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class MesQcSeedQualityBatchRetestReqVO {
    @NotEmpty private List<@NotNull Long> ids;
    @NotEmpty @Valid private List<MesQcSeedQualitySubmitReqVO.Indicator> indicators;
    private String reason;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
@@ -11,6 +11,8 @@
    private Long id;
    private String code;
    @Schema(description = "对应托盘码;未绑定托盘时为空")
    private String palletCode;
    private Long itemId;
    private Long batchId;
    private String batchCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityScopeRetestReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class MesQcSeedQualityScopeRetestReqVO {
    @NotBlank private String code;
    @NotBlank private String scanType;
    @NotNull private Integer workflowStatus;
    @NotEmpty private List<@NotNull Long> qualityIds;
    @NotEmpty @Valid private List<MesQcSeedQualitySubmitReqVO.Indicator> indicators;
    private String reason;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java
@@ -16,8 +16,8 @@
    @NotNull private Long qualifiedWarehouseId;
    @Data public static class Indicator {
        @NotBlank private String code;
        @NotNull @DecimalMin("0") private BigDecimal value;
        @NotNull @DecimalMin("0") private BigDecimal minValue;
        @NotNull @DecimalMin("0") private BigDecimal maxValue;
        @NotNull @DecimalMin("0") @DecimalMax("100") private BigDecimal value;
        @NotNull @DecimalMin("0") @DecimalMax("100") private BigDecimal minValue;
        @NotNull @DecimalMin("0") @DecimalMax("100") private BigDecimal maxValue;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/approver/MesQcApproverConfigDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.approver;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApproveNodeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApprovalTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * MES è´¨æ£€å®¡æ ¸äººé…ç½® DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_qc_approver_config")
@KeySequence("mes_qc_approver_config_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesQcApproverConfigDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * è´¨æ£€ç±»åž‹
     *
     * æžšä¸¾ {@link MesQcApprovalTypeEnum}
     */
    private Integer qcType;
    /**
     * å®¡æ ¸èŠ‚ç‚¹
     *
     * æžšä¸¾ {@link MesQcApproveNodeEnum}
     */
    private Integer approveNode;
    /**
     * å®¡æ ¸äººç”¨æˆ· ID
     */
    private Long userId;
    /**
     * çŠ¶æ€ï¼š0 å¯ç”¨ï¼Œ1 åœç”¨
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -181,8 +181,6 @@
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum}
     */
    private Integer status;
    /** 0草稿;10待复检;20待销售部审批;30待公司质检审批;40已放行;50待销售部不合格评审;60待公司不合格审批;70已拒绝出货。 */
    private Integer workflowStatus;
    /**
     * å¤‡æ³¨
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedIndicatorRangeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@TableName("mes_qc_seed_indicator_range")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesQcSeedIndicatorRangeDO extends BaseDO {
    @TableId(type = IdType.INPUT)
    private String indicatorCode;
    private BigDecimal minValue;
    private BigDecimal maxValue;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
@@ -17,7 +17,7 @@
    private String batchCode;
    private Integer result;
    private String reason;
    /** 10 å¾…复检;20 å¾…加工部审批;30 å¾…公司质检审批;40 å·²æ”¾è¡Œï¼›50 å¾…加工部不合格评审;60 å¾…公司不合格审批;70 å·²æŠ¥åºŸã€‚ */
    /** 10 å¾…复检;20 å¾…初检审核;25 å¾…初检不合格处理;30 å¾…复检审核;40 å·²æ”¾è¡Œï¼›50 åˆæ£€é©³å›žå¾…重检;60 å¾…复检不合格处理;70 å·²æŠ¥åºŸã€‚ */
    private Integer workflowStatus;
    private Long qualifiedWarehouseId;
    private Integer retestResult;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java
@@ -40,10 +40,6 @@
        return selectOne(MesMdItemDO::getCode, code);
    }
    default MesMdItemDO selectByName(String name) {
        return selectOne(MesMdItemDO::getName, name);
    }
    default Long selectCountByItemTypeId(Long itemTypeId) {
        return selectCount(MesMdItemDO::getItemTypeId, itemTypeId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
@@ -260,6 +260,16 @@
                .orderByAsc(MesProBagCodeDO::getId));
    }
    default List<MesProBagCodeDO> selectUnboundPalletlessList() {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .isNull(MesProBagCodeDO::getBatchId)
                .isNull(MesProBagCodeDO::getPalletId)
                .in(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.GENERATED.getStatus(),
                        MesProBagCodeStatusEnum.PRINTED.getStatus())
                .isNotNull(MesProBagCodeDO::getItemId)
                .orderByAsc(MesProBagCodeDO::getId));
    }
    /** æ‰¹æ¬¡å†…有效袋码的品种编号集合(忽略历史空品种袋码),用于录入批次时的品种一致性校验 */
    @Select("SELECT DISTINCT item_id FROM mes_pro_bag_code " +
            "WHERE deleted = 0 AND status <> 30 AND batch_id = #{batchId} AND item_id IS NOT NULL")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/approver/MesQcApproverConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.approver;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.approver.MesQcApproverConfigDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES è´¨æ£€å®¡æ ¸äººé…ç½® Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesQcApproverConfigMapper extends BaseMapperX<MesQcApproverConfigDO> {
    default PageResult<MesQcApproverConfigDO> selectPage(MesQcApproverConfigPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesQcApproverConfigDO>()
                .eqIfPresent(MesQcApproverConfigDO::getQcType, reqVO.getQcType())
                .eqIfPresent(MesQcApproverConfigDO::getApproveNode, reqVO.getApproveNode())
                .eqIfPresent(MesQcApproverConfigDO::getUserId, reqVO.getUserId())
                .eqIfPresent(MesQcApproverConfigDO::getStatus, reqVO.getStatus())
                .orderByAsc(MesQcApproverConfigDO::getQcType)
                .orderByAsc(MesQcApproverConfigDO::getApproveNode)
                .orderByDesc(MesQcApproverConfigDO::getId));
    }
    default MesQcApproverConfigDO selectByUnique(Integer qcType, Integer approveNode, Long userId) {
        return selectOne(new LambdaQueryWrapperX<MesQcApproverConfigDO>()
                .eq(MesQcApproverConfigDO::getQcType, qcType)
                .eq(MesQcApproverConfigDO::getApproveNode, approveNode)
                .eq(MesQcApproverConfigDO::getUserId, userId));
    }
    default Long selectCountByNode(Integer qcType, Integer approveNode, Integer status) {
        return selectCount(new LambdaQueryWrapperX<MesQcApproverConfigDO>()
                .eq(MesQcApproverConfigDO::getQcType, qcType)
                .eq(MesQcApproverConfigDO::getApproveNode, approveNode)
                .eq(MesQcApproverConfigDO::getStatus, status));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -25,7 +25,6 @@
                .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                .orderByDesc(MesQcOqcDO::getId));
    }
@@ -39,7 +38,6 @@
                .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                .orderByDesc(MesQcOqcDO::getId);
        if (permittedClientIds != null) {
            if (CollUtil.isEmpty(permittedClientIds)) {
@@ -52,9 +50,6 @@
    default MesQcOqcDO selectByCode(String code) {
        return selectOne(MesQcOqcDO::getCode, code);
    }
    default MesQcOqcDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesQcOqcDO>().eq(MesQcOqcDO::getId, id).last("FOR UPDATE"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedIndicatorRangeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedIndicatorRangeDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesQcSeedIndicatorRangeMapper extends BaseMapperX<MesQcSeedIndicatorRangeDO> {
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -41,7 +41,6 @@
    // ========== MES åŸºç¡€æ•°æ®-物料(1-040-102-000) ==========
    ErrorCode MD_ITEM_NOT_EXISTS = new ErrorCode(1_040_102_000, "物料不存在");
    ErrorCode MD_ITEM_CODE_DUPLICATE = new ErrorCode(1_040_102_001, "物料编码已存在");
    ErrorCode MD_ITEM_NAME_DUPLICATE = new ErrorCode(1_040_102_002, "物料名称已存在");
    ErrorCode MD_ITEM_BATCH_REQUIRED = new ErrorCode(1_040_102_004, "当前物料启用了批次管理,请选择批次");
    ErrorCode MD_ITEM_IS_DISABLE = new ErrorCode(1_040_102_005, "物料已禁用");
@@ -447,6 +446,12 @@
    ErrorCode QC_NCR_NOT_REVIEWED = new ErrorCode(1_040_609_003, "只有已评审状态的不合格品处理单才可执行处置");
    ErrorCode QC_NCR_NOT_DISPOSED = new ErrorCode(1_040_609_004, "只有已处置状态的不合格品处理单才可关闭");
    // ========== MES è´¨é‡ç®¡ç†-质检审核人配置(1-040-610-000) ==========
    ErrorCode QC_APPROVER_CONFIG_NOT_EXISTS = new ErrorCode(1_040_610_000, "质检审核人配置不存在");
    ErrorCode QC_APPROVER_CONFIG_DUPLICATE = new ErrorCode(1_040_610_001, "该质检类型和审核节点下已存在该审核人");
    ErrorCode QC_APPROVER_NODE_INVALID = new ErrorCode(1_040_610_002, "质检类型与审核节点组合不合法");
    ErrorCode QC_APPROVER_NOT_CONFIGURED = new ErrorCode(1_040_610_003, "当前用户不是该节点的质检审核人,请联系管理员配置");
    // ========== MES ä»“库管理-仓库(1-040-700-000) ==========
    ErrorCode WM_WAREHOUSE_NOT_EXISTS = new ErrorCode(1_040_700_000, "仓库不存在");
    ErrorCode WM_WAREHOUSE_CODE_DUPLICATE = new ErrorCode(1_040_700_001, "仓库编码已存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcApprovalTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
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 è´¨æ£€å®¡æ ¸ç±»åž‹æžšä¸¾ï¼ˆç”¨äºŽå®¡æ ¸äººé…ç½®ï¼‰
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesQcApprovalTypeEnum implements ArrayValuable<Integer> {
    SEED_QUALITY(1, "种子质检");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcApprovalTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹å€¼
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcApproveNodeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
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;
import java.util.Objects;
import java.util.Set;
/**
 * MES è´¨æ£€å®¡æ ¸èŠ‚ç‚¹æžšä¸¾ï¼ˆç”¨äºŽå®¡æ ¸äººé…ç½®ï¼‰
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesQcApproveNodeEnum implements ArrayValuable<Integer> {
    PROCESS(1, "初检审核"),
    COMPANY(3, "复检审核");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcApproveNodeEnum::getNode).toArray(Integer[]::new);
    /**
     * èŠ‚ç‚¹å€¼
     */
    private final Integer node;
    /**
     * èŠ‚ç‚¹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    /**
     * æ ¡éªŒè´¨æ£€ç±»åž‹ä¸Žå®¡æ ¸èŠ‚ç‚¹çš„ç»„åˆæ˜¯å¦åˆæ³•
     */
    public static boolean isValidCombination(Integer qcType, Integer node) {
        return Objects.equals(qcType, MesQcApprovalTypeEnum.SEED_QUALITY.getType())
                && Set.of(PROCESS.getNode(), COMPANY.getNode()).contains(node);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -135,8 +135,6 @@
    private void validateItemSaveData(MesMdItemSaveReqVO reqVO) {
        // æ ¡éªŒç‰©æ–™ç¼–码的唯一性
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒç‰©æ–™åç§°çš„唯一性
        validateItemNameUnique(reqVO.getId(), reqVO.getName());
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨ï¼ˆMDM å“ç§åŒæ­¥ä¸ä¼ åˆ†ç±»ï¼Œå…è®¸ä¸ºç©ºï¼‰
        if (reqVO.getItemTypeId() != null) {
            validateItemTypeExists(reqVO.getItemTypeId());
@@ -210,16 +208,6 @@
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
            throw exception(MD_ITEM_CODE_DUPLICATE);
        }
    }
    private void validateItemNameUnique(Long id, String name) {
        MesMdItemDO item = itemMapper.selectByName(name);
        if (item == null) {
            return;
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
            throw exception(MD_ITEM_NAME_DUPLICATE);
        }
    }
@@ -421,4 +409,4 @@
        return item;
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
@@ -43,6 +43,9 @@
    void bindBatch(@Valid MesProBagCodeBindBatchReqVO bindReqVO);
    /** å°šæœªç»‘定批次、没有托盘且品种计量单位为吨的袋码。 */
    List<MesProBagCodeDO> getUnboundTonBags();
    /**
     * ä½œåºŸç ´æŸè¢‹ç å¹¶åœ¨åŽŸæ‰¹æ¬¡ç”Ÿæˆç­‰é‡æ›¿æ¢è¢‹ç 
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -209,6 +209,20 @@
        return type;
    }
    @Override
    public List<MesProBagCodeDO> getUnboundTonBags() {
        Map<Long, Boolean> tonItems = new LinkedHashMap<>();
        return bagCodeMapper.selectUnboundPalletlessList().stream()
                .filter(bag -> tonItems.computeIfAbsent(bag.getItemId(), id -> {
                    MesMdmItemDO item = mdmItemService.getItem(id);
                    if (item == null || item.getUnitMeasureId() == null) return false;
                    CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(item.getUnitMeasureId());
                    return result != null && result.isSuccess() && result.getData() != null
                            && UNIT_TYPE_TON.equals(StrUtil.trim(result.getData().getType()));
                }))
                .toList();
    }
    /**
     * è§£æžã€ŒN袋」类型得出每托盘袋数
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchService.java
@@ -71,6 +71,9 @@
     */
    MesProBatchDO getBatch(Long id);
    /** æ‰¹æ¬¡å·²æœ‰çš„绑定方式:pallet、tonBag æˆ–空。 */
    String getBindingMode(Long id);
    /**
     * æ ¡éªŒç”Ÿäº§æ‰¹æ¬¡å­˜åœ¨
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -7,6 +7,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindBatchReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
@@ -20,6 +24,8 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -61,6 +67,12 @@
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesProBagCodeService bagCodeService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
@@ -86,7 +98,7 @@
        String code = generateBatchCode(line.getCode());
        // 3. æ’入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds());
        boolean hasBindings = hasBindings(createReqVO);
        MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class)
                .setCode(code)
                .setBusinessCode(codeRedisDao.generate())
@@ -94,7 +106,7 @@
                .setLineName(line.getName())
                .setBagQuantity(0)
                .setPalletQuantity(0)
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                .setStatus(hasBindings ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.insert(batch);
@@ -102,6 +114,7 @@
        insertBatchWorkers(batch.getId(), createReqVO.getWorkers());
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(batch.getId(), createReqVO.getPalletIds());
        bindTonBagsToBatch(batch.getId(), createReqVO.getTonBagIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦
        integrityService.recordBatchIntegrity(batch.getId());
        return batch.getId();
@@ -145,12 +158,12 @@
        fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO));
        String code = generateBatchCode(line.getCode());
        // 3. è½åº“业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds());
        boolean hasBindings = hasBindings(fillReqVO);
        MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                .setStatus(hasBindings ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.updateById(updateObj);
        // 4. é‡å»ºç”Ÿäº§äººå‘˜é…ç½®
@@ -158,6 +171,7 @@
        insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers());
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds());
        bindTonBagsToBatch(fillReqVO.getId(), fillReqVO.getTonBagIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦ï¼ˆè¡¥å½•正式信息后覆盖)
        integrityService.recordBatchIntegrity(fillReqVO.getId());
    }
@@ -187,9 +201,9 @@
            updateObj.setLineCode(line.getCode()).setLineName(line.getName());
        }
        // 4. æ›´æ–°æ‰¹æ¬¡ï¼ˆå‹¾é€‰ç»‘定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑)
        boolean bindPallets = CollUtil.isNotEmpty(updateReqVO.getPalletIds());
        boolean hasBindings = hasBindings(updateReqVO);
        updateObj.setCode(code);
        if (bindPallets) {
        if (hasBindings) {
            updateObj.setStatus(MesProBatchStatusEnum.PRODUCING.getStatus());
        }
        batchMapper.updateById(updateObj);
@@ -199,6 +213,7 @@
        insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers());
        // 6. ç»‘定新增勾选的托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds());
        bindTonBagsToBatch(updateReqVO.getId(), updateReqVO.getTonBagIds());
        // 7. æ‘˜è¦è¦†ç›–范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        integrityService.recordBatchIntegrity(updateReqVO.getId());
    }
@@ -252,6 +267,18 @@
    @Override
    public MesProBatchDO getBatch(Long id) {
        return batchMapper.selectById(id);
    }
    @Override
    public String getBindingMode(Long id) {
        if (!palletMapper.selectListByBatchId(id).isEmpty()) return "pallet";
        return bagCodeMapper.selectListByBatchId(id).stream()
                .filter(bag -> bag.getPalletId() == null)
                .map(MesProBagCodeDO::getItemId)
                .filter(Objects::nonNull)
                .distinct()
                .anyMatch(this::isTonItem)
                ? "tonBag" : null;
    }
    @Override
@@ -344,6 +371,15 @@
     * ç”Ÿæˆæ­£å¼æ‰˜ç›˜ç å¹¶çº§è”录入托盘下未绑批次的袋码,失败整体回滚
     */
    private Long resolveBindingItem(MesProBatchDO batch, MesProBatchSaveReqVO req) {
        boolean palletsRequested = CollUtil.isNotEmpty(req.getPalletIds());
        boolean tonsRequested = CollUtil.isNotEmpty(req.getTonBagIds());
        if (palletsRequested && tonsRequested) throw bindingError("绑定托盘和绑定吨袋只能选择一种");
        if (batch != null && (palletsRequested || tonsRequested)) {
            String existingMode = getBindingMode(batch.getId());
            if ((tonsRequested && "pallet".equals(existingMode))
                    || (palletsRequested && "tonBag".equals(existingMode)))
                throw bindingError("该批次已绑定另一种码,绑定托盘和绑定吨袋只能选择一种");
        }
        Long itemId = batch == null ? null : batch.getItemId();
        if (batch != null) {
            for (Long existing : bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()))
@@ -359,12 +395,48 @@
            if (candidate == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "托盘缺少品种,请先绑定同品种袋码");
            itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, candidate);
        }
        // è¯·æ±‚里的品种只能与已绑定/待绑定托盘一致,不能通过编辑改变批次品种。
        if (tonsRequested) {
            if (req.getTonBagIds().contains(null)) throw bindingError("请选择有效的吨袋");
            List<Long> ids = req.getTonBagIds().stream().distinct().sorted().toList();
            List<MesProBagCodeDO> bags = bagCodeMapper.selectListByIdsForUpdate(ids);
            if (bags.size() != ids.size()) throw bindingError("所选吨袋不存在");
            for (MesProBagCodeDO bag : bags) {
                if (bag.getBatchId() != null || bag.getPalletId() != null || !isTonItem(bag.getItemId()))
                    throw bindingError("只能绑定未录入批次、没有托盘且计量单位为吨的袋码");
                itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, bag.getItemId());
            }
        }
        // è¯·æ±‚里的品种只能与已绑定或待绑定的托盘/吨袋一致,不能通过编辑改变批次品种。
        if (req.getItemId() != null && !Objects.equals(req.getItemId(), itemId))
            throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "品种由绑定托盘带入,不可修改为其他品种");
            throw bindingError("品种由绑定托盘或吨袋带入,不可修改为其他品种");
        return itemId;
    }
    private boolean hasBindings(MesProBatchSaveReqVO req) {
        return CollUtil.isNotEmpty(req.getPalletIds()) || CollUtil.isNotEmpty(req.getTonBagIds());
    }
    private boolean isTonItem(Long itemId) {
        if (itemId == null) return false;
        MesMdmItemDO item = mdmItemService.getItem(itemId);
        if (item == null || item.getUnitMeasureId() == null) return false;
        var result = unitMeasureApi.getUnitMeasure(item.getUnitMeasureId());
        return result != null && result.isSuccess() && result.getData() != null
                && "吨".equals(StrUtil.trim(result.getData().getType()));
    }
    private cn.iocoder.yudao.framework.common.exception.ServiceException bindingError(String message) {
        return new cn.iocoder.yudao.framework.common.exception.ServiceException(400, message);
    }
    private void bindTonBagsToBatch(Long batchId, List<Long> tonBagIds) {
        if (CollUtil.isEmpty(tonBagIds)) return;
        MesProBagCodeBindBatchReqVO req = new MesProBagCodeBindBatchReqVO();
        req.setBatchId(batchId);
        req.setIds(tonBagIds);
        bagCodeService.bindBatch(req);
    }
    private void bindPalletsToBatch(Long batchId, List<Long> palletIds) {
        if (CollUtil.isEmpty(palletIds)) {
            return;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/approver/MesQcApproverService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.service.qc.approver;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.approver.MesQcApproverConfigDO;
import java.util.List;
/**
 * MES è´¨æ£€å®¡æ ¸äºº Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesQcApproverService {
    /**
     * åˆ›å»ºè´¨æ£€å®¡æ ¸äººé…ç½®
     */
    Long createConfig(MesQcApproverConfigSaveReqVO reqVO);
    /**
     * æ›´æ–°è´¨æ£€å®¡æ ¸äººé…ç½®
     */
    void updateConfig(MesQcApproverConfigSaveReqVO reqVO);
    /**
     * åˆ é™¤è´¨æ£€å®¡æ ¸äººé…ç½®
     */
    void deleteConfig(Long id);
    /**
     * èŽ·å¾—è´¨æ£€å®¡æ ¸äººé…ç½®
     */
    MesQcApproverConfigDO getConfig(Long id);
    /**
     * èŽ·å¾—è´¨æ£€å®¡æ ¸äººé…ç½®åˆ†é¡µ
     */
    PageResult<MesQcApproverConfigDO> getConfigPage(MesQcApproverConfigPageReqVO pageReqVO);
    /**
     * åˆ¤æ–­ç”¨æˆ·æ˜¯å¦ä¸ºæŒ‡å®šè´¨æ£€ç±»åž‹å’Œå®¡æ ¸èŠ‚ç‚¹çš„å®¡æ ¸äºº
     */
    boolean isApprover(Integer qcType, Integer approveNode, Long userId);
    /**
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦ä¸ºæŒ‡å®šè´¨æ£€ç±»åž‹å’Œå®¡æ ¸èŠ‚ç‚¹çš„å®¡æ ¸äººï¼Œä¸æ˜¯åˆ™æŠ›å‡ºå¼‚å¸¸
     */
    void validateApprover(Integer qcType, Integer approveNode, Long userId);
    /**
     * èŽ·å¾—æŒ‡å®šè´¨æ£€ç±»åž‹å’Œå®¡æ ¸èŠ‚ç‚¹çš„å®¡æ ¸äººç”¨æˆ· ID é›†åˆ
     */
    List<Long> getApproverUserIds(Integer qcType, Integer approveNode);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/approver/MesQcApproverServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
package cn.iocoder.yudao.module.mes.service.qc.approver;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.approver.vo.MesQcApproverConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.approver.MesQcApproverConfigDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.approver.MesQcApproverConfigMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApproveNodeEnum;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES è´¨æ£€å®¡æ ¸äºº Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesQcApproverServiceImpl implements MesQcApproverService {
    private static final String SUPER_ADMIN_ROLE = "super_admin";
    private static final Integer STATUS_ENABLE = 0;
    @Resource
    private MesQcApproverConfigMapper approverConfigMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private PermissionApi permissionApi;
    @Override
    public Long createConfig(MesQcApproverConfigSaveReqVO reqVO) {
        validateConfigData(reqVO, null);
        MesQcApproverConfigDO config = BeanUtils.toBean(reqVO, MesQcApproverConfigDO.class);
        approverConfigMapper.insert(config);
        return config.getId();
    }
    @Override
    public void updateConfig(MesQcApproverConfigSaveReqVO reqVO) {
        validateConfigExists(reqVO.getId());
        validateConfigData(reqVO, reqVO.getId());
        approverConfigMapper.updateById(BeanUtils.toBean(reqVO, MesQcApproverConfigDO.class));
    }
    @Override
    public void deleteConfig(Long id) {
        validateConfigExists(id);
        approverConfigMapper.deleteById(id);
    }
    @Override
    public MesQcApproverConfigDO getConfig(Long id) {
        return approverConfigMapper.selectById(id);
    }
    @Override
    public PageResult<MesQcApproverConfigDO> getConfigPage(MesQcApproverConfigPageReqVO pageReqVO) {
        return approverConfigMapper.selectPage(pageReqVO);
    }
    @Override
    public boolean isApprover(Integer qcType, Integer approveNode, Long userId) {
        if (userId == null) {
            return false;
        }
        // è¶…管不受审核人配置限制
        if (permissionApi.hasAnyRoles(userId, SUPER_ADMIN_ROLE)) {
            return true;
        }
        MesQcApproverConfigDO config = approverConfigMapper.selectByUnique(qcType, approveNode, userId);
        return config != null && Objects.equals(config.getStatus(), STATUS_ENABLE);
    }
    @Override
    public void validateApprover(Integer qcType, Integer approveNode, Long userId) {
        if (!isApprover(qcType, approveNode, userId)) {
            throw exception(QC_APPROVER_NOT_CONFIGURED);
        }
    }
    @Override
    public List<Long> getApproverUserIds(Integer qcType, Integer approveNode) {
        return approverConfigMapper.selectList(new LambdaQueryWrapperX<MesQcApproverConfigDO>()
                        .eq(MesQcApproverConfigDO::getQcType, qcType)
                        .eq(MesQcApproverConfigDO::getApproveNode, approveNode)
                        .eq(MesQcApproverConfigDO::getStatus, STATUS_ENABLE))
                .stream().map(MesQcApproverConfigDO::getUserId).toList();
    }
    private MesQcApproverConfigDO validateConfigExists(Long id) {
        MesQcApproverConfigDO config = approverConfigMapper.selectById(id);
        if (config == null) {
            throw exception(QC_APPROVER_CONFIG_NOT_EXISTS);
        }
        return config;
    }
    private void validateConfigData(MesQcApproverConfigSaveReqVO reqVO, Long excludeId) {
        if (!MesQcApproveNodeEnum.isValidCombination(reqVO.getQcType(), reqVO.getApproveNode())) {
            throw exception(QC_APPROVER_NODE_INVALID);
        }
        adminUserApi.validateUser(reqVO.getUserId());
        MesQcApproverConfigDO existed = approverConfigMapper.selectByUnique(
                reqVO.getQcType(), reqVO.getApproveNode(), reqVO.getUserId());
        if (existed != null && !Objects.equals(existed.getId(), excludeId)) {
            throw exception(QC_APPROVER_CONFIG_DUPLICATE);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java
@@ -38,10 +38,6 @@
     * @param id ç¼–号
     */
    void finishOqc(Long id);
    void retestOqc(cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO reqVO);
    void salesApprove(Long id, boolean approved, String opinion);
    void companyApprove(Long id, boolean approved, String opinion);
    List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO> getActions(Long id);
    /**
     * åˆ é™¤å‡ºè´§æ£€éªŒå•
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -1,18 +1,14 @@
package cn.iocoder.yudao.module.mes.service.qc.oqc;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcActionMapper;
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;
@@ -40,11 +36,8 @@
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -56,13 +49,8 @@
@Validated
public class MesQcOqcServiceImpl implements MesQcOqcService {
    private static final int WAIT_RETEST = 10, WAIT_SALES = 20, WAIT_COMPANY = 30,
            RELEASED = 40, WAIT_SALES_FAIL = 50, WAIT_COMPANY_FAIL = 60, REJECTED = 70;
    @Resource
    private MesQcOqcMapper oqcMapper;
    @Resource
    private MesQcOqcActionMapper actionMapper;
    @Resource
    private MesQcTemplateItemService templateItemService;
@@ -145,114 +133,25 @@
    @Transactional(rollbackFor = Exception.class)
    public void finishOqc(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        MesQcOqcDO oqc = lock(id);
        if (!MesQcStatusEnum.DRAFT.getStatus().equals(oqc.getStatus())) throw exception(QC_OQC_NOT_PREPARE);
        MesQcOqcDO oqc = validateOqcStatusPrepare(id);
        // 1.2 æ ¡éªŒæ£€æµ‹ç»“论必填
        if (oqc.getCheckResult() == null) {
            throw exception(QC_OQC_CHECK_RESULT_EMPTY);
        }
        if (oqc.getUnqualifiedQuantity() != null && oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0
                && MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
            throw new IllegalArgumentException("存在不合格品时,出货检验结论不能为合格");
        }
        if ((oqc.getUnqualifiedQuantity() == null || oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0)
                && !MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
            throw new IllegalArgumentException("不合格数量为 0 æ—¶ï¼Œå‡ºè´§æ£€éªŒç»“论必须为合格");
        }
        // 1.3 æ ¡éªŒè‡³å°‘存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.OQC.getType());
        // 2. æ›´æ–°çŠ¶æ€ä¸ºå·²å®Œæˆ
        MesQcOqcDO updateObj = new MesQcOqcDO()
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus())
                .setWorkflowStatus(MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult()) ? WAIT_SALES : WAIT_RETEST);
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
        oqcMapper.updateById(updateObj);
        recordAction(id, "INITIAL", oqc.getCheckResult(), null, oqc);
        // é”€å”®å‡ºåº“明细仍为待检;公司质检审批通过后才回写并允许后续出库。
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void retestOqc(MesQcOqcRetestReqVO req) {
        MesQcOqcDO oqc = requireStatus(req.getId(), WAIT_RETEST);
        if (req.getCheckQuantity().compareTo(req.getQualifiedQuantity().add(req.getUnqualifiedQuantity())) != 0)
            throw new IllegalArgumentException("检测数量必须等于合格与不合格数量之和");
        if (req.getCheckQuantity().compareTo(oqc.getOutQuantity()) > 0)
            throw new IllegalArgumentException("检测数量不能超过出货数量");
        if (!Set.of(1, 2).contains(req.getCheckResult())) throw new IllegalArgumentException("复检结论只能为合格或不合格");
        boolean passed = req.getCheckResult() == 1;
        if (passed != (req.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0))
            throw new IllegalArgumentException("复检结论与不合格数量不一致");
        if (!passed && (req.getOpinion() == null || req.getOpinion().isBlank()))
            throw new IllegalArgumentException("复检不合格必须填写原因");
        MesQcOqcDO update = new MesQcOqcDO().setId(req.getId())
                .setCheckQuantity(req.getCheckQuantity()).setQualifiedQuantity(req.getQualifiedQuantity())
                .setUnqualifiedQuantity(req.getUnqualifiedQuantity()).setCheckResult(req.getCheckResult())
                .setInspectDate(LocalDateTime.now()).setInspectorUserId(getLoginUserId())
                .setWorkflowStatus(passed ? WAIT_SALES : WAIT_SALES_FAIL);
        oqcMapper.updateById(update);
        recordAction(req.getId(), "RETEST", req.getCheckResult(), req.getOpinion(), update);
    }
        // 3. ä¸åˆæ ¼æ—¶è‡ªåŠ¨åˆ›å»º NCR
        generateNcrIfNeeded(oqc);
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void salesApprove(Long id, boolean approved, String opinion) {
        MesQcOqcDO oqc = lock(id);
        if (!Set.of(WAIT_SALES, WAIT_SALES_FAIL).contains(oqc.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行销售部审批");
        requireOpinion(opinion);
        int next = approved ? (oqc.getWorkflowStatus() == WAIT_SALES ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
        recordAction(id, approved ? "SALES_APPROVE" : "SALES_REJECT", oqc.getCheckResult(), opinion, oqc);
        // 4. å›žå†™æ¥æºå•据
        writeBackSourceDoc(oqc);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void companyApprove(Long id, boolean approved, String opinion) {
        MesQcOqcDO oqc = lock(id);
        if (!Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(oqc.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行公司质检审批");
        requireOpinion(opinion);
        if (approved) {
            if (oqc.getWorkflowStatus() == WAIT_COMPANY_FAIL) generateNcrIfNeeded(oqc);
            writeBackSourceDoc(oqc);
        }
        int next = approved ? (oqc.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : REJECTED) : WAIT_RETEST;
        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", oqc.getCheckResult(), opinion, oqc);
    }
    private MesQcOqcDO lock(Long id) {
        MesQcOqcDO oqc = oqcMapper.selectByIdForUpdate(id);
        if (oqc == null) throw exception(QC_OQC_NOT_EXISTS);
        return oqc;
    }
    private MesQcOqcDO requireStatus(Long id, int status) {
        MesQcOqcDO oqc = lock(id);
        if (!Objects.equals(oqc.getWorkflowStatus(), status))
            throw new IllegalArgumentException("销售质检状态已变化,请刷新后重试");
        return oqc;
    }
    private void requireOpinion(String opinion) {
        if (opinion == null || opinion.isBlank()) throw new IllegalArgumentException("请填写审批意见");
    }
    private void recordAction(Long id, String action, Integer result, String opinion, MesQcOqcDO snapshot) {
        MesQcOqcActionDO event = new MesQcOqcActionDO();
        event.setOqcId(id);event.setAction(action);event.setResult(result);event.setOpinion(opinion);
        java.util.Map<String, BigDecimal> quantities = new java.util.HashMap<>();
        quantities.put("checkQuantity", snapshot.getCheckQuantity());
        quantities.put("qualifiedQuantity", snapshot.getQualifiedQuantity());
        quantities.put("unqualifiedQuantity", snapshot.getUnqualifiedQuantity());
        event.setQuantitiesJson(JSONUtil.toJsonStr(quantities));
        event.setOperatorId(getLoginUserId());event.setCreateTime(LocalDateTime.now());
        actionMapper.insert(event);
    }
    @Override
    public List<MesQcOqcActionDO> getActions(Long id) { return actionMapper.selectByOqcId(id); }
    /**
     * ä¸åˆæ ¼æ—¶è‡ªåŠ¨åˆ›å»º NCR(不合格品处理单)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedIndicatorRangeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedIndicatorRangeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedIndicatorRangeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedIndicatorRangeMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.math.BigDecimal;
import java.util.stream.Collectors;
@Service
public class MesQcSeedIndicatorRangeService {
    private static final Set<String> CODES = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    @Resource private MesQcSeedIndicatorRangeMapper mapper;
    public List<MesQcSeedIndicatorRangeDO> getRanges() {
        return mapper.selectList().stream().filter(item -> CODES.contains(item.getIndicatorCode())).toList();
    }
    @Transactional(rollbackFor = Exception.class)
    public void saveRanges(MesQcSeedIndicatorRangeSaveReqVO request) {
        List<MesQcSeedIndicatorRangeSaveReqVO.Item> items = request.getRanges();
        if (items == null || items.size() != CODES.size() || items.stream().anyMatch(item -> item == null || item.getIndicatorCode() == null
                || item.getMinValue() == null || item.getMaxValue() == null)
                || !items.stream().map(MesQcSeedIndicatorRangeSaveReqVO.Item::getIndicatorCode)
                .collect(Collectors.toSet()).equals(CODES))
            throw new IllegalArgumentException("必须完整配置纯度、净度、发芽率、水分四项指标");
        for (MesQcSeedIndicatorRangeSaveReqVO.Item item : items) {
            if (item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0
                    || item.getMinValue().compareTo(item.getMaxValue()) > 0)
                throw new IllegalArgumentException("指标合格范围必须处于 0–100 ä¸”下限不大于上限:" + item.getIndicatorCode());
            MesQcSeedIndicatorRangeDO row = new MesQcSeedIndicatorRangeDO();
            row.setIndicatorCode(item.getIndicatorCode());
            row.setMinValue(item.getMinValue());
            row.setMaxValue(item.getMaxValue());
            if (mapper.selectById(item.getIndicatorCode()) == null) mapper.insert(row);
            else mapper.updateById(row);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
@@ -16,6 +16,9 @@
    java.util.Map<String, Object> resolveScan(String code, String scanType);
    java.util.Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO);
    void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO reqVO);
    void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO reqVO);
    java.util.Map<String, Object> resolveRetestScope(String code, String scanType);
    java.util.Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO reqVO);
    void processApprove(Long id, boolean approved, String opinion);
    void companyApprove(Long id, boolean approved, String opinion);
    void processApproveBatch(List<Long> ids, boolean approved, String opinion);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -22,11 +22,14 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApproveNodeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcApprovalTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService;
import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -52,9 +55,10 @@
@Service @Validated @Slf4j
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
    private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_COMPANY = 30,
            RELEASED = 40, WAIT_PROCESS_FAIL = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_INITIAL_FAIL_REVIEW = 25, WAIT_COMPANY = 30,
            RELEASED = 40, WAIT_INITIAL_RECHECK = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcApproverService approverService;
    @Resource private MesQcSeedQualityActionMapper actionMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
@@ -210,8 +214,13 @@
                .inspectedAt(LocalDateTime.now()).build();
        qualityMapper.insert(quality);
        for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
            if (item == null || item.getCode() == null || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
                throw new IllegalArgumentException("四项指标的实测值和本次合格范围必须完整");
            if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复");
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限");
            if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0
                    || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0)
                throw new IllegalArgumentException("百分比指标必须处于 0–100");
            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
            indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
@@ -219,10 +228,10 @@
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
        quality.setResult(passed ? 1 : 2);
        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST);
        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW);
        qualityMapper.updateById(quality);
        recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators());
        // åˆæ£€ä»…记录结论;袋仍留在暂存库,待复检及两级审批后再放行或报废。
        // åˆæ£€ä»…记录结论;袋仍留在暂存库,待初检审核、复检及最终审核后再放行或报废。
        return quality.getId();
    }
@@ -310,15 +319,24 @@
    @Override @Transactional(rollbackFor = Exception.class)
    public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
        MesQcSeedQualityDO quality = requireStatus(id, WAIT_RETEST);
        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("复检袋码与原质检记录不一致");
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能提交复检,请刷新后重试");
        boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK);
        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("检验袋码与原质检记录不一致");
        boolean passed = validateIndicators(req.getIndicators());
        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("复检不合格必须填写原因");
        recordAction(id, "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
                .reason(passed ? "" : req.getReason().trim()).retestResult(passed ? 1 : 2).retestedAt(LocalDateTime.now())
        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("检验不合格必须填写原因");
        recordAction(id, initialRecheck ? "INITIAL_RECHECK" : "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
        MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
                .reason(passed ? "" : req.getReason().trim())
                .inspectorId(getLoginUserId()).inspectedAt(LocalDateTime.now())
                .workflowStatus(passed ? WAIT_PROCESS : WAIT_PROCESS_FAIL).build());
                .workflowStatus(initialRecheck ? (passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW)
                        : (passed ? WAIT_COMPANY : WAIT_COMPANY_FAIL)).build();
        if (!initialRecheck) {
            update.setRetestResult(passed ? 1 : 2);
            update.setRetestedAt(LocalDateTime.now());
        }
        qualityMapper.updateById(update);
        // é¡µé¢ã€å…¬å¼€è¿½æº¯è¯»å–主指标表;同步为最新一次测值,历次测值在操作留痕表保留。
        Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>();
        for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id))
@@ -332,12 +350,108 @@
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO req) {
        List<Long> ids = approvalIds(req.getIds());
        String batchCode = null;
        Integer workflowStatus = null;
        boolean first = true;
        List<MesQcSeedQualityDO> qualities = new ArrayList<>();
        for (Long id : ids) {
            MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
            if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
                throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试");
            if (StrUtil.isBlank(quality.getBatchCode()))
                throw new IllegalArgumentException("记录缺少生产批次,不能批量质检");
            if (first) {
                batchCode = quality.getBatchCode();
                workflowStatus = quality.getWorkflowStatus();
                first = false;
            } else if (!Objects.equals(batchCode, quality.getBatchCode()) || !Objects.equals(workflowStatus, quality.getWorkflowStatus())) {
                throw new IllegalArgumentException("批量质检只允许同生产批次、同流程状态的记录");
            }
            qualities.add(quality);
        }
        for (MesQcSeedQualityDO quality : qualities) {
            cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO single =
                    new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
            single.setCode(quality.getCode());
            single.setIndicators(req.getIndicators());
            single.setReason(req.getReason());
            retest(quality.getId(), single);
        }
    }
    private void checkRetestScanType(String scanType) {
        if (!Set.of("PALLET", "BATCH").contains(scanType))
            throw new IllegalArgumentException("批量质检只支持托盘码或生产批次码");
    }
    @Override
    public Map<String, Object> resolveRetestScope(String code, String scanType) {
        checkRetestScanType(scanType);
        Scope scope = scope(code, scanType, false);
        if (scope.canceled()) throw new IllegalArgumentException("质检对象已作废");
        List<String> codes = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
                .map(MesProBagCodeDO::getCode).toList();
        List<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes);
        List<Long> retestIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_RETEST))
                .map(MesQcSeedQualityDO::getId).sorted().toList();
        List<Long> initialRecheckIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_RECHECK))
                .map(MesQcSeedQualityDO::getId).sorted().toList();
        long pendingProcessCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS)
                || Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW)).count();
        long pendingCompanyCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY)
                || Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL)).count();
        Map<String, Object> result = new HashMap<>();
        result.put("code", scope.code()); result.put("scanType", scanType); result.put("batchCode", scope.batchCode());
        result.put("totalCount", codes.size()); result.put("retestIds", retestIds);
        result.put("initialRecheckIds", initialRecheckIds);
        result.put("pendingProcessCount", pendingProcessCount);
        result.put("pendingCompanyCount", pendingCompanyCount);
        return result;
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO req) {
        checkRetestScanType(req.getScanType());
        if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus()))
            throw new IllegalArgumentException("批量质检状态无效");
        List<Long> confirmed = approvalIds(req.getQualityIds());
        Scope scope = scope(req.getCode(), req.getScanType(), true);
        if (scope.canceled()) throw new IllegalArgumentException("质检对象已作废");
        List<MesProBagCodeDO> effective = scope.bags().stream()
                .filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null).toList();
        if (effective.isEmpty()) throw new IllegalArgumentException("当前范围没有已入库的有效袋码");
        List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList());
        if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
            throw new IllegalArgumentException("袋码状态已变化,请重新扫描");
        Scope current = scope(req.getCode(), req.getScanType(), false);
        Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
                .map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
        if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
            throw new IllegalArgumentException("袋码归属已变化,请重新扫描");
        List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList());
        List<MesQcSeedQualityDO> pending = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus()))
                .sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList();
        if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesQcSeedQualityDO::getId).toList()))
            throw new IllegalArgumentException("待质检清单已变化,请重新扫描核对");
        for (MesQcSeedQualityDO quality : pending) {
            var single = new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
            single.setCode(quality.getCode()); single.setIndicators(req.getIndicators()); single.setReason(req.getReason());
            retest(quality.getId(), single);
        }
        return Map.of("processedCount", pending.size());
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void processApprove(Long id, boolean approved, String opinion) {
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_PROCESS_FAIL).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行加工部审批");
        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("请填写审批意见");
        int next = approved ? (quality.getWorkflowStatus() == WAIT_PROCESS ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行初检审核");
        approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
                MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
        if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
        int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK;
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
                .processApprovedAt(LocalDateTime.now()).build());
        recordAction(id, approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null);
@@ -347,8 +461,10 @@
    public void companyApprove(Long id, boolean approved, String opinion) {
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行公司质检审批");
        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("请填写审批意见");
            throw new IllegalArgumentException("当前状态不能进行复检审核");
        approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
                MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId());
        if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见");
        if (approved) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY);
        int next = approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST;
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
@@ -390,6 +506,9 @@
                    || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
                throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复");
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限");
            if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0
                    || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0)
                throw new IllegalArgumentException("百分比指标必须处于 0–100");
            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
        }
        return passed;
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -2,7 +2,6 @@
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
@@ -76,9 +75,6 @@
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setCheckResult(checkResult);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
        });
        oqcMapper.insert(oqc);
@@ -88,83 +84,9 @@
        // æ–­è¨€ï¼šéªŒè¯çŠ¶æ€æ›´æ–°ä¸ºå·²å®Œæˆ
        MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
        assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
        assertEquals(20, updatedOqc.getWorkflowStatus());
        // åˆæ£€å®Œæˆä¸æ”¾è¡Œï¼›é”€å”®éƒ¨å’Œå…¬å¸è´¨æ£€å‡å®¡æ‰¹åŽæ‰å›žå†™ã€‚
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        oqcService.companyApprove(oqc.getId(), true, "公司质检同意");
        // æ–­è¨€ï¼šéªŒè¯å›žå†™é”€å”®å‡ºåº“单行
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(
                eq(sourceLineId), eq(oqc.getId()), eq(checkResult));
    }
    @Test
    public void testFailedInitialRetestPassOnlyReleasesAfterBothApprovals() {
        Long sourceLineId = randomLongId();
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
            o.setCheckResult(2);
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        assertEquals(10, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
        retest.setId(oqc.getId());
        retest.setCheckQuantity(java.math.BigDecimal.TEN);
        retest.setQualifiedQuantity(java.math.BigDecimal.TEN);
        retest.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
        retest.setCheckResult(1);
        oqcService.retestOqc(retest);
        assertEquals(20, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        oqcService.salesApprove(oqc.getId(), true, "销售部通过");
        assertEquals(30, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        oqcService.companyApprove(oqc.getId(), true, "公司质检通过");
        assertEquals(40, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 1);
        verifyNoInteractions(ncrService);
    }
    @Test
    public void testFailedRetestCreatesNcrOnlyAfterCompanyApproval() {
        Long sourceLineId = randomLongId();
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
            o.setCheckResult(2);
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
        retest.setId(oqc.getId());
        retest.setCheckQuantity(java.math.BigDecimal.TEN);
        retest.setQualifiedQuantity(java.math.BigDecimal.valueOf(8));
        retest.setUnqualifiedQuantity(java.math.BigDecimal.valueOf(2));
        retest.setCheckResult(2);
        retest.setOpinion("复检仍不合格");
        oqcService.retestOqc(retest);
        assertEquals(50, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        oqcService.salesApprove(oqc.getId(), true, "销售部确认不合格");
        assertEquals(60, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        oqcService.companyApprove(oqc.getId(), true, "公司质检确认不合格");
        assertEquals(70, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(ncrService).createNcr(any());
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 2);
    }
    @Test
@@ -173,9 +95,6 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(null);
            o.setSourceLineId(null);
        });
@@ -198,17 +117,13 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(null); // è„æ•°æ®
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "公司质检同意"));
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
    }
    @Test
@@ -217,17 +132,13 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(99999); // æ— æ³•识别的类型
            o.setSourceLineId(randomLongId());
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "公司质检同意"));
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
    }
    @Test
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedIndicatorRangeServiceTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedIndicatorRangeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedIndicatorRangeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedIndicatorRangeMapper;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.util.List;
import static org.mockito.Mockito.*;
class MesQcSeedIndicatorRangeServiceTest {
    private final MesQcSeedIndicatorRangeService service = new MesQcSeedIndicatorRangeService();
    private final MesQcSeedIndicatorRangeMapper mapper = mock(MesQcSeedIndicatorRangeMapper.class);
    @Test
    void savesTemplateWithoutRestrictingActualInspection() {
        ReflectionTestUtils.setField(service, "mapper", mapper);
        MesQcSeedIndicatorRangeSaveReqVO request = new MesQcSeedIndicatorRangeSaveReqVO();
        request.setRanges(List.of(item("purity"), item("cleanliness"), item("germination_rate"), item("moisture")));
        service.saveRanges(request);
        verify(mapper, times(4)).insert(any(MesQcSeedIndicatorRangeDO.class));
    }
    private MesQcSeedIndicatorRangeSaveReqVO.Item item(String code) {
        MesQcSeedIndicatorRangeSaveReqVO.Item row = new MesQcSeedIndicatorRangeSaveReqVO.Item();
        row.setIndicatorCode(code);
        row.setMinValue(new BigDecimal("90"));
        row.setMaxValue(new BigDecimal("100"));
        return row;
    }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
@@ -78,4 +78,33 @@
        var proxy=(MesQcSeedQualityService)factory.getProxy();
        assertThrows(IllegalStateException.class,()->proxy.submitGroup(req));assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM qc_result",Integer.class));
    }
    @Test void retestScopePreviewSeparatesWorkflowStages() {
        when(qualities.selectListByCodes(any())).thenReturn(List.of(
                MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
                MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(50).build()));
        var result = service.resolveRetestScope("PALLET-BUS", "PALLET");
        assertEquals(List.of(11L), result.get("retestIds"));
        assertEquals(List.of(12L), result.get("initialRecheckIds"));
    }
    @Test void retestScopeRejectsStaleConfirmedList() {
        when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(
                MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
                MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()));
        var request = new MesQcSeedQualityScopeRetestReqVO();
        request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10);
        request.setQualityIds(List.of(11L)); request.setIndicators(List.of());
        assertThrows(IllegalArgumentException.class, () -> service.retestScope(request));
        verify(service, never()).retest(anyLong(), any());
    }
    @Test void retestScopeProcessesAllConfirmedBags() {
        when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(
                MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
                MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()));
        doNothing().when(service).retest(anyLong(), any());
        var request = new MesQcSeedQualityScopeRetestReqVO();
        request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10);
        request.setQualityIds(List.of(11L, 12L)); request.setIndicators(List.of());
        assertEquals(2, service.retestScope(request).get("processedCount"));
        verify(service, times(2)).retest(anyLong(), any());
    }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java
@@ -1,13 +1,17 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityActionMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper;
import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -17,30 +21,76 @@
    private final MesQcSeedQualityMapper qualityMapper = mock(MesQcSeedQualityMapper.class);
    private final MesQcSeedQualityActionMapper actionMapper = mock(MesQcSeedQualityActionMapper.class);
    private final MesWmTransactionService transactionService = mock(MesWmTransactionService.class);
    private final MesQcApproverService approverService = mock(MesQcApproverService.class);
    @BeforeEach
    void setUp() {
        ReflectionTestUtils.setField(service, "qualityMapper", qualityMapper);
        ReflectionTestUtils.setField(service, "actionMapper", actionMapper);
        ReflectionTestUtils.setField(service, "wmTransactionService", transactionService);
        ReflectionTestUtils.setField(service, "approverService", approverService);
    }
    @Test
    void failedRetestNeedsBothApprovalsBeforeDisposal() {
    void initialApprovalOnlyQueuesRetestAndFailedRetestCanReturnToRetest() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(50).build(),
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(20).build(),
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(60).build());
        service.processApprove(1L, true, "确认不合格,送公司质检审批");
        service.processApprove(1L, true, "");
        verifyNoInteractions(transactionService);
        service.companyApprove(1L, false, "返回复检");
        service.companyApprove(1L, false, "复检不合格,重新取样");
        ArgumentCaptor<MesQcSeedQualityDO> update = ArgumentCaptor.forClass(MesQcSeedQualityDO.class);
        verify(qualityMapper, times(2)).updateById(update.capture());
        assertEquals(60, update.getAllValues().get(0).getWorkflowStatus());
        assertEquals(10, update.getAllValues().get(0).getWorkflowStatus());
        assertEquals(10, update.getAllValues().get(1).getWorkflowStatus());
        verifyNoInteractions(transactionService);
    }
    @Test
    void rejectedInitialInspectionNeedsRecheck() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(25).build());
        service.processApprove(1L, false, "初检样本无效,重新检测");
        ArgumentCaptor<MesQcSeedQualityDO> update = ArgumentCaptor.forClass(MesQcSeedQualityDO.class);
        verify(qualityMapper).updateById(update.capture());
        assertEquals(50, update.getValue().getWorkflowStatus());
        verifyNoInteractions(transactionService);
    }
    @Test
    void rejectionRequiresOpinion() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).workflowStatus(20).build());
        assertEquals("驳回时请填写审批意见", assertThrows(IllegalArgumentException.class,
                () -> service.processApprove(1L, false, " ")).getMessage());
        verify(qualityMapper, never()).updateById(any(MesQcSeedQualityDO.class));
        verifyNoInteractions(actionMapper, transactionService);
    }
    @Test
    void cannotRetestBeforeInitialReview() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).code("B1").workflowStatus(25).build());
        assertThrows(IllegalArgumentException.class,
                () -> service.retest(1L, new MesQcSeedQualityRetestReqVO()));
        verify(qualityMapper, never()).updateById(any(MesQcSeedQualityDO.class));
        verifyNoInteractions(transactionService, actionMapper);
    }
    @Test
    void batchRetestRejectsDifferentProductionBatchesBeforeAnyWrite() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).code("B1").batchCode("A").workflowStatus(10).build());
        when(qualityMapper.selectByIdForUpdate(2L)).thenReturn(
                MesQcSeedQualityDO.builder().id(2L).code("B2").batchCode("B").workflowStatus(10).build());
        MesQcSeedQualityBatchRetestReqVO request = new MesQcSeedQualityBatchRetestReqVO();
        request.setIds(List.of(1L, 2L));
        assertThrows(IllegalArgumentException.class, () -> service.retestBatch(request));
        verify(qualityMapper, never()).updateById(any(MesQcSeedQualityDO.class));
        verifyNoInteractions(transactionService, actionMapper);
    }
    @Test
    void skipsApprovalWhenStatusChanged() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).workflowStatus(40).build());
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -413,7 +413,6 @@
    "inspect_date" timestamp DEFAULT NULL,
    "inspector_user_id" bigint DEFAULT NULL,
    "status" tinyint NOT NULL DEFAULT 0,
    "workflow_status" tinyint NOT NULL DEFAULT 0,
    "remark" varchar(500) DEFAULT '',
    "creator" varchar(64) DEFAULT '',
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -424,15 +423,22 @@
    PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "mes_qc_oqc_action" (
-- ----------------------------
-- MES è´¨æ£€å®¡æ ¸äººé…ç½®
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_approver_config" (
    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    "oqc_id" bigint NOT NULL,
    "action" varchar(32) NOT NULL,
    "result" tinyint DEFAULT NULL,
    "opinion" varchar(500) DEFAULT NULL,
    "quantities_json" varchar(4000) DEFAULT NULL,
    "operator_id" bigint DEFAULT NULL,
    "qc_type" tinyint NOT NULL,
    "approve_node" tinyint NOT NULL,
    "user_id" bigint NOT NULL,
    "status" tinyint NOT NULL DEFAULT 0,
    "remark" varchar(255) DEFAULT NULL,
    "creator" varchar(64) DEFAULT '',
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updater" varchar(64) DEFAULT '',
    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "deleted" bit NOT NULL DEFAULT FALSE,
    "tenant_id" bigint NOT NULL DEFAULT 0,
    PRIMARY KEY ("id")
);