11 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
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
package cn.iocoder.yudao.module.qcreport.service.instance;
 
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
 
/**
 * 报告编号生成。
 * <p>
 * 格式 {@code QR + yyyyMMdd + "-" + 4 位流水},例如 {@code QR20260918-0001}。
 * 流水按天独立,跨天从 0001 重新开始——报告编号是给人看的单据号,按天分段最好念、最好找。
 * <p>
 * 这里只负责「算」:取当天流水、拼字符串。落库时的并发冲突由调用方重试兜底,
 * 所以这个类不依赖数据库、不依赖 Spring,可以直接单测。
 */
public final class ReportNoGenerator {
 
    /** 编号前缀。QR = Quality Report */
    private static final String PREFIX = "QR";
 
    /**
     * 同样锁 {@link Locale#ROOT}:{@code ofPattern} 取默认 locale,
     * 而默认 locale 自带历法(如泰历),会让年份整体偏移。
     */
    private static final DateTimeFormatter DAY_FORMAT =
            DateTimeFormatter.ofPattern("yyyyMMdd", Locale.ROOT);
 
    /** 序号位数不足时补到这个长度 */
    private static final int SEQUENCE_WIDTH = 4;
 
    /**
     * 序号最多认这么长。编号由本类生成,正常就是 4~6 位;
     * 出现更长的尾巴说明存进去的不是本生成器的产物,当作「不认识」而不是硬解析。
     */
    private static final int MAX_SEQUENCE_DIGITS = 9;
 
    private ReportNoGenerator() {
    }
 
    /** 某天的编号前缀,形如 {@code QR20260918-} */
    public static String prefix(LocalDate day) {
        return PREFIX + DAY_FORMAT.format(day) + "-";
    }
 
    /**
     * 拼出完整报告编号,形如 {@code QR20260918-0001};序号超过 9999 时自然加宽。
     * <p>
     * 锁 {@link Locale#ROOT}:{@code %d} 会跟随默认 locale 选数字字形,
     * 服务器 locale 一旦不是拉丁语系,编号里就会出现阿拉伯-印度数字之类的非 ASCII 字符。
     */
    public static String format(LocalDate day, int sequence) {
        return prefix(day) + String.format(Locale.ROOT, "%0" + SEQUENCE_WIDTH + "d", sequence);
    }
 
    /**
     * 由「当天已有的最大编号」推出下一个序号。
     * <p>
     * 不是同一天、格式不认识、或还没有任何编号时,都从 1 开始:这些情况都意味着
     * 「今天是第一天」,而不是出错——比如传进来的是昨天的号。
     *
     * @param latestReportNo 当天已有的最大报告编号,可为 null
     * @param day            出件当天
     * @return 下一个序号,至少为 1
     */
    public static int nextSequence(String latestReportNo, LocalDate day) {
        if (latestReportNo == null) {
            return 1;
        }
        String prefix = prefix(day);
        if (!latestReportNo.startsWith(prefix)) {
            return 1;
        }
        String tail = latestReportNo.substring(prefix.length());
        if (tail.length() < SEQUENCE_WIDTH || tail.length() > MAX_SEQUENCE_DIGITS) {
            return 1;
        }
        for (int i = 0; i < tail.length(); i++) {
            if (!Character.isDigit(tail.charAt(i))) {
                return 1;
            }
        }
        return Integer.parseInt(tail) + 1;
    }
 
}