liu
2026-10-24 fc1288355802c1b8e69fe08f544df675fb807572
fix(erp): 修复销售订单审批未生成MPS
已添加1个文件
已修改2个文件
177 ■■■■■ 文件已修改
sql/mysql/fix_sale_order_mps_link.sql 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/fix_sale_order_mps_link.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
-- ============================================================
-- é”€å”®è®¢å• -> ç”Ÿäº§ä¸»è®¡åˆ’(MPS)链路数据检查与修复
--
-- é€‚用场景:销售订单已经审核,但勾选“需要生产”的明细没有生成 MPS。
-- ä»£ç å‘布后,新审核单据会在 MPS åˆ›å»ºå¤±è´¥æ—¶æ•´ä½“回滚并返回明确错误。
-- ============================================================
-- 1. æ£€æŸ¥ï¼šéœ€è¦ç”Ÿäº§çš„销售明细,是否已经同步为 MES äº§å“ã€‚
-- mes_item_id ä¸ºç©ºè¡¨ç¤ºæ²¡æœ‰ MES è®°å½•ï¼›item_or_product ä¸æ˜¯ PRODUCT è¡¨ç¤ºä¸èƒ½ç”¨äºŽ MPS。
SELECT so.id                         AS sale_order_id,
       so.no                         AS sale_order_no,
       soi.id                        AS sale_order_item_id,
       mdm.id                        AS mdm_item_id,
       mdm.code                      AS item_code,
       mdm.name                      AS item_name,
       mi.id                         AS mes_item_id,
       mi.mdm_item_id                AS mes_mdm_item_id,
       mit.item_or_product           AS item_or_product,
       mi.status                     AS mes_item_status
FROM erp_sale_order so
JOIN erp_sale_order_items soi ON soi.order_id = so.id AND soi.deleted = b'0'
JOIN mdm_item mdm ON mdm.id = soi.product_id AND mdm.deleted = b'0'
LEFT JOIN mes_md_item mi ON mi.mdm_item_id = mdm.id AND mi.deleted = b'0'
LEFT JOIN mes_md_item_type mit ON mit.id = mi.item_type_id AND mit.deleted = b'0'
WHERE so.deleted = b'0'
  AND soi.need_production = 1
  AND (mi.id IS NULL
       OR COALESCE(mi.status, 1) <> 0
       OR COALESCE(mit.item_or_product, '') <> 'PRODUCT')
ORDER BY so.id, soi.id;
-- 2. ä¿®å¤ï¼šåŽ†å² MES ç‰©æ–™æŒ‰ç¼–码已存在、但 mdm_item_id ä¸ºç©ºæ—¶ï¼Œè¡¥é½ MDM å…³è”。
-- mdm_item.code æ˜¯å…¨å±€å”¯ä¸€ç¼–码;仅更新两边都未删除、关联为空且编码相同的数据。
UPDATE mes_md_item mi
JOIN mdm_item mdm ON mdm.code = mi.code AND mdm.deleted = b'0'
LEFT JOIN mes_md_item linked ON linked.mdm_item_id = mdm.id AND linked.deleted = b'0'
SET mi.mdm_item_id = mdm.id,
    mi.updater = '1',
    mi.update_time = NOW()
WHERE mi.deleted = b'0'
  AND mi.mdm_item_id IS NULL
  AND linked.id IS NULL;
-- 3. ä¿®å¤ï¼šMPS å·²å­˜åœ¨ä½†é”€å”®æ˜Žç»†çš„ mps_id æœªå›žå¡«æ—¶ï¼Œè¡¥é½å…³è”。
-- å·²å–消 MPS(status=3)不回填。
UPDATE erp_sale_order_items soi
JOIN (
    SELECT sale_order_item_id, MAX(id) AS mps_id
    FROM mes_pro_mps
    WHERE deleted = b'0'
      AND status <> 3
      AND sale_order_item_id IS NOT NULL
    GROUP BY sale_order_item_id
) mps ON mps.sale_order_item_id = soi.id
SET soi.mps_id = mps.mps_id,
    soi.updater = '1',
    soi.update_time = NOW()
WHERE soi.deleted = b'0'
  AND (soi.mps_id IS NULL OR soi.mps_id <> mps.mps_id);
-- 4. æ£€æŸ¥ï¼šå·²ç»å®¡æ ¸ã€éœ€è¦ç”Ÿäº§ï¼Œä½†ä»æ²¡æœ‰æœ‰æ•ˆ MPS çš„历史订单。
SELECT so.id                  AS sale_order_id,
       so.no                  AS sale_order_no,
       soi.id                 AS sale_order_item_id,
       mdm.code               AS item_code,
       mdm.name               AS item_name,
       soi.count              AS plan_quantity
FROM erp_sale_order so
JOIN erp_sale_order_items soi ON soi.order_id = so.id AND soi.deleted = b'0'
JOIN mdm_item mdm ON mdm.id = soi.product_id AND mdm.deleted = b'0'
LEFT JOIN mes_pro_mps mps ON mps.sale_order_item_id = soi.id
                         AND mps.deleted = b'0'
                         AND mps.status <> 3
WHERE so.deleted = b'0'
  AND so.status = 20
  AND soi.need_production = 1
  AND mps.id IS NULL
ORDER BY so.id, soi.id;
-- 5. åŽ†å²å•æ®é‡è¯•ï¼ˆæŒ‰å•æ‰§è¡Œï¼‰ï¼šå…ˆç¡®è®¤ç¬¬ 1、4 æ­¥æ£€æŸ¥ç»“果已经正常,再填写订单 ID。
-- å°†è¯¥è®¢å•退回“待审核(10)”,然后在页面重新点击“审批”;新代码会自动生成 MPS。
SET @sale_order_id = 0; -- æ”¹ä¸ºéœ€è¦é‡è¯•的销售订单 ID;0 è¡¨ç¤ºä¸æ‰§è¡Œä»»ä½•æ›´æ–°
UPDATE erp_sale_order so
SET so.status = 10,
    so.updater = '1',
    so.update_time = NOW()
WHERE so.id = @sale_order_id
  AND @sale_order_id > 0
  AND so.deleted = b'0'
  AND so.status = 20
  AND COALESCE(so.out_count, 0) = 0
  AND EXISTS (
      SELECT 1
      FROM erp_sale_order_items soi
      LEFT JOIN mes_pro_mps mps ON mps.sale_order_item_id = soi.id
                               AND mps.deleted = b'0'
                               AND mps.status <> 3
      WHERE soi.order_id = so.id
        AND soi.deleted = b'0'
        AND soi.need_production = 1
        AND mps.id IS NULL
  );
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -31,17 +31,14 @@
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
@@ -67,6 +64,8 @@
@Validated
@Slf4j
public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
    private static final Integer MPS_STATUS_CANCELED = 3;
    @Resource
    private ErpSaleOrderMapper saleOrderMapper;
@@ -94,22 +93,6 @@
    private ErpAccountMapper accountMapper;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private PlatformTransactionManager transactionManager;
    /**
     * MPS åˆ›å»ºä½¿ç”¨ç‹¬ç«‹äº‹åŠ¡ï¼ˆREQUIRES_NEW):
     * å®¡æ ¸æ—¶è‡ªåŠ¨åˆ›å»º MPS å±žäºŽå°½åŠ›è€Œä¸ºçš„è”åŠ¨ï¼Œå¤±è´¥ä»…è®°å½•æ—¥å¿—å¹¶å›žæ»šè‡ªèº«ï¼Œ
     * ä¸èƒ½æ±¡æŸ“销售订单审核事务(否则内层异常会把外层事务标记为 rollback-only,导致整个审核被回滚)
     */
    private TransactionTemplate mpsTransactionTemplate;
    @PostConstruct
    public void initMpsTransactionTemplate() {
        mpsTransactionTemplate = new TransactionTemplate(transactionManager);
        mpsTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
@@ -237,31 +220,36 @@
            return;
        }
        int mpsCount = 0;
        int createdCount = 0;
        int reusedCount = 0;
        for (ErpSaleOrderItemDO item : items) {
            if (item.getNeedProduction() == null || item.getNeedProduction() != 1) {
                continue;
            }
            try {
                // ç‹¬ç«‹äº‹åŠ¡åˆ›å»º MPS:失败只回滚 MPS è‡ªèº«ï¼Œä¸å½±å“å®¡æ ¸äº‹åŠ¡
                Long mpsId = mpsTransactionTemplate.execute(txStatus -> {
                    Long newMpsId = createMpsForSaleOrderItem(saleOrder, item);
                    if (newMpsId != null) {
                        // æ›´æ–°é”€å”®è®¢å•明细的 MPS å…³è”
                        saleOrderItemMapper.updateById(new ErpSaleOrderItemDO()
                                .setId(item.getId())
                                .setMpsId(newMpsId));
                    }
                    return newMpsId;
                });
                if (mpsId != null) {
                    mpsCount++;
            // åŒä¸€é”€å”®æ˜Žç»†åªä¿ç•™ä¸€æ¡æœ‰æ•ˆ MPS,避免反审核后再次审核产生重复计划
            MesProMpsRespDTO existingMps = mpsApi.getMpsBySaleOrderItemId(item.getId());
            if (existingMps != null && ObjectUtil.notEqual(existingMps.getStatus(), MPS_STATUS_CANCELED)) {
                if (ObjectUtil.notEqual(item.getMpsId(), existingMps.getId())) {
                    saleOrderItemMapper.updateById(new ErpSaleOrderItemDO()
                            .setId(item.getId())
                            .setMpsId(existingMps.getId()));
                }
            } catch (Exception e) {
                log.error("[createMpsForSaleOrder] åˆ›å»º MPS å¤±è´¥ï¼ŒsaleOrderItemId={}", item.getId(), e);
                reusedCount++;
                log.info("[createMpsForSaleOrder] é”€å”®è®¢å•明细({}) å·²å­˜åœ¨æœ‰æ•ˆ MPS({}),跳过重复创建",
                        item.getId(), existingMps.getId());
                continue;
            }
            // ä¸Žé”€å”®è®¢å•审核共用事务;任一明细创建失败都会回滚审核状态和本次已创建的 MPS
            Long mpsId = createMpsForSaleOrderItem(saleOrder, item);
            saleOrderItemMapper.updateById(new ErpSaleOrderItemDO()
                    .setId(item.getId())
                    .setMpsId(mpsId));
            createdCount++;
        }
        log.info("[createMpsForSaleOrder] é”€å”®è®¢å•({}) å®¡æ ¸å®Œæˆï¼Œåˆ›å»º MPS {} æ¡", orderId, mpsCount);
        log.info("[createMpsForSaleOrder] é”€å”®è®¢å•({}) å®¡æ ¸å®Œæˆï¼Œåˆ›å»º MPS {} æ¡ï¼Œå¤ç”¨ MPS {} æ¡",
                orderId, createdCount, reusedCount);
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java
@@ -64,14 +64,20 @@
    }
    default MesProMpsDO selectBySaleOrderItemId(Long saleOrderItemId) {
        return selectOne(MesProMpsDO::getSaleOrderItemId, saleOrderItemId);
        return selectOne(new LambdaQueryWrapperX<MesProMpsDO>()
                .eq(MesProMpsDO::getSaleOrderItemId, saleOrderItemId)
                .orderByDesc(MesProMpsDO::getId)
                .last("LIMIT 1"));
    }
    default List<MesProMpsDO> selectListBySaleOrderItemIds(Collection<Long> saleOrderItemIds) {
        if (saleOrderItemIds == null || saleOrderItemIds.isEmpty()) {
            return Collections.emptyList();
        }
        return selectList(MesProMpsDO::getSaleOrderItemId, saleOrderItemIds);
        // åŒä¸€é”€å”®æ˜Žç»†å¯èƒ½å­˜åœ¨â€œæ—§è®¡åˆ’已取消、新计划重新创建”的历史记录;按 ID å‡åºï¼Œè°ƒç”¨æ–¹å–最后一条即最新计划
        return selectList(new LambdaQueryWrapperX<MesProMpsDO>()
                .in(MesProMpsDO::getSaleOrderItemId, saleOrderItemIds)
                .orderByAsc(MesProMpsDO::getId));
    }
    /**