From 87733eb3623593da247c051593d64b8ff9e1d3a4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 06 十一月 2026 19:00:39 +0800
Subject: [PATCH] feat(crm,mes): 扩展客户字段与重构批次号生成逻辑

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java |  171 ++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 139 insertions(+), 32 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
index bae3140..43946d8 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
+++ b/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;
@@ -16,10 +20,10 @@
 import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchWorkerMapper;
 import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
 import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
-import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
 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;
@@ -29,12 +33,14 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -50,6 +56,12 @@
 @Slf4j
 public class MesProBatchServiceImpl implements MesProBatchService {
 
+    /**
+     * 鎵规鍙锋牸寮忥細4 浣嶅勾浠� + 3 浣嶆祦姘达紙濡� 2026001锛�
+     */
+    private static final Pattern BATCH_CODE_PATTERN = Pattern.compile("^[0-9]{7}$");
+    private static final int BATCH_CODE_MAX_SERIAL = 999;
+
     @Resource
     private MesProBatchMapper batchMapper;
     @Resource
@@ -61,12 +73,16 @@
 
     @Resource
     private MesProPalletService palletService;
+    @Resource
+    private MesProBagCodeService bagCodeService;
+    @Resource
+    private MesMdmItemService mdmItemService;
+    @Resource
+    private MdmUnitMeasureApi unitMeasureApi;
 
     @Resource
     private MesCodeRedisDAO codeRedisDao;
 
-    @Resource
-    private MesMdAutoCodeRecordService autoCodeRecordService;
     @Resource
     private AdminUserApi adminUserApi;
     @Resource
@@ -82,11 +98,11 @@
         validateRelations(createReqVO);
         createReqVO.setItemId(resolveBindingItem(null, createReqVO));
 
-        // 2. 鐢熸垚鎵规鍙凤紙缂栫爜瑙勫垯锛氬墠缂� + 鏃ユ湡 + 浜х嚎缂栫爜 + 娴佹按鍙凤級
-        String code = generateBatchCode(line.getCode());
+        // 2. 纭畾鎵规鍙凤紙浜哄伐杈撳叆浼樺厛锛屾湭杈撳叆鍒欐寜銆屽勾浠�+3浣嶆祦姘淬�嶈嚜鍔ㄧ敓鎴愶級
+        String code = resolveBatchCode(createReqVO.getCode(), null);
 
         // 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 +110,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 +118,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();
@@ -139,18 +156,18 @@
         if (StrUtil.isNotEmpty(batch.getCode())) {
             throw exception(PRO_BATCH_ALREADY_FILLED);
         }
-        // 2. 鏍¢獙鍏宠仈鏁版嵁骞剁敓鎴愭寮忔壒娆″彿
+        // 2. 鏍¢獙鍏宠仈鏁版嵁骞剁‘瀹氭寮忔壒娆″彿锛堜汉宸ヨ緭鍏ヤ紭鍏堬紝鏈緭鍏ュ垯鑷姩鐢熸垚锛�
         MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId());
         validateRelations(fillReqVO);
         fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO));
-        String code = generateBatchCode(line.getCode());
+        String code = resolveBatchCode(fillReqVO.getCode(), null);
         // 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 +175,7 @@
         insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers());
         // 5. 缁戝畾鎵樼洏锛堢骇鑱旂敓鎴愭寮忔墭鐩樼爜骞跺綍鍏ユ墭鐩樹笅琚嬬爜锛屽け璐ユ暣浣撳洖婊氾級
         bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds());
+        bindTonBagsToBatch(fillReqVO.getId(), fillReqVO.getTonBagIds());
         // 6. 鐢熸垚鎵规瀹屾暣鎬ф憳瑕侊紙琛ュ綍姝e紡淇℃伅鍚庤鐩栵級
         integrityService.recordBatchIntegrity(fillReqVO.getId());
     }
@@ -173,23 +191,20 @@
         validateRelations(updateReqVO);
         updateReqVO.setItemId(resolveBindingItem(oldBatch, updateReqVO));
 
-        // 3. 鏈敓鎴愭寮忔壒娆″彿鎴栦骇绾垮彉鍖栨椂閲嶆柊鐢熸垚鎵规鍙凤紙鎵规鍙峰寘鍚骇绾跨紪鐮佽绱狅級
+        // 3. 鎵规鍙蜂粎鍚勾浠戒笌娴佹按鍙凤紝鐢熸垚鍚庝笉鍐嶉殢浜х嚎鍙樺寲锛涙湭鐢熸垚姝e紡鎵规鍙锋椂琛ョ敓鎴�
         boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode());
         boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId());
-        String code = oldBatch.getCode();
+        String code = codeEmpty ? resolveBatchCode(updateReqVO.getCode(), oldBatch.getId()) : oldBatch.getCode();
         MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class);
-        if (codeEmpty || lineChanged) {
-            // 鍙樻洿浜х嚎鏃讹紝鐩爣浜х嚎蹇呴』鍚敤锛涙湭鍙樻洿鍒欎粎鏍¢獙瀛樺湪锛堥伩鍏嶅仠鐢ㄤ骇绾跨殑鍘嗗彶鎵规鏃犳硶缂栬緫鍏朵粬瀛楁锛�
-            MesProLineDO line = (lineChanged || codeEmpty)
-                    ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId())
-                    : lineService.validateLineExists(updateReqVO.getLineId());
-            code = generateBatchCode(line.getCode());
+        if (lineChanged || codeEmpty) {
+            // 鍙樻洿浜х嚎鏃讹紝鐩爣浜х嚎蹇呴』鍚敤
+            MesProLineDO line = lineService.validateLineExistsAndEnable(updateReqVO.getLineId());
             updateObj.setLineCode(line.getCode()).setLineName(line.getName());
         }
         // 4. 鏇存柊鎵规锛堝嬀閫夌粦瀹氭墭鐩樻椂鎵规鐩存帴杩涘叆鐢熶骇涓紱鍙栨秷鍕鹃�変笉鍥為��锛屽凡缁戝畾鎵樼洏涓嶅彲瑙g粦锛�
-        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 +214,7 @@
         insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers());
         // 6. 缁戝畾鏂板鍕鹃�夌殑鎵樼洏锛堢骇鑱旂敓鎴愭寮忔墭鐩樼爜骞跺綍鍏ユ墭鐩樹笅琚嬬爜锛屽け璐ユ暣浣撳洖婊氾級
         bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds());
+        bindTonBagsToBatch(updateReqVO.getId(), updateReqVO.getTonBagIds());
         // 7. 鎽樿瑕嗙洊鑼冨洿鍚搧绉�/浜х嚎/鐢熶骇鏃ユ湡/璁″垝鏁伴噺锛岀紪杈戝悗閲嶆柊鐢熸垚閬垮厤璇姤绡℃敼
         integrityService.recordBatchIntegrity(updateReqVO.getId());
     }
@@ -252,6 +268,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 +372,15 @@
      * 鐢熸垚姝e紡鎵樼洏鐮佸苟绾ц仈褰曞叆鎵樼洏涓嬫湭缁戞壒娆$殑琚嬬爜锛屽け璐ユ暣浣撳洖婊�
      */
     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,10 +396,46 @@
             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) {
@@ -376,16 +449,50 @@
     }
 
     /**
-     * 鐢熸垚鎵规鍙�
-     * <p>
-     * 缂栫爜瑙勫垯锛氬浐瀹氬墠缂�(PC) + 鏃ユ湡(yyyyMMdd) + 杈撳叆瀛楃(浜х嚎缂栫爜) + 娴佹按鍙�(鎸夋棩寰幆)
+     * 纭畾鎵规鍙凤細浜哄伐杈撳叆浼樺厛锛屾湭杈撳叆鍒欐寜銆屽綋鍓嶅勾浠� + 3 浣嶆祦姘淬�嶈嚜鍔ㄧ敓鎴�
      *
-     * @param lineCode 浜х嚎缂栫爜
-     * @return 鎵规鍙�
+     * @param requestedCode 浜哄伐杈撳叆鐨勬壒娆″彿锛屽彲涓虹┖
+     * @param selfId        褰撳墠鎵规缂栧彿锛堟洿鏂版椂鎺掗櫎鑷韩锛屾柊澧炰紶 null锛�
+     * @return 鏈�缁堟壒娆″彿
      */
-    private String generateBatchCode(String lineCode) {
-        String inputChar = StrUtil.emptyToDefault(lineCode, "");
-        return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar);
+    private String resolveBatchCode(String requestedCode, Long selfId) {
+        if (StrUtil.isBlank(requestedCode)) {
+            return generateNextBatchCode();
+        }
+        String code = StrUtil.trim(requestedCode);
+        validateBatchCode(code, selfId);
+        return code;
+    }
+
+    /**
+     * 鏍¢獙鎵规鍙锋牸寮忥紙4 浣嶅勾浠� + 3 浣嶆祦姘达紝濡� 2026001锛夊強鍞竴鎬�
+     */
+    private void validateBatchCode(String code, Long selfId) {
+        if (!BATCH_CODE_PATTERN.matcher(code).matches()) {
+            throw exception(PRO_BATCH_CODE_INVALID);
+        }
+        int year = Integer.parseInt(code.substring(0, 4));
+        int serial = Integer.parseInt(code.substring(4));
+        if (year < 2000 || year > 2100 || serial < 1) {
+            throw exception(PRO_BATCH_CODE_INVALID);
+        }
+        MesProBatchDO existing = batchMapper.selectByCodeIgnoreDeleted(code);
+        if (existing != null && !Objects.equals(existing.getId(), selfId)) {
+            throw exception(PRO_BATCH_CODE_DUPLICATE, code);
+        }
+    }
+
+    /**
+     * 鐢熸垚褰撳墠骞翠唤鐨勪笅涓�涓壒娆″彿
+     */
+    private String generateNextBatchCode() {
+        String year = String.valueOf(LocalDate.now().getYear());
+        Integer maxSerial = batchMapper.selectMaxCodeSerialByYear(year);
+        int serial = codeRedisDao.allocateBatchNo(year, maxSerial);
+        if (serial > BATCH_CODE_MAX_SERIAL) {
+            throw exception(PRO_BATCH_CODE_EXHAUSTED);
+        }
+        return year + String.format("%03d", serial);
     }
 
     /**

--
Gitblit v1.9.3