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;
|
}
|
|
}
|