单号生成器
简介
NumGeneratorHelper
是一个用于生成带有前缀、日期、随机字符串等格式的唯一业务单号工具类。支持多种内容类型(数字、小写字母、大写字母、混合字符、雪花ID、UUID ,ObjectId等),并提供灵活的模板配置和分段拼接功能。
快速上手
生成单号
通过NumGeneratorHelper.generateNo()
即可生成单号,generateNo()
提供了多个参数配置重载方法可以见重载方法表。
示例:
Go
String ckd = NumGeneratorHelper.generateNo(
18, // 长度
NumContentTypeEnum.MIXED_CHARACTERS,// 随机数类型
"CKD", // 前缀
"$P-$T$S" // 模板字符串
);
// 返回: CKD-20250529mZe2qIM
// 多段生成示例----------------------------------------------------------------------------
LinkedHashMap<Integer, NumContentTypeEnum> map= new LinkedHashMap<>();
map.put(3, NumContentTypeEnum.NUMBERS);
map.put(4, NumContentTypeEnum.LOWERCASE_LETTERS);
map.put(5, NumContentTypeEnum.UPPERCASE_LETTERS);
map.put(6, NumContentTypeEnum.MIXEd_CHARACTERS);
String rkd= NumContentTypeEnum.generateNo("CKD",map,'_');
// 返回:CKD_741_nnwh_KCUWT_VfnGwP
生成自增单号
通过NumGeneratorHelper.generateNoAutoIncrement()
可以返回一个自增的单号方法提供了重载可自定义分隔符和日期格式。
示例:
Java
// 自增单号生成示例-----------------------------------------------------------------------
String autoIncrementNo = NumGeneratorHelper.generateNoAutoIncrement("CKD", '-', "yyyyMMdd");
// 返回 CKD-202553029507
单号唯一性校验
通过NumGeneratorHelper.unique()
可以校验唯一性,需传入两个参数第一个参数为生成参数的方法,第二个参数为校验唯一性的方法,比如:在数据库中查询是否唯一。
示例:
Java
// 唯一校验示例----------------------------------------------------------------------------
String uniquedNumber = NumGeneratorHelper.unique(
NumGeneratorHelper::generateNo,
(number) -> {
MPJLambdaWrapper<Order> wrapper = new MPJLambdaWrapper<>();
wrapper.eq(Order::getOrderNo, number);
return orderService.count(wrapper) > 0;
}
);
重载方法:
方法名 | 描述 |
---|---|
generateNo() | 使用默认配置生成单号(前缀 YT+ 雪花ID) |
generateNo(int length) | 指定长度生成混合类型的随机单号 |
generateNo(int length, NOContentTypeEnum contentType) | 指定长度和内容类型生成单号 |
generateNo(int length, String prefix, NOContentTypeEnum contentType) | 自定义长度、前缀、内容类型 |
generateNo(int length, NOContentTypeEnum contentType, String prefix, String format) | 支持自定义模板格式 |
generateNo(int length, NOContentTypeEnum contentType, String prefix, String templateFormat, String dateFormat) | 完全自定义生成规则 |
generateNo(String prefix, Map<Integer, NOContentTypeEnum> segmentsMap, char split) | 根据分段规则拼接生成单号 |
generateNoAutoIncrement(String prefix) | 根据前缀生成自增单号 |
generateNoAutoIncrement(String prefix, String dateFormat) | 根据前缀生成自增单号,自定义时间格式 |
generateNoAutoIncrement(String prefix, char split, String dateFormat) | 根据前缀生成自增单号,自定义时间格式、分隔符 |
常量说明:
常量名称 | 含义 | 示例值 |
---|---|---|
PRE_PLACE = "$P" | 前缀占位符 | 在模板中使用 $P 表示插入前缀 |
DATE_PLACE = "$T" | 时间占位符 | 在模板中使用 $T 表示插入时间 |
SEQ_PLACE = "$S" | 序列号占位符 | 在模板中使用 $S 表示插入随机字符串 |
DEFAULT_NO_LENGTH = 18 | 默认单号总长度 | |
DEFAULT_NO_PREFIX = "YT" | 默认前缀 | |
DEFAULT_NO_FORMAT = "$P$T$S" | 默认模板格式 |
枚举类型(ContentTypeEnum):
枚举值 | 描述 |
---|---|
NUMBERS | 数字 |
LOWERCASE_LETTERS | 小写字母 |
UPPERCASE_LETTERS | 大写字母 |
MIXEd_CHARACTERS | 数字+大小写字母混合 |
SNOWFLAKE_ID | 雪花算法生成的 ID(使用该类型时不支持指定长度) |
UUID | UUID 字符串(使用该类型时不支持指定长度) |
ObjectId | ObjectId 字符串(使用该类型时不支持指定长度) |
随机数工具类
java
public class RandomStringUtils {
private static final String NUMBERS = "0123456789";
private static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";
private static final String UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String ALL_CHARACTERS = NUMBERS + LOWERCASE_LETTERS + UPPERCASE_LETTERS;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// 私有构造器,防止实例化
private RandomStringUtils() {
}
/**
* 生成指定长度的随机字符串(包含大小写字母和数字)
*
* @param length 长度(必须≥1)
* @return 随机字符串
*/
public static String generateMixed(int length) {
return generate(length, true, true, true);
}
/**
* 生成指定长度的纯数字字符串
*
* @param length 长度(必须≥1)
* @return 数字字符串
*/
public static String generateNumbers(int length) {
return generate(length, false, false, true);
}
/**
* 生成指定长度的纯小写字母字符串
*
* @param length 长度(必须≥1)
* @return 小写字母字符串
*/
public static String generateLowercase(int length) {
return generate(length, true, false, false);
}
/**
* 生成指定长度的纯大写字母字符串
*
* @param length 长度(必须≥1)
* @return 大写字母字符串
*/
public static String generateUppercase(int length) {
return generate(length, false, true, false);
}
/**
* 生成自定义组合的随机字符串
*
* @param length 长度(必须≥1)
* @param includeLower 是否包含小写字母
* @param includeUpper 是否包含大写字母
* @param includeNumbers 是否包含数字
* @return 自定义组合的随机字符串
*/
public static String generate(int length, boolean includeLower, boolean includeUpper, boolean includeNumbers) {
if (length < 1) {
throw new ServiceException("生成自定义组合的随机字符串 长度必须≥1");
}
// 构建可用字符集
StringBuilder charSet = new StringBuilder();
if (includeLower) {
charSet.append(LOWERCASE_LETTERS);
}
if (includeUpper) {
charSet.append(UPPERCASE_LETTERS);
}
if (includeNumbers) {
charSet.append(NUMBERS);
}
if (charSet.length() == 0) {
throw new ServiceException("生成自定义组合的随机字符串 必须选择至少一种字符类型");
}
// 使用SecureRandom生成安全随机数(适合敏感场景)
char[] buffer = new char[length];
for (int i = 0; i < length; i++) {
int index = SECURE_RANDOM.nextInt(charSet.length());
buffer[i] = charSet.charAt(index);
}
return new String(buffer);
}
}
代码
工具类
java
package com.ruoyi.common.utils.random;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.ruoyi.common.enums.NumContentTypeEnum;
import com.ruoyi.common.service.SysSerialNumberRecordService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.yootone.utils.spring.SpringUtils;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Description : 单号工具助手
* @Author : LiuJun
* @Date: 2025/5/29 10:32
*/
public class NumGeneratorHelper {
/**
* 单号前缀占位符
*/
public static final String PRE_PLACE = "$P";
/**
* 单号时间占位符
*/
public static final String DATE_PLACE = "$T";
/**
* 单号随机数占位符
*/
public static final String SEQ_PLACE = "$S";
/**
* 默认单号长度
*/
private static final int DEFAULT_NO_LENGTH = 18;
/**
* 默认单号前缀
*/
private static final String DEFAULT_NO_PREFIX = "YT";
private static final int WORKER_ID = 1;
private static final int DATACENTER_ID = 1;
/**
* 单号格式
*/
private static final String DEFAULT_NO_FORMAT = PRE_PLACE + SEQ_PLACE;
private NumGeneratorHelper() {
}
/**
* 生成单号
*
* @return YT+雪花id 例如:YT1927979404944871424
*/
public static String generateNo() {
return generateNo(
DEFAULT_NO_LENGTH,
NumContentTypeEnum.SNOWFLAKE_ID,
DEFAULT_NO_PREFIX,
DEFAULT_NO_FORMAT,
DateUtils.YYYYMMDD);
}
/**
* 生成单号
*
* @param length 长度
* @return 混合字符串 例如:YTABCDedf
*/
public static String generateNo(int length) {
return generateNo(
length,
NumContentTypeEnum.MIXED_CHARACTERS,
DEFAULT_NO_PREFIX,
DEFAULT_NO_FORMAT,
DateUtils.YYYYMMDD);
}
/**
* 生成单号
*
* @param length 长度
* @param contentType 内容类型
* @return 生成的单号字符串 例如:YT123456
*/
public static String generateNo(int length, NumContentTypeEnum contentType) {
return generateNo(
length,
contentType,
DEFAULT_NO_PREFIX,
DEFAULT_NO_FORMAT,
DateUtils.YYYYMMDD);
}
/**
* 生成单号
*
* @param length 长度
* @param prefix 前缀
* @param contentType 内容类型
* @return 生成的单号字符串 自定义前缀+随机数 例如:YT123456
*/
public static String generateNo(int length, String prefix, NumContentTypeEnum contentType) {
return generateNo(
length,
contentType,
prefix,
DEFAULT_NO_FORMAT,
DateUtils.YYYYMMDD);
}
/**
* 生成单号
*
* @param length 长度
* @param contentType 内容类型
* @param prefix 前缀
* @param format 格式
* @return 生成的单号字符串 例如:YT20250101ABCD1234
*/
public static String generateNo(int length, NumContentTypeEnum contentType, String prefix, String format) {
return generateNo(
length,
contentType,
prefix,
format,
DateUtils.YYYYMMDD);
}
/**
* 生成单号
*
* @param length 长度
* @param contentType 内容类型
* @param prefix 前缀
* @param templateFormat 模板格式
* @param dateFormat 日期格式
* @return 生成的单号字符串
*/
public static String generateNo(
int length,
NumContentTypeEnum contentType,
String prefix,
String templateFormat,
String dateFormat
) {
// 校验前缀
if (StringUtils.isEmpty(prefix)) {
throw new IllegalArgumentException("前缀不能为空");
}
// 校验长度
lengthCheck(length, prefix.length(), dateFormat.length());
// 校验字符串模板
templateCheck(templateFormat);
// 解析时间
String dateStr = parseDate(dateFormat);
// 生成随机数
String randomStr = getRandomStr(length - prefix.length() - dateStr.length(), contentType);
// 解析模板格式
return templateFormat
.replace(PRE_PLACE, prefix)
.replace(DATE_PLACE, dateStr)
.replace(SEQ_PLACE, randomStr);
}
/**
* 生成单号
*
* @param prefix 前缀
* @param segmentsMap 分段映射 key为分段长度,value为内容类型
* @param split 分隔符
* @return 生成的单号字符串
*/
public static String generateNo(
String prefix,
Map<Integer, NumContentTypeEnum> segmentsMap,
char split
) {
if (StringUtils.isEmpty(prefix)) {
throw new IllegalArgumentException("前缀不能为空");
}
if (segmentsMap == null || segmentsMap.isEmpty()) {
throw new IllegalArgumentException("分段映射不能为空");
}
StringJoiner joiner = new StringJoiner(String.valueOf(split));
joiner.add(prefix); // 添加前缀
// 遍历分段映射
for (Map.Entry<Integer, NumContentTypeEnum> entry : segmentsMap.entrySet()) {
int segmentLength = entry.getKey();
NumContentTypeEnum contentType = entry.getValue();
// 生成每个分段的随机字符串
String segmentStr = getRandomStr(segmentLength, contentType);
// 添加到结果字符串
joiner.add(segmentStr);
}
return joiner.toString();
}
/**
* 根据前缀生成自增单号
*
* @param prefix 前缀
* @return
*/
public static String generateNoAutoIncrement(String prefix) {
return generateNoAutoIncrement(prefix, '\0', null);
}
/**
* 根据前缀生成自增单号
*
* @param prefix 前缀
* @return
*/
public static String generateNoAutoIncrement(String prefix, String dateFormat) {
return generateNoAutoIncrement(prefix, '\0', dateFormat);
}
/**
* 根据前缀生成自增单号
*
* @param prefix 前缀
* @param split 分隔符
* @return
*/
public static String generateNoAutoIncrement(String prefix, char split, String dateFormat) {
SysSerialNumberRecordService recordService = SpringUtils.getBean(SysSerialNumberRecordService.class);
Long nextNumber = recordService.getNextSerialNumberByPrefix(prefix);
StringJoiner stringJoiner = new StringJoiner(split == '\0' ? "" : String.valueOf(split));
stringJoiner.add(prefix); // 添加前缀
if (StringUtils.isNotEmpty(dateFormat)) {
// 如果指定了日期格式,则添加日期
String dateStr = parseDate(dateFormat);
stringJoiner.add(dateStr);
}
// 添加自增序列号
stringJoiner.add(nextNumber.toString());
return stringJoiner.toString();
}
/**
* 唯一检查
*
* @param idGenerator 生成单号方法
* @param isUnique 检查单号是否唯一方法
* @return 生成的唯一单号
*/
public static String unique(Supplier<String> idGenerator, Predicate<String> isUnique) {
String generatorNo = idGenerator.get();
// 检查是否重复
boolean tested = isUnique.test(generatorNo);
if (!tested) {
// 如果不重复,返回生成的单号
return generatorNo;
}
return unique(idGenerator, isUnique);
}
/**
* 生成随机字符串
*
* @param length 长度
* @param contentType 随机数内容 雪花id和uuid不支持指定长度
* @return 随机字符串
*/
private static String getRandomStr(int length, NumContentTypeEnum contentType) {
// 生成随机数
switch (contentType) {
case NUMBERS:
return RandomStringUtils.generateNumbers(length);
case LOWERCASE_LETTERS:
return RandomStringUtils.generateLowercase(length);
case UPPERCASE_LETTERS:
return RandomStringUtils.generateUppercase(length);
case MIXED_CHARACTERS:
return RandomStringUtils.generateMixed(length);
case SNOWFLAKE_ID:
Snowflake snowflake = IdUtil.getSnowflake(WORKER_ID, DATACENTER_ID);
return snowflake.nextIdStr();
case UUID:
return IdUtil.simpleUUID();
case OBJECT_ID:
return IdUtil.objectId();
default:
throw new IllegalArgumentException("不支持的内容类型: " + contentType);
}
}
/**
* 解析时间
*
* @param dateFormat 时间格式模板
* @return 时间字符串
*/
private static String parseDate(String dateFormat) {
return DateUtils.parseDateToStr(dateFormat, DateUtils.getNowDate());
}
/**
* 校验模板格式
*
* @param templateFormat 模板格式
*/
private static void templateCheck(String templateFormat) {
if (StringUtils.isEmpty(templateFormat)) {
throw new IllegalArgumentException("模板格式不能为空");
}
// 必需的占位符集合
Set<String> requiredPlaceholders = new HashSet<>(Arrays.asList(PRE_PLACE, SEQ_PLACE));
Set<String> foundPlaceholders = new HashSet<>();
// 查找所有占位符
StringBuilder patternBuilder = new StringBuilder();
for (String placeholder : requiredPlaceholders) {
patternBuilder.append(Pattern.quote(placeholder)).append('|');
}
patternBuilder.setLength(patternBuilder.length() - 1); // 移除最后一个'|'
Pattern pattern = Pattern.compile(patternBuilder.toString());
Matcher matcher = pattern.matcher(templateFormat);
while (matcher.find()) {
String matchedPlaceholder = matcher.group();
if (!foundPlaceholders.add(matchedPlaceholder)) {
throw new IllegalArgumentException("模板格式中的占位符 " + matchedPlaceholder + " 出现多次");
}
}
// 检查是否缺少任何占位符
if (!foundPlaceholders.containsAll(requiredPlaceholders)) {
requiredPlaceholders.removeAll(foundPlaceholders);
throw new IllegalArgumentException("模板格式缺少占位符: " + String.join(", ", requiredPlaceholders));
}
}
/**
* 校验单号长度
*
* @param length 单号长度
* @param prefixLength 前缀长度
* @param dateLength 日期长度
*/
private static void lengthCheck(int length, int prefixLength, int dateLength) {
// 长度必须大于0
if (length <= 0) {
throw new IllegalArgumentException("单号长度必须大于0");
}
// 长度必须大于前缀加上日期长度
if (length < prefixLength + dateLength + 1) {
throw new IllegalArgumentException("单号长度必须大于前缀和日期格式的长度之和加一");
}
}
}
数据库表
sql
CREATE TABLE `sys_serial_number_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '单号表主键',
`prefix` varchar(50) NOT NULL COMMENT '单号前缀',
`current_number` bigint(20) NOT NULL DEFAULT 0 COMMENT '当前自增的数值',
`create_time` datetime NULL COMMENT '创建时间',
`update_time` datetime NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `prefix_unique`(`prefix`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单号生成记录表';
实体
java
/**
* 系统单号生成记录表 sys_serial_number_record
*/
@Data
@TableName("sys_serial_number_record")
public class SysSerialNumberRecord {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
@TableField(value = "prefix")
private String prefix;
@TableField(value = "current_number")
private Long currentNumber;
@TableField(value = "create_time")
private Date createTime;
@TableField(value = "update_time")
private Date updateTime;
}
自增服务
这里使用的是MPJ需使用其他的可以自行修改
java
/**
* 单号记录Service接口
*/
public interface SysSerialNumberRecordService extends MPJBaseService<SysSerialNumberRecord> {
/**
* 根据前缀获取下一个单号
* @param prefix 前缀
* @return 下一个单号
*/
Long getNextSerialNumberByPrefix(String prefix);
}
mapper
@Mapper
public interface SysSerialNumberRecordMapper extends MPJBaseMapper<SysSerialNumberRecord> {
}
实现类:
java
package com.ruoyi.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.yulichang.base.MPJBaseServiceImpl;
import com.ruoyi.common.annotation.RedissonLock;
import com.ruoyi.common.core.domain.entity.SysSerialNumberRecord;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.mapper.SysSerialNumberRecordMapper;
import com.ruoyi.common.service.SysSerialNumberRecordService;
import com.ruoyi.common.utils.DateUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* @Description : 单号服务实现
* @Author : LiuJun
* @Date: 2025/5/30 13:51
*/
@Slf4j
@Service
@AllArgsConstructor
public class SysSerialNumberRecordServiceImpl
extends MPJBaseServiceImpl<SysSerialNumberRecordMapper, SysSerialNumberRecord>
implements SysSerialNumberRecordService {
/**
* 默认单号
*/
private static final Long DEFAULT_NUMBER = 0L;
/**
* 缓存单号步长
*/
private static final int CACHE_SERIAL_NUMBER_INTERVAL = 100;
/**
* 单号前缀
*/
private static final String SERIAL_NUMBER_PREFIX = "sys_serial_number:";
private final SysSerialNumberRecordMapper mapper;
private final RedisTemplate<String, String> redisTemplate;
/**
* 根据前缀获取下一个单号
*
* @param prefix 前缀
* @return 下一个单号
*/
@RedissonLock(
prefixKey = "sys_serial_number_record_lock:",
key = {"#prefix"},
waitTime = 3
)
@Transactional(rollbackFor = Exception.class)
@Override
public Long getNextSerialNumberByPrefix(String prefix) {
// 检查缓存中是否存在key
Boolean hasKey = redisTemplate.hasKey(SERIAL_NUMBER_PREFIX + prefix);
if(Boolean.TRUE.equals(hasKey)) {
Long redisNumber = redisTemplate.opsForValue().increment(SERIAL_NUMBER_PREFIX+prefix);
// 检查单号是否超出缓存间隔的倍数
if(redisNumber% CACHE_SERIAL_NUMBER_INTERVAL != 0L){
return redisNumber;
}
// 如果单号是缓存间隔的倍数,说明需要向数据库生成一批单号
SysSerialNumberRecord sysSerialNumberRecord = new SysSerialNumberRecord();
sysSerialNumberRecord.setPrefix(prefix);
sysSerialNumberRecord.setCurrentNumber(redisNumber + CACHE_SERIAL_NUMBER_INTERVAL);
sysSerialNumberRecord.setUpdateTime(DateUtils.getNowDate());
sysSerialNumberRecord.setCreateTime(DateUtils.getNowDate());
LambdaUpdateWrapper<SysSerialNumberRecord> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.eq(SysSerialNumberRecord::getPrefix, prefix);
int updated = mapper.update(sysSerialNumberRecord, updateWrapper);
if(updated <= 0) {
log.error("更新单号记录失败,前缀: {}", prefix);
throw new ServiceException("更新单号记录失败");
}
return redisNumber;
}
LambdaQueryWrapper<SysSerialNumberRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysSerialNumberRecord::getPrefix, prefix);
queryWrapper.last("FOR UPDATE");
SysSerialNumberRecord sysRecord = mapper.selectOne(queryWrapper);
// 验证数据库中是否存在对应的前缀记录
if (sysRecord == null) {
// 没有时插入一个
SysSerialNumberRecord insertRecord = new SysSerialNumberRecord();
insertRecord.setPrefix(prefix);
insertRecord.setCreateTime(DateUtils.getNowDate());
insertRecord.setUpdateTime(DateUtils.getNowDate());
insertRecord.setCurrentNumber(DEFAULT_NUMBER);
boolean saved = save(insertRecord);
if (!saved) {
throw new ServiceException("创建" + prefix + "单号记录出错");
}
redisTemplate.opsForValue().set(SERIAL_NUMBER_PREFIX + prefix, DEFAULT_NUMBER.toString());
// 返回初始单号
return DEFAULT_NUMBER;
}
// 批量申请单号
sysRecord.setCurrentNumber(sysRecord.getCurrentNumber()+ CACHE_SERIAL_NUMBER_INTERVAL);
// 保存更新后的记录
boolean updated = updateById(sysRecord);
if (!updated) {
throw new ServiceException("更新" + prefix + "单号记录出错");
}
// 获取当前单号 存入缓存中
Long currentNumber = sysRecord.getCurrentNumber();
currentNumber++;
redisTemplate.opsForValue().set(SERIAL_NUMBER_PREFIX + prefix, currentNumber.toString());
return currentNumber;
}
}