云
21 小时以前 d80fa460742ddf052844746482801de355e85528
feat(qc): 拆分部门审批流程并支持扫码自动识别

- 拆分部门审批为加工部与销售部独立流程
- 监听器重构以同时支持两个部门审批流程
- 质检扫码新增自动识别袋码托盘及批次码功能
- 移除移动端手动选择扫码类型改为自动识别
- 审批详情页根据质检线动态展示对应部门标题
- 库存事务流水列表新增计量单位名称字段展示
- 更新质检双线集成及流程迁移相关说明文档
已添加2个文件
已修改9个文件
227 ■■■■■ 文件已修改
docs/qc_bpm_migration_plan.md 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_seed_quality_dual_line_integration.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/wm_transaction_unit_measure_integration.md 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/deploy-seed-quality-bpm.ps1 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/listener/MesQcSeedLineStatusListener.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/bpm/mes_seed_quality_line_approve.bpmn20.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/bpm/mes_seed_quality_sales_line_approve.bpmn20.xml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/qc_bpm_migration_plan.md
@@ -13,11 +13,12 @@
- åˆæ£€åˆæ ¼è¿›å…¥éƒ¨é—¨å®¡æ‰¹ï¼Œåˆæ£€ä¸åˆæ ¼å…ˆå¤æ£€ï¼›éƒ¨é—¨å®¡æ‰¹å®ŒæˆåŽï¼Œä¸¤æ¡çº¿æ±‡åˆåˆ°å…¬å¸è´¨æ£€å®¡æ‰¹ã€‚退货复检结果进入销售部审批。
- BPM å›žè°ƒæŒ‰æµç¨‹å®žä¾‹ç¼–号匹配记录,审批驳回后清理对应实例编号,以便重检后重新发起。
- è´¨æ£€è®°å½•页提供 BPM è¯¦æƒ…入口;流程启动失败时可对该记录重新发起。
- ä¸‰ä»½ BPMN æ¨¡åž‹æ–‡ä»¶ä½äºŽ `yudao-module-mes/src/main/resources/bpm/`,候选人暂为当前数据库默认 admin ç”¨æˆ· ID 1。
- éƒ¨é—¨å®¡æ‰¹æ‹†åˆ†ä¸ºåŠ å·¥éƒ¨ã€é”€å”®éƒ¨ä¸¤ä¸ªç‹¬ç«‹æµç¨‹æ¨¡åž‹ï¼šåŠ å·¥éƒ¨æ²¿ç”¨ `mes_seed_quality_line_approve`,销售部新增 `mes_seed_quality_sales_line_approve`,发起审批时按 `qcLine` é€‰æ‹©å¯¹åº”流程定义。
- å››ä»½ BPMN æ¨¡åž‹æ–‡ä»¶ä½äºŽ `yudao-module-mes/src/main/resources/bpm/`,候选人暂为当前数据库默认 admin ç”¨æˆ· ID 1。
## ä¸Šçº¿å‰ç½®å·¥ä½œ
- åœ¨ BPM æµç¨‹æ¨¡åž‹ç®¡ç†ä¸­å¯¼å…¥å¹¶å‘布三份 BPMN æ–‡ä»¶ï¼Œæµç¨‹å®šä¹‰ key å¿…须与文件中 `process id` ä¸€è‡´ã€‚可用 `scripts/deploy-seed-quality-bpm.ps1 -ApiBaseUrl <管理后台 API æ ¹åœ°å€> -AccessToken <admin ä»¤ç‰Œ>` åˆ›å»ºæˆ–更新并发布。脚本显式设置 admin(用户 ID 1)为审批人且禁用自动审批;目标环境中应先确认 admin çš„ ID ä¸º 1。
- åœ¨ BPM æµç¨‹æ¨¡åž‹ç®¡ç†ä¸­å¯¼å…¥å¹¶å‘布四份 BPMN æ–‡ä»¶ï¼Œæµç¨‹å®šä¹‰ key å¿…须与文件中 `process id` ä¸€è‡´ã€‚可用 `scripts/deploy-seed-quality-bpm.ps1 -ApiBaseUrl <管理后台 API æ ¹åœ°å€> -AccessToken <admin ä»¤ç‰Œ>` åˆ›å»ºæˆ–更新并发布。脚本显式设置 admin(用户 ID 1)为审批人且禁用自动审批;目标环境中应先确认 admin çš„ ID ä¸º 1。
- æ‰§è¡Œ `sql/mysql/mes_seed_quality_dual_line_20261105.sql`,确保 `qc_line`、`biz_type`、流程实例字段和双线唯一索引已就绪。
- å¤„理旧的在途人工审批记录及已经产生的 BPM æµç¨‹å®žä¾‹ï¼Œé¿å…æ–°æ—§çŠ¶æ€æ··ç”¨ã€‚
- ä¸‰ä¸ª BPM æµç¨‹å‘布并完成在途数据处理后,执行 `sql/mysql/mes_qc_approver_menu_retire_20261106.sql` éšè—æ—§å®¡æ ¸äººé…ç½®èœå•。历史配置数据不删除。
docs/qc_seed_quality_dual_line_integration.md
@@ -11,7 +11,7 @@
1. åŠ å·¥éƒ¨å¼€é”€å”®å…¥åº“ï¼šè¢‹ç å…¥åº“åŽå¯æ‰«ç è´¨æ£€ï¼Œæ‰˜ç›˜ç æ¯ 48 è¢‹æˆ– 100 è¢‹ç”Ÿæˆï¼Œæ‰¹æ¬¡ç å¸¦å‡ºæ•´æ‰¹è¢‹æ•°ã€‚
2. **一次种子质检提交即同时生成两条质检记录**:`qcLine=1`(加工部)与 `qcLine=2`(销售部),两条记录的指标、结论完全一致;后续复检、审批分别按质检线独立进行。
3. åˆæ£€åˆæ ¼ç›´æŽ¥è¿›å…¥æœ¬çº¿é¢†å¯¼ BPM å®¡æ‰¹ï¼›åˆæ£€ä¸åˆæ ¼å…ˆå¤æ£€ï¼Œå†è¿›å…¥æœ¬çº¿é¢†å¯¼å®¡æ‰¹ã€‚部门审批驳回后重新初检。
4. ä¸¤æ¡çº¿åˆ†åˆ«ä½¿ç”¨æµç¨‹å®šä¹‰ `mes_seed_quality_line_approve`,businessKey ä¸º `质检线#生产批次号`;审批通过后进入公司质检待审状态。
4. ä¸¤æ¡çº¿ä½¿ç”¨å„自的流程定义:加工部 `mes_seed_quality_line_approve`、销售部 `mes_seed_quality_sales_line_approve`,businessKey å‡ä¸º `质检线#生产批次号`;审批通过后进入公司质检待审状态。
5. åŒä¸€ç”Ÿäº§æ‰¹æ¬¡å†…所有袋的两条线都完成部门审批后,系统自动发起**一次**公司质检审批(流程定义 `mes_seed_quality_company_approve`,businessKey ä¸º**生产批次号**)。公司审批**按批次发起、按袋落库**:每个袋两条线都为合格才放行入库,任一条不合格则该袋报废;驳回则整批两条线一起退回待复检。
6. æ”¾è¡ŒåŽäº§å“æ­£å¼å…¥åº“,后续库管扫码出库、销售转移经销商、终端农户扫码溯源。
7. ç»é”€å•†é€€è´§åˆ†æ”¯ï¼šé”€å”®é€€è´§å•确认后发起退货质检 â†’ å¤æ£€ â†’ é”€å”®éƒ¨é¢†å¯¼å®¡æ‰¹ï¼›ä¸åˆæ ¼åˆ™é€€è´§å•自动取消、不予退货并保留检验记录。
@@ -99,10 +99,12 @@
| æµç¨‹å®šä¹‰ key | businessKey | æµç¨‹å˜é‡ |
|---|---|---|
| `mes_seed_quality_line_approve` | `{qcLine}#{生产批次号}` | `qcLine`、`batchCode` |
| `mes_seed_quality_sales_line_approve` | `{qcLine}#{生产批次号}` | `qcLine`、`batchCode` |
| `mes_seed_quality_company_approve` | ç”Ÿäº§æ‰¹æ¬¡å· | `batchCode`、`code`(兼容旧流程)、`passed` |
| `mes_seed_return_approve` | é”€å”®é€€è´§å•编号 | `returnSalesId`、`returnSalesCode`、`code` |
- æµç¨‹ç›‘听器在流程结束(通过/驳回)时回写质检状态;取消(CANCEL)不改变业务状态。
- æµç¨‹ç›‘听器同时监听加工部与销售部两个流程定义,流程结束(通过/驳回)时回写质检状态;取消(CANCEL)不改变业务状态。
- æµç¨‹è¯¦æƒ…页(`views/mes/qc/seedQuality/process-detail.vue`)已识别新增的 `mes_seed_quality_sales_line_approve`,并按质检线展示「加工部审批 / é”€å”®éƒ¨å®¡æ‰¹ã€æ ‡é¢˜ã€‚
- è‹¥ BPM æµç¨‹å®šä¹‰æœªéƒ¨ç½²ï¼Œå‘起流程会失败并记录日志;修复配置后可通过质检记录页的“重新发起审批”恢复。新双线记录不能使用人工审批接口。
## å­—段展示规则
docs/wm_transaction_unit_measure_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
# åº“存事务流水计量单位(规格)联调方案
## æ¶‰åŠé¡µé¢
- ä»“储物流 -> åº“存事务流水
- ä»“储物流 -> åº“存现有量(字段命名保持一致)
## ä¸šåŠ¡èƒŒæ™¯
按需求「库存流水增加规格字段;库存现有量『单位』改为『规格』、『规格型号』改为『类别』」:
- è§„格列展示物料的计量单位名称(如 `48千克`、`100千克`),数据来源为 MDM è®¡é‡å•位。
- ç±»åˆ«åˆ—展示物料的规格型号(如 `大田用种`)。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/transaction/page | åº“存事务流水分页(响应新增字段) |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| unitMeasureName | String | è®¡é‡å•位名称,按物料 `unitMeasureId` å…³è” MDM è®¡é‡å•位得到;未维护时为空 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "itemId": 1,
        "itemCode": "9335999888099723",
        "itemName": "塔河4号",
        "unitMeasureName": "48千克",
        "specification": null,
        "itemTypeName": null,
        "quantity": 1.00
      }
    ],
    "total": 1
  }
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | åˆ—标题 | è¯´æ˜Ž |
|------|----------|--------|------|
| unitMeasureName | åº“存事务流水、库存现有量、库存选择弹窗 | è§„æ ¼ | è®¡é‡å•位名称 |
| specification | åº“存事务流水、库存现有量、库存选择弹窗 | ç±»åˆ« | ç‰©æ–™è§„格型号 |
| itemTypeName | ä¸å†å±•示 | - | æŽ¥å£ä»è¿”回,兼容旧调用 |
## æ³¨æ„äº‹é¡¹
- `unitMeasureName` ä¸ºç©ºè¡¨ç¤ºç‰©æ–™æœªç»´æŠ¤è®¡é‡å•位,请先在 MDM ç‰©æ–™æˆ– MES å“ç§ä¸­ç»´æŠ¤ã€‚
- åº“存现有量接口 `/mes/wm/material-stock/page` åŽŸæœ¬å·²è¿”å›ž `unitMeasureName`,本次仅调整前端列映射,无接口变化。
- æœ¬æ¬¡ä¸ºå‘后兼容的响应字段新增,不涉及数据库结构变更。
scripts/deploy-seed-quality-bpm.ps1
@@ -13,6 +13,7 @@
# ä¸­æ–‡å‘½åç»Ÿä¸€ä»Ž BPMN æ–‡ä»¶è¯»å–,避免 Windows PowerShell 5.1 æŒ‰ ANSI è§£æžè„šæœ¬å¯¼è‡´ä¹±ç 
$models = @(
    'mes_seed_quality_line_approve',
    'mes_seed_quality_sales_line_approve',
    'mes_seed_quality_company_approve',
    'mes_seed_return_approve'
)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -86,7 +86,7 @@
    @GetMapping("/resolve")
    @Operation(summary = "解析待质检袋码及确认该袋质检结果")
    @PreAuthorize("@ss.hasAnyPermissions('mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', 'mes:qc-seed-quality:process-approve', 'mes:qc-seed-quality:company-approve')")
    public CommonResult<java.util.Map<String, Object>> resolve(@RequestParam String code, @RequestParam(defaultValue = "BAG") String scanType,
    public CommonResult<java.util.Map<String, Object>> resolve(@RequestParam String code, @RequestParam(defaultValue = "AUTO") String scanType,
                                                                @RequestParam(defaultValue = "1") Integer qcLine) {
        return success(service.resolveScan(code, scanType, qcLine));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
@@ -22,7 +22,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -73,6 +75,7 @@
    @Resource private MesWmProductSalesDetailMapper salesDetailMapper;
    @Resource private MesMdItemMapper mesMdItemMapper;
    @Resource private MesMdItemTypeService mesMdItemTypeService;
    @Resource private MesMdUnitMeasureService unitMeasureService;
    @GetMapping("/page")
    @Operation(summary = "获取库存事务流水分页")
@@ -100,6 +103,9 @@
        mesItemTypeIds.remove(null);
        Map<Long, MesMdItemTypeDO> mesItemTypeMap = mesItemTypeIds.isEmpty() ? Map.of()
                : mesMdItemTypeService.getItemTypeMap(mesItemTypeIds);
        // å…³è”查询计量单位,规格列展示计量单位名称
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdmItemDO::getUnitMeasureId));
        // å…³è”查询仓库信息
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(pageResult.getList(), MesWmTransactionDO::getWarehouseId));
@@ -152,6 +158,10 @@
                // è§„格:品种管理未维护时,回退取 MES ç‰©æ–™è§„格型号
                vo.setSpecification(StrUtil.blankToDefault(item.getSpecification(),
                        mesItem != null ? mesItem.getSpecification() : null));
                MesMdUnitMeasureDO unitMeasure = unitMeasureMap.get(item.getUnitMeasureId());
                if (unitMeasure != null) {
                    vo.setUnitMeasureName(unitMeasure.getName());
                }
                // ç±»åˆ«ï¼šä¼˜å…ˆå“ç§åˆ†ç±»ï¼Œå…¶æ¬¡ MES ç‰©æ–™åˆ†ç±»
                MdmItemCategoryRespDTO category = itemCategoryMap.get(item.getCategoryId());
                String itemTypeName = category != null ? category.getName() : null;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java
@@ -45,6 +45,9 @@
    @Schema(description = "规格型号", example = "10mm*100mm")
    private String specification;
    @Schema(description = "计量单位名称", example = "48千克")
    private String unitMeasureName;
    @Schema(description = "物料类别名称", example = "五金配件")
    private String itemTypeName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -12,9 +12,11 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedReturnQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesTraceCodeUtils;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO;
@@ -78,8 +80,10 @@
    private static final int QC_LINE_PROCESS = 1, QC_LINE_SALES = 2;
    /** ä¸šåŠ¡ç±»åž‹ï¼š1 ç”Ÿäº§å…¥åº“质检;2 ç»é”€å•†é€€è´§è´¨æ£€ */
    private static final int BIZ_TYPE_PRODUCTION = 1, BIZ_TYPE_RETURN = 2;
    /** æœ¬çº¿é¢†å¯¼å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    /** åŠ å·¥éƒ¨é¢†å¯¼å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    private static final String LINE_PROCESS_KEY = "mes_seed_quality_line_approve";
    /** é”€å”®éƒ¨é¢†å¯¼å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    private static final String SALES_LINE_PROCESS_KEY = "mes_seed_quality_sales_line_approve";
    /** å…¬å¸è´¨æ£€å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    private static final String COMPANY_PROCESS_KEY = "mes_seed_quality_company_approve";
    /** ç»é”€å•†é€€è´§è´¨æ£€å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
@@ -194,7 +198,7 @@
    @Override
    public Map<String, Object> resolveScan(String code) {
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(MesTraceCodeUtils.extractCode(code));
        if (bag == null) throw new IllegalArgumentException("未解析到有效袋码,请扫描袋码");
        Map<String, Object> result = new HashMap<>();
        result.put("code", bag.getCode());
@@ -227,28 +231,66 @@
        return result;
    }
    private record Scope(String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { }
    private record Scope(String type, String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { }
    /** æ‰«ç ç±»åž‹ï¼šç”±ç å€¼è‡ªåŠ¨è¯†åˆ«è¢‹ç  / æ‰˜ç›˜ç  / ç”Ÿäº§æ‰¹æ¬¡ç  */
    private static final String SCAN_TYPE_AUTO = "AUTO";
    private Scope scope(String code, String type, boolean lock) {
        // æ ‡ç­¾äºŒç»´ç å†…容可能是整段追溯链接,先拆解为纯码值再匹配
        code = MesTraceCodeUtils.extractCode(code);
        if (StrUtil.isBlank(type) || SCAN_TYPE_AUTO.equalsIgnoreCase(type)) {
            return autoScope(code, lock);
        }
        if ("BAG".equals(type)) {
            MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
            if (bag == null) throw invalidParamException("未找到袋码,请核对扫码类型和码值");
            return new Scope(bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(), Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
            return bagScope(bag);
        }
        if ("PALLET".equals(type)) {
            var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
            if (pallet == null) throw invalidParamException("未找到托盘码,请核对扫码类型和码值");
            if (pallet.getBatchId() == null) throw invalidParamException("托盘尚未绑定生产批次,无法质检");
            return new Scope(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
                    Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
            return palletScope(pallet, lock);
        }
        if ("BATCH".equals(type)) {
            var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
            if (batch == null) throw invalidParamException("未找到生产批次码,请核对扫码类型和码值");
            return new Scope(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
                    Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
            return batchScope(batch, lock);
        }
        throw invalidParamException("不支持的质检扫码类型");
    }
    /** ä¸Žæ‰«ç ç»‘定托盘一致:按码值自动识别类型,依次尝试袋码、托盘码、生产批次码 */
    private Scope autoScope(String code, boolean lock) {
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
        if (bag != null) {
            return bagScope(bag);
        }
        var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
        if (pallet != null) {
            return palletScope(pallet, lock);
        }
        var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
        if (batch != null) {
            return batchScope(batch, lock);
        }
        throw invalidParamException("未找到对应的袋码、托盘码或生产批次码,请核对码值");
    }
    private Scope bagScope(MesProBagCodeDO bag) {
        return new Scope("BAG", bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(),
                Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
    }
    private Scope palletScope(MesProPalletDO pallet, boolean lock) {
        if (pallet.getBatchId() == null) throw invalidParamException("托盘尚未绑定生产批次,无法质检");
        return new Scope("PALLET", StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
                Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
    }
    private Scope batchScope(MesProBatchDO batch, boolean lock) {
        return new Scope("BATCH", StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
                Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
    }
    @Override
@@ -266,7 +308,8 @@
                .filter(q -> !isReturnQuality(q))
                .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
        Map<String, Object> result = new HashMap<>();
        result.put("scanType", scanType); result.put("code", scope.code()); result.put("businessCode", scope.businessCode());
        // è‡ªåŠ¨è¯†åˆ«æ—¶å›žä¼ å®žé™…ç ç±»åž‹ï¼Œå‰ç«¯æ®æ­¤å±•ç¤ºå¯¹è±¡ç±»åž‹å¹¶ç”¨äºŽæäº¤
        result.put("scanType", scope.type()); result.put("code", scope.code()); result.put("businessCode", scope.businessCode());
        result.put("batchCode", scope.batchCode()); result.put("canceled", scope.canceled());
        result.put("totalCount", effective.size());
        result.put("inbound", !effective.isEmpty() && effective.stream().allMatch(b -> b.getInboundEventId() != null));
@@ -278,7 +321,7 @@
        result.put("itemCode", item == null ? null : item.getCode());
        result.put("itemName", item == null ? null : item.getName());
        result.put("indicators", toRangePayload(itemId));
        if ("BAG".equals(scanType)) {
        if ("BAG".equals(scope.type())) {
            List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCode(scope.code());
            MesQcSeedQualityDO quality = qualities.stream()
                    .filter(q -> !isReturnQuality(q)).findFirst().orElse(null);
@@ -1076,9 +1119,11 @@
        Map<String, Object> variables = new HashMap<>();
        variables.put("qcLine", qcLine);
        variables.put("batchCode", batchCode);
        String processDefinitionKey = Objects.equals(qcLine, QC_LINE_SALES)
                ? SALES_LINE_PROCESS_KEY : LINE_PROCESS_KEY;
        String processInstanceId = processInstanceApi.createProcessInstance(operatorId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(LINE_PROCESS_KEY)
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(qcLine + "#" + batchCode)
                        .setVariables(variables));
        for (MesQcSeedQualityDO quality : pending) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/listener/MesQcSeedLineStatusListener.java
@@ -1,35 +1,39 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
 * ç§å­è´¨æ£€æœ¬çº¿é¢†å¯¼å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 * ç§å­è´¨æ£€éƒ¨é—¨é¢†å¯¼å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 *
 * <p>流程定义 key:mes_seed_quality_line_approve;businessKey æ ¼å¼ï¼š{质检线}#{生产批次号}。
 * å®¡æ‰¹é€šè¿‡åŽæœ¬çº¿è¿›å…¥å¤æ£€ï¼Œé©³å›žåŽæœ¬çº¿é‡æ–°åˆæ£€ã€‚
 * <p>加工部、销售部各使用独立流程定义;businessKey æ ¼å¼ï¼š{质检线}#{生产批次号}。
 * å®¡æ‰¹é€šè¿‡åŽæœ¬çº¿è¿›å…¥å…¬å¸è´¨æ£€å¾…审,驳回后本线重新初检。
 */
@Component
@Slf4j
public class MesQcSeedLineStatusListener extends BpmProcessInstanceStatusEventListener {
public class MesQcSeedLineStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
    private static final String BPM_PROCESS_DEFINITION_KEY = "mes_seed_quality_line_approve";
    /** åŠ å·¥éƒ¨é¢†å¯¼å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    private static final String LINE_PROCESS_KEY = "mes_seed_quality_line_approve";
    /** é”€å”®éƒ¨é¢†å¯¼å®¡æ‰¹æµç¨‹å®šä¹‰æ ‡è¯† */
    private static final String SALES_LINE_PROCESS_KEY = "mes_seed_quality_sales_line_approve";
    private static final Set<String> PROCESS_KEYS = Set.of(LINE_PROCESS_KEY, SALES_LINE_PROCESS_KEY);
    @Resource
    private MesQcSeedQualityService seedQualityService;
    @Override
    protected String getProcessDefinitionKey() {
        return BPM_PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        if (!PROCESS_KEYS.contains(event.getProcessDefinitionKey())) {
            return;
        }
        if (!BpmProcessInstanceStatusEnum.isProcessEndStatus(event.getStatus())
                || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(event.getStatus())) {
            return;
@@ -37,19 +41,19 @@
        String businessKey = event.getBusinessKey();
        int index = businessKey == null ? -1 : businessKey.indexOf('#');
        if (index <= 0 || index == businessKey.length() - 1) {
            log.warn("[onEvent][本线审批 businessKey({}) æ ¼å¼æ— æ•ˆï¼Œåº”为 è´¨æ£€çº¿#批次号]", businessKey);
            log.warn("[onApplicationEvent][部门审批 businessKey({}) æ ¼å¼æ— æ•ˆï¼Œåº”为 è´¨æ£€çº¿#批次号]", businessKey);
            return;
        }
        Integer qcLine;
        try {
            qcLine = Integer.valueOf(businessKey.substring(0, index));
        } catch (NumberFormatException ex) {
            log.warn("[onEvent][本线审批 businessKey({}) è´¨æ£€çº¿æ— æ•ˆ]", businessKey);
            log.warn("[onApplicationEvent][部门审批 businessKey({}) è´¨æ£€çº¿æ— æ•ˆ]", businessKey);
            return;
        }
        String batchCode = businessKey.substring(index + 1);
        boolean approved = BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(event.getStatus());
        log.info("[onEvent][本线审批完成,批次={} è´¨æ£€çº¿={} ç»“æžœ={}]", batchCode, qcLine, event.getStatus());
        log.info("[onApplicationEvent][部门审批完成,批次={} è´¨æ£€çº¿={} ç»“æžœ={}]", batchCode, qcLine, event.getStatus());
        seedQualityService.handleLineProcessResult(event.getId(), qcLine, batchCode, approved, event.getReason());
    }
}
yudao-module-mes/src/main/resources/bpm/mes_seed_quality_line_approve.bpmn20.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" targetNamespace="http://www.flowable.org/processdef">
  <process id="mes_seed_quality_line_approve" name="种子质检部门审批" isExecutable="true">
  <process id="mes_seed_quality_line_approve" name="种子质检加工部审批" isExecutable="true">
    <startEvent id="start" />
    <sequenceFlow id="start_to_review" sourceRef="start" targetRef="review" />
    <userTask id="review" name="部门领导审批(暂由 admin å¤„理)" flowable:candidateStrategy="30" flowable:candidateParam="1" />
    <userTask id="review" name="加工部领导审批(暂由 admin å¤„理)" flowable:candidateStrategy="30" flowable:candidateParam="1" />
    <sequenceFlow id="review_to_end" sourceRef="review" targetRef="end" />
    <endEvent id="end" />
  </process>
yudao-module-mes/src/main/resources/bpm/mes_seed_quality_sales_line_approve.bpmn20.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" targetNamespace="http://www.flowable.org/processdef">
  <process id="mes_seed_quality_sales_line_approve" name="种子质检销售部审批" isExecutable="true">
    <startEvent id="start" />
    <sequenceFlow id="start_to_review" sourceRef="start" targetRef="review" />
    <userTask id="review" name="销售部领导审批(暂由 admin å¤„理)" flowable:candidateStrategy="30" flowable:candidateParam="1" />
    <sequenceFlow id="review_to_end" sourceRef="review" targetRef="end" />
    <endEvent id="end" />
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_mes_seed_quality_sales_line_approve">
    <bpmndi:BPMNPlane bpmnElement="mes_seed_quality_sales_line_approve" id="BPMNPlane_mes_seed_quality_sales_line_approve">
      <bpmndi:BPMNShape bpmnElement="start" id="BPMNShape_start">
        <omgdc:Bounds height="36.0" width="36.0" x="160.0" y="182.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="review" id="BPMNShape_review">
        <omgdc:Bounds height="80.0" width="100.0" x="250.0" y="160.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="end" id="BPMNShape_end">
        <omgdc:Bounds height="36.0" width="36.0" x="400.0" y="182.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="start_to_review" id="BPMNEdge_start_to_review">
        <omgdi:waypoint x="196.0" y="200.0"></omgdi:waypoint>
        <omgdi:waypoint x="250.0" y="200.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="review_to_end" id="BPMNEdge_review_to_end">
        <omgdi:waypoint x="350.0" y="200.0"></omgdi:waypoint>
        <omgdi:waypoint x="400.0" y="200.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>