tuqin
13 小时以前 7580d02bd3f9399143a6a388732658bea2610bc4
feat: 业务编码统一为16位纯数字并保证全系统唯一

所有自动生成的业务编码(单据号/主数据编码/袋码/托盘码/临时码)统一改为纯16位数字,全系统共用同一 Redis 自增序列保证唯一;新增启动兜底:Redis 计数丢失时从库内最大编码恢复。
已添加4个文件
已修改9个文件
422 ■■■■■ 文件已修改
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/BizNoUtils.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/trace/MesProTraceResolveController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizNoSeqInitializer.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/BizNoUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.common.util.number;
/**
 * ä¸šåŠ¡ç¼–ç ï¼ˆå•æ®å· / ä¸»æ•°æ®ç¼–码)生成的公共约定。
 *
 * <p>需求:系统中所有自动生成的业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一(任意两条编码都不重复)。
 *
 * <p>实现约定:全系统共用同一个 Redis è‡ªå¢ž KEY({@link #GLOBAL_SEQ_KEY}),
 * æ¯æ¬¡ç”Ÿæˆæ—¶å¯¹å…¶æ‰§è¡Œ INCR å¾—到单调递增的序列号,再左补 '0' åˆ° {@link #CODE_LENGTH} ä½ã€‚
 * ç”±äºŽæ‰€æœ‰ç¼–码都来自同一条递增序列,故天然保证「全系统唯一」。
 *
 * <p>注意:
 * <ul>
 *     <li>该 KEY ä¸è®¾ç½®è¿‡æœŸæ—¶é—´ï¼Œé¿å…è¿‡æœŸå½’零导致重号;</li>
 *     <li>Redis è‹¥å‘生数据丢失 / æ¸…空,需要从数据库现有编码的最大值恢复计数器,否则可能重号。</li>
 * </ul>
 */
public class BizNoUtils {
    /**
     * å…¨ç³»ç»Ÿä¸šåŠ¡ç¼–ç ç»Ÿä¸€ä½¿ç”¨çš„ Redis è‡ªå¢ž KEY。
     *
     * <p>所有模块(erp / crm / trade / aftersales / hrm / bpm / mes)共用此 KEY,
     * ä»¥ä¿è¯ã€Œå…¨ç³»ç»Ÿå”¯ä¸€ã€ã€‚
     */
    public static final String GLOBAL_SEQ_KEY = "biz:seq_no:global";
    /**
     * ä¸šåŠ¡ç¼–ç çš„æ€»é•¿åº¦ï¼š16 ä½
     */
    public static final int CODE_LENGTH = 16;
    /**
     * å°†è‡ªå¢žåºåˆ—号格式化为 16 ä½çº¯æ•°å­—编码(左侧补 '0')
     *
     * @param seq è‡ªå¢žåºåˆ—号
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public static String format(Long seq) {
        return String.format("%0" + CODE_LENGTH + "d", seq);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.aftersales.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.aftersales.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * å”®åŽæ¨¡å—单号 Redis DAO
@@ -30,17 +25,17 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
@@ -1,16 +1,10 @@
package cn.iocoder.yudao.module.bpm.dal.redis;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
import static cn.hutool.core.date.DatePattern.*;
/**
 * BPM æµç¨‹ Id ç¼–码的 Redis DAO
@@ -24,38 +18,17 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å®šä¹‰çš„ processIdRule è§„则生成
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param processIdRule è§„则
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code processIdRule} å…¥å‚,但不再参与编码拼接。
     *
     * @param processIdRule æµç¨‹ Id è§„则(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) {
        // ç”Ÿæˆæ—¥æœŸå‰ç¼€
        String infix = "";
        switch (processIdRule.getInfix()) {
            case "DAY":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN);
                break;
            case "HOUR":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HH");
                break;
            case "MINUTE":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HHmm");
                break;
            case "SECOND":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATETIME_PATTERN);
                break;
        }
        // ç”Ÿæˆåºå·
        String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix();
        String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        if (StrUtil.isEmpty(infix)) {
            // ç‰¹æ®Šï¼šæ²¡æœ‰å‰ç¼€ï¼Œåˆ™ä¸èƒ½è¿‡æœŸï¼Œä¸èƒ½æ¯æ¬¡éƒ½æ˜¯ä»Ž 0 å¼€å§‹ã€‚可见 https://t.zsxq.com/MU1E2 è®¨è®º
            stringRedisTemplate.expire(key, Duration.ofDays(1L));
        }
        return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.crm.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.crm.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
@@ -43,20 +38,17 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ä¾‹å¦‚说:QTRK 202109 000001 ï¼ˆæ²¡æœ‰ä¸­é—´ç©ºæ ¼ï¼‰
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        // é€’增序号
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.erp.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.erp.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
@@ -52,20 +47,17 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ä¾‹å¦‚说:CGDD 202109 000001 ï¼ˆæ²¡æœ‰ä¸­é—´ç©ºæ ¼ï¼‰
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        // é€’增序号
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -1,13 +1,9 @@
package cn.iocoder.yudao.module.hrm.dal.redis;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * HRM ç¼–号生成的 Redis DAO
@@ -131,19 +127,16 @@
    }
    /**
     * ç”Ÿæˆç¼–号
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @param digits ä½æ•°
     * @return ç¼–号
     * <p>为兼容既有调用方,保留入参,但不再参与编码拼接。
     *
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix, int digits) {
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = HrmRedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´ 1 å¤©
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%0" + digits + "d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/trace/MesProTraceResolveController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.trace;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
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 org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MES æ‰«ç è¿½æº¯ - ç¼–码类型解析。
 *
 * <p>编码统一为「16 ä½çº¯æ•°å­—」后,无法再按前缀判断类型(BAG-/PL-/PB- ç­‰å‰ç¼€å·²å–消),
 * å‰ç«¯æ‰«ç åŽå…ˆè°ƒç”¨æœ¬æŽ¥å£æŒ‰ç¼–码反查其所属类型,再调用对应类型的追溯接口。
 */
@Tag(name = "管理后台 - MES æ‰«ç æº¯æºï¼ˆç±»åž‹è§£æžï¼‰")
@RestController
@RequestMapping("/mes/pro/trace")
@Validated
public class MesProTraceResolveController {
    @Resource
    private MesProBagCodeService bagCodeService;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesProBatchService batchService;
    @GetMapping("/resolve-type")
    @Operation(summary = "根据编码解析溯源类型:bag / pallet / batch;均不匹配返回空")
    @Parameter(name = "code", description = "编码(二维码内容)", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')")
    public CommonResult<String> resolveType(@RequestParam("code") String code) {
        if (bagCodeService.getBagCodeByCode(code) != null) {
            return success("bag");
        }
        if (palletService.getPalletByCode(code) != null) {
            return success("pallet");
        }
        if (batchService.getBatchByCode(code) != null) {
            return success("batch");
        }
        return success(null);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.dal.redis;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
/**
 * MES ä¸šåŠ¡ç¼–ç ç”Ÿæˆçš„ Redis DAO(实例码:袋码 / æ‰˜ç›˜ç  / ä¸´æ—¶ç ç­‰ï¼‰ã€‚
 *
 * <p>需求:所有业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一。
 * è¿™é‡Œå¤ç”¨å…¨ç³»ç»Ÿç»Ÿä¸€çš„ Redis è‡ªå¢ž KEY,保证与其它模块的编码也不重复。
 */
@Repository
public class MesCodeRedisDAO {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆä¸€ä¸ª 16 ä½çº¯æ•°å­—业务编码(全系统唯一)
     *
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate() {
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
@@ -13,6 +14,7 @@
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodePartStrategy;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -41,6 +43,9 @@
    private MesMdAutoCodeRuleService ruleService;
    @Resource
    private MesMdAutoCodePartService partService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private List<MesMdAutoCodePartStrategy> strategyList;
@@ -93,23 +98,10 @@
     */
    private String generateCode(MesMdAutoCodeRuleDO rule, List<MesMdAutoCodePartDO> parts,
                                MesMdAutoCodeContext context) {
        StringBuilder codeBuilder = new StringBuilder();
        for (MesMdAutoCodePartDO part : parts) {
            MesMdAutoCodePartStrategy strategy = strategyMap.get(part.getType());
            Assert.notNull(strategy, () -> exception(AUTO_CODE_GENERATE_FAILED));
            // ç”Ÿæˆåˆ†æ®µç¼–码
            String partCode = strategy.generate(part, context);
            // ç»Ÿä¸€æˆªå–指定长度
            partCode = StrUtil.sub(partCode, 0, part.getLength());
            // æ‹¼æŽ¥åˆ†æ®µç¼–码
            codeBuilder.append(partCode);
        }
        // è¡¥é½å¤„理
        String result = codeBuilder.toString();
        if (Boolean.TRUE.equals(rule.getPadded()) && rule.getMaxLength() != null) {
            result = padCode(result, rule);
        }
        return result;
        // éœ€æ±‚:所有业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一。
        // å› æ­¤ä¸å†æŒ‰è§„则分段拼装(固定字符/日期段会带入字母),改为取全系统统一自增序列。
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -25,6 +25,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeReplaceMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
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.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
@@ -85,6 +86,9 @@
     * è®¡é‡å•位类型「N千克」:每托盘装 N ä¸ªè¢‹ç 
     */
    private static final Pattern KG_TYPE_PATTERN = Pattern.compile("^(\\d{1,5})千克$");
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -163,7 +167,7 @@
            int bagNo = startNo + i;
            bagCodes.add(new MesProBagCodeDO()
                    .setUuid(uuid)
                    .setCode("BAG-" + StrUtil.padPre(String.valueOf(bagNo), 8, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBagNo(bagNo)
                    .setItemId(item.getId())
                    .setItemCode(item.getCode())
@@ -339,7 +343,7 @@
            MesProPalletDO pallet = oldBagCode.getPalletId() == null ? null : palletMap.get(oldBagCode.getPalletId());
            MesProBagCodeDO newBagCode = new MesProBagCodeDO()
                    .setUuid(replaceUuid)
                    .setCode(batch.getCode() + "-" + StrUtil.padPre(String.valueOf(newBagNo), 4, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBagNo(newBagNo)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -15,6 +15,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
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;
@@ -60,6 +61,9 @@
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
@@ -109,7 +113,7 @@
        List<MesProBatchDO> list = new ArrayList<>(reqVO.getCount());
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProBatchDO()
                    .setTempCode("PB-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setTempCode(codeRedisDao.generate())
                    .setBagQuantity(0)
                    .setPalletQuantity(0)
                    .setStatus(MesProBatchStatusEnum.DRAFT.getStatus())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
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.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
@@ -48,6 +49,9 @@
    @Resource
    private MesProBatchMapper batchMapper;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> batchCreatePallet(MesProPalletBatchCreateReqVO createReqVO) {
@@ -71,7 +75,7 @@
        for (int i = 0; i < createReqVO.getCount(); i++) {
            int palletNo = startNo + i;
            pallets.add(new MesProPalletDO()
                    .setCode(batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo)
@@ -97,7 +101,8 @@
        List<MesProPalletDO> list = new ArrayList<>(reqVO.getCount());
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProPalletDO()
                    .setTempCode("PL-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setTempCode(codeRedisDao.generate())
                    .setCapacity(reqVO.getCapacity())
                    .setBagCount(0)
                    .setAutoGenerated(false)
                    .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())
@@ -164,7 +169,7 @@
            int palletNo = startNo + i;
            palletMapper.updateById(new MesProPalletDO()
                    .setId(pallets.get(i).getId())
                    .setCode(batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo));
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizNoSeqInitializer.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.server.framework.bizno;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
/**
 * ä¸šåŠ¡ç¼–ç è®¡æ•°å™¨å…œåº•åˆå§‹åŒ–ã€‚
 *
 * <p>全系统编码统一由 Redis çš„全局自增 KEY({@link BizNoUtils#GLOBAL_SEQ_KEY})生成,
 * ç”¨äºŽä¿è¯ã€Œ16 ä½çº¯æ•°å­— + å…¨ç³»ç»Ÿå”¯ä¸€ã€ã€‚但 Redis æ˜¯å†…存存储,若被清空 / ä¸¢å¤±ï¼Œ
 * è¯¥ KEY ä¼šå½’零,从 1 é‡æ–°å¼€å§‹ï¼Œå¯¼è‡´ä¸ŽåŽ†å²ç¼–ç é‡å¤ã€‚
 *
 * <p>因此:当启动时发现该 KEY **不存在**(说明 Redis ä¸¢å¤±è¿‡è®¡æ•°ï¼‰ï¼Œ
 * ä»Žæ•°æ®åº“中扫描所有业务编码列的最大值,把计数器恢复到该最大值,避免重号。
 *
 * <p>说明:
 * <ul>
 *     <li>仅在 KEY ç¼ºå¤±æ—¶æ‰«æï¼ˆæ­£å¸¸å¯åŠ¨æ— é¢å¤–å¼€é”€ï¼‰ï¼›</li>
 *     <li>扫描是 best-effort,失败只告警、不影响启动;</li>
 *     <li>只统计「16 ä½çº¯æ•°å­—」的编码,旧的带字母编码会被忽略。</li>
 * </ul>
 */
@Component
@Slf4j
public class BizNoSeqInitializer implements ApplicationRunner {
    private final StringRedisTemplate stringRedisTemplate;
    private final JdbcTemplate jdbcTemplate;
    public BizNoSeqInitializer(StringRedisTemplate stringRedisTemplate, DataSource dataSource) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public void run(ApplicationArguments args) {
        String current = stringRedisTemplate.opsForValue().get(BizNoUtils.GLOBAL_SEQ_KEY);
        if (current != null) {
            log.info("[run][业务编码计数器兜底] Redis è®¡æ•°å·²å­˜åœ¨({}),跳过库内扫描", current);
            return;
        }
        long start = System.currentTimeMillis();
        try {
            long maxDb = computeMaxCodeInDb();
            stringRedisTemplate.opsForValue().set(BizNoUtils.GLOBAL_SEQ_KEY, String.valueOf(maxDb));
            log.info("[run][业务编码计数器兜底] Redis è®¡æ•°ç¼ºå¤±ï¼Œå·²ä»Žåº“内最大编码恢复为 {}", maxDb);
        } catch (Exception e) {
            log.warn("[run][业务编码计数器兜底] åˆå§‹åŒ–失败(不影响启动),请排查", e);
        } finally {
            log.info("[run][业务编码计数器兜底] åº“内扫描耗时 {} ms", System.currentTimeMillis() - start);
        }
    }
    /**
     * æ‰«æåº“内所有可能存放业务编码的列,返回「16 ä½çº¯æ•°å­—」编码的最大值。
     */
    private long computeMaxCodeInDb() {
        String schema = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class);
        // æ‰¾å‡ºæ‰€æœ‰ varchar/char ä¸”名字像编码的列(no / code / *_no / *_code / temp_code)
        List<Map<String, Object>> columns = jdbcTemplate.queryForList(
                "SELECT table_name, column_name FROM information_schema.columns " +
                        "WHERE table_schema = ? AND data_type IN ('varchar', 'char') " +
                        "AND (column_name = 'no' OR column_name = 'code' OR column_name = 'temp_code' " +
                        "     OR column_name LIKE '%\\_no' OR column_name LIKE '%\\_code')",
                schema);
        if (columns.isEmpty()) {
            return 0L;
        }
        StringBuilder sql = new StringBuilder("SELECT MAX(v) FROM (");
        boolean first = true;
        for (Map<String, Object> column : columns) {
            String table = String.valueOf(column.get("table_name"));
            String col = String.valueOf(column.get("column_name"));
            // è·³è¿‡ Quartz æ¡†æž¶è¡¨ï¼Œé¿å…æ— å…³æ‰«æ
            if (table.toLowerCase().startsWith("qrtz_") || table.toLowerCase().startsWith("act_")) {
                continue;
            }
            if (!first) {
                sql.append(" UNION ALL ");
            }
            first = false;
            sql.append("SELECT MAX(CAST(`").append(col).append("` AS UNSIGNED)) AS v FROM `").append(table)
                    .append("` WHERE `").append(col).append("` REGEXP '^[0-9]{").append(BizNoUtils.CODE_LENGTH).append("}$'");
        }
        if (first) {
            return 0L;
        }
        sql.append(") t");
        Long max = jdbcTemplate.queryForObject(sql.toString(), Long.class);
        return max == null ? 0L : max;
    }
}