package cn.iocoder.yudao.module.qcreport.service.instance; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; /** * 报告编号生成。 *
* 格式 {@code QR + yyyyMMdd + "-" + 4 位流水},例如 {@code QR20260918-0001}。 * 流水按天独立,跨天从 0001 重新开始——报告编号是给人看的单据号,按天分段最好念、最好找。 *
* 这里只负责「算」:取当天流水、拼字符串。落库时的并发冲突由调用方重试兜底, * 所以这个类不依赖数据库、不依赖 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 时自然加宽。 *
* 锁 {@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); } /** * 由「当天已有的最大编号」推出下一个序号。 *
* 不是同一天、格式不认识、或还没有任何编号时,都从 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; } }