tuqin
13 小时以前 7580d02bd3f9399143a6a388732658bea2610bc4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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;
    }
 
}