tuqin
19 小时以前 0ddbb4ec727ffce844ef672107e3410d84b1f11a
feat: 业务编码改为随机16位数字并保证全系统唯一

将全系统业务编码由自增序列改为随机16位纯数字;用 Redis 集合 biz:code:used 查重(SADD)保证全系统唯一,冲突重试;启动兜底改为集合丢失时载入库内已有编码。
已修改8个文件
已添加1个文件
已删除1个文件
316 ■■■■■ 文件已修改
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/BizNoUtils.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizCodeSetInitializer.java 87 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -1,29 +1,30 @@
package cn.iocoder.yudao.framework.common.util.number;
import java.util.concurrent.ThreadLocalRandom;
/**
 * ä¸šåŠ¡ç¼–ç ï¼ˆå•æ®å· / ä¸»æ•°æ®ç¼–码)生成的公共约定。
 *
 * <p>需求:系统中所有自动生成的业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一(任意两条编码都不重复)。
 * <p>需求:系统中所有自动生成的业务编码统一为「16 ä½çº¯æ•°å­—」,且**随机生成、不使用自增/顺序**,
 * åŒæ—¶ä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä»»æ„ä¸¤æ¡ç¼–码都不重复)。
 *
 * <p>实现约定:全系统共用同一个 Redis è‡ªå¢ž KEY({@link #GLOBAL_SEQ_KEY}),
 * æ¯æ¬¡ç”Ÿæˆæ—¶å¯¹å…¶æ‰§è¡Œ INCR å¾—到单调递增的序列号,再左补 '0' åˆ° {@link #CODE_LENGTH} ä½ã€‚
 * ç”±äºŽæ‰€æœ‰ç¼–码都来自同一条递增序列,故天然保证「全系统唯一」。
 *
 * <p>注意:
 * <p>实现约定:
 * <ul>
 *     <li>该 KEY ä¸è®¾ç½®è¿‡æœŸæ—¶é—´ï¼Œé¿å…è¿‡æœŸå½’零导致重号;</li>
 *     <li>Redis è‹¥å‘生数据丢失 / æ¸…空,需要从数据库现有编码的最大值恢复计数器,否则可能重号。</li>
 *     <li>编码 = éšæœº 16 ä½æ•°å­—(范围 0 ~ 10^16-1,左侧补 '0');</li>
 *     <li>唯一性由 Redis é›†åˆ {@link #USED_SET_KEY} æŸ¥é‡ä¿è¯ï¼šéšæœºç”ŸæˆåŽå†™å…¥é›†åˆï¼Œ
 *         å†™æˆåŠŸï¼ˆè¿”å›ž 1)说明没用过才采用,否则重试;</li>
 *     <li>各生成点:见 {@link #randomCode()} ä¸Ž {@link #MAX_RETRY}。</li>
 * </ul>
 *
 * <p>注意:Redis é›†åˆä¸¢å¤±åŽï¼ŒåŽ†å²ç¼–ç å¯èƒ½è¢«é‡æ–°éšæœºå‘½ä¸­ï¼›åº”ç”¨å¯åŠ¨æ—¶ä¼šå°†åº“å†…å·²æœ‰ç¼–ç 
 * é‡æ–°è½½å…¥é›†åˆï¼ˆè§ yudao-server çš„启动兜底)。
 */
public class BizNoUtils {
    /**
     * å…¨ç³»ç»Ÿä¸šåŠ¡ç¼–ç ç»Ÿä¸€ä½¿ç”¨çš„ Redis è‡ªå¢ž KEY。
     *
     * <p>所有模块(erp / crm / trade / aftersales / hrm / bpm / mes)共用此 KEY,
     * ä»¥ä¿è¯ã€Œå…¨ç³»ç»Ÿå”¯ä¸€ã€ã€‚
     * å·²ä½¿ç”¨ä¸šåŠ¡ç¼–ç çš„ Redis é›†åˆ KEY,用于随机编码查重,保证全系统唯一。
     */
    public static final String GLOBAL_SEQ_KEY = "biz:seq_no:global";
    public static final String USED_SET_KEY = "biz:code:used";
    /**
     * ä¸šåŠ¡ç¼–ç çš„æ€»é•¿åº¦ï¼š16 ä½
@@ -31,13 +32,24 @@
    public static final int CODE_LENGTH = 16;
    /**
     * å°†è‡ªå¢žåºåˆ—号格式化为 16 ä½çº¯æ•°å­—编码(左侧补 '0')
     *
     * @param seq è‡ªå¢žåºåˆ—号
     * @return 16 ä½çº¯æ•°å­—编码
     * éšæœºç¼–码的空间上限(不含):10^16
     */
    public static String format(Long seq) {
        return String.format("%0" + CODE_LENGTH + "d", seq);
    private static final long CODE_SPACE = 10_000_000_000_000_000L;
    /**
     * ç”Ÿæˆéšæœºç¼–码时的最大重试次数(命中已用编码时重试)
     */
    public static final int MAX_RETRY = 10;
    /**
     * ç”Ÿæˆä¸€ä¸ªéšæœº 16 ä½çº¯æ•°å­—编码(左侧补 '0'),**不做查重**。
     *
     * <p>调用方需自行用 {@link #USED_SET_KEY} æŸ¥é‡ï¼Œå‘½ä¸­åˆ™é‡è¯•。
     *
     * @return éšæœº 16 ä½çº¯æ•°å­—编码
     */
    public static String randomCode() {
        return String.format("%0" + CODE_LENGTH + "d", ThreadLocalRandom.current().nextLong(CODE_SPACE));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -34,8 +34,15 @@
     */
    public String generate(String prefix) {
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
@@ -27,8 +27,15 @@
     */
    public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) {
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
@@ -47,8 +47,15 @@
     */
    public String generate(String prefix) {
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -56,8 +56,15 @@
     */
    public String generate(String prefix) {
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -135,8 +135,15 @@
     */
    public String generate(String prefix, int digits) {
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
@@ -23,8 +23,15 @@
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate() {
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java
@@ -100,8 +100,15 @@
                                MesMdAutoCodeContext context) {
        // éœ€æ±‚:所有业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一。
        // å› æ­¤ä¸å†æŒ‰è§„则分段拼装(固定字符/日期段会带入字母),改为取全系统统一自增序列。
        Long no = stringRedisTemplate.opsForValue().increment(BizNoUtils.GLOBAL_SEQ_KEY);
        return BizNoUtils.format(no);
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
    /**
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizCodeSetInitializer.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
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>业务编码为「随机 16 ä½çº¯æ•°å­—」,唯一性靠 Redis é›†åˆï¼ˆ{@link BizNoUtils#USED_SET_KEY})查重。
 * è‹¥ Redis ä¸¢å¤±è¯¥é›†åˆï¼Œå·²ç”Ÿæˆçš„历史编码会被"忘记",之后可能被重新随机命中而重号。
 *
 * <p>因此:启动时若发现该集合 **不存在**,则从数据库中扫描所有业务编码列里的
 * ã€Œ16 ä½çº¯æ•°å­—」编码,重新载入集合,恢复防重能力。
 *
 * <p>说明:
 * <ul>
 *     <li>仅在集合缺失时扫描(正常启动无额外开销);</li>
 *     <li>best-effort,失败只告警、不影响启动;</li>
 *     <li>旧的带字母编码会被忽略。</li>
 * </ul>
 */
@Component
@Slf4j
public class BizCodeSetInitializer implements ApplicationRunner {
    /** é›†åˆåˆå§‹åŒ–标记(非 16 ä½ï¼Œä¸ä¼šä¸ŽçœŸå®žç¼–码冲突),用于在库内无编码时也标记为已初始化 */
    private static final String INIT_MARKER = "__initialized__";
    private final StringRedisTemplate stringRedisTemplate;
    private final JdbcTemplate jdbcTemplate;
    public BizCodeSetInitializer(StringRedisTemplate stringRedisTemplate, DataSource dataSource) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public void run(ApplicationArguments args) {
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(BizNoUtils.USED_SET_KEY))) {
            log.info("[run][已用编码集合兜底] é›†åˆå·²å­˜åœ¨ï¼Œè·³è¿‡åº“内扫描");
            return;
        }
        long start = System.currentTimeMillis();
        long total = 0L;
        try {
            String schema = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class);
            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);
            for (Map<String, Object> column : columns) {
                String table = String.valueOf(column.get("table_name"));
                String col = String.valueOf(column.get("column_name"));
                if (table.toLowerCase().startsWith("qrtz_") || table.toLowerCase().startsWith("act_")) {
                    continue;
                }
                List<String> codes = jdbcTemplate.queryForList(
                        "SELECT `" + col + "` FROM `" + table + "` WHERE `" + col + "` REGEXP '^[0-9]{"
                                + BizNoUtils.CODE_LENGTH + "}$'",
                        String.class);
                if (!codes.isEmpty()) {
                    stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, codes.toArray(new String[0]));
                    total += codes.size();
                }
            }
            // æ ‡è®°ä¸ºå·²åˆå§‹åŒ–,避免下次启动重复扫描
            stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, INIT_MARKER);
            log.info("[run][已用编码集合兜底] é›†åˆç¼ºå¤±ï¼Œå·²è½½å…¥åº“内 {} ä¸ªç¼–码", total);
        } catch (Exception e) {
            log.warn("[run][已用编码集合兜底] åˆå§‹åŒ–失败(不影响启动),请排查", e);
        } finally {
            log.info("[run][已用编码集合兜底] åº“内扫描耗时 {} ms", System.currentTimeMillis() - start);
        }
    }
}
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizNoSeqInitializer.java
ÎļþÒÑɾ³ý