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; /** * 业务编码计数器兜底初始化。 * *

全系统编码统一由 Redis 的全局自增 KEY({@link BizNoUtils#GLOBAL_SEQ_KEY})生成, * 用于保证「16 位纯数字 + 全系统唯一」。但 Redis 是内存存储,若被清空 / 丢失, * 该 KEY 会归零,从 1 重新开始,导致与历史编码重复。 * *

因此:当启动时发现该 KEY **不存在**(说明 Redis 丢失过计数), * 从数据库中扫描所有业务编码列的最大值,把计数器恢复到该最大值,避免重号。 * *

说明: *

*/ @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> 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 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; } }