编码规则(订单流水号生成)
一、前言
在电商、金融等业务场景中,订单流水号的作用不言而喻。它不仅能唯一标识一个订单,还能方便地追踪订单状态。本文将分享一种使用Java编写的编码规则,用于生成订单流水号。
二、编码规则设计
在设计订单流水号编码规则时,我们需要考虑以下几个要素:
- 唯一性:确保每个订单流水号在系统中内唯一
- 可读性:流水号应具有一定的可读性,便于人工识别 例如 ( DD202410250002 ) 代表订单2024年0月25日的第二个订单
- 规律性:流水号应具有一定的规律性,方便后续数据检查
三、数据库设计
1、编码生成规则 ( auto_code_rule )
- 如果不存在多租户可删除 tenant_id 字段
- create_dept、create_by、create_time、update_by、update_time 均为 Entity 基类字段
- status 为字典格式
- del_flag 为 MybatisPlus 逻辑删除字段
sql文件
CREATE TABLE `auto_code_rule` (
`rule_id` bigint(20) NOT NULL COMMENT '规则id',
`tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '租户编号',
`rule_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '规则编码',
`rule_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '规则名称',
`rule_length` int(11) NULL DEFAULT NULL COMMENT '编码长度',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'Y' COMMENT '状态',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
`create_dept` bigint(20) NULL DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`rule_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '编码生成规则表' ROW_FORMAT = Dynamic;
2、编码生成规则组成 ( auto_code_part )
- 如果不存在多租户可删除 tenant_id 字段
- rule_id 为逻辑外键
- create_dept、create_by、create_time、update_by、update_time 均为Entity基类字段
- del_flag 为 MybatisPlus 逻辑删除字段
sql文件
CREATE TABLE `auto_code_part` (
`part_id` bigint(20) NOT NULL COMMENT '分段ID',
`tenant_id` varchar(20) DEFAULT NULL COMMENT '租户编号',
`rule_id` bigint(20) DEFAULT NULL COMMENT '规则ID',
`part_index` int(11) DEFAULT NULL COMMENT '分段序号',
`part_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '分段名称',
`part_length` int(11) DEFAULT NULL COMMENT '分段长度',
`part_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '分段类型,DATETIME:日期时间,FIXEDCHAR:固定字符,SERIALNUMBER:流水号',
`fixed_character` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '固定字符',
`date_format` varchar(50) DEFAULT NULL COMMENT '日期格式',
`seria_start_no` int(11) DEFAULT NULL COMMENT '流水号起始值',
`seria_step` int(11) DEFAULT NULL COMMENT '流水号步长',
`cycle_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '流水号是否循环',
`cycle_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '循环方式,YEAR:按年,MONTH:按月,DAY:按天,HOUR:按小时,MINITE:按分钟',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`part_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='编码生成规则组成表';
3、编码生成记录 ( auto_code_result )
- 如果不存在多租户可删除 tenant_id 字段
- create_dept、create_by、create_time、update_by、update_time 均为 Entity 基类字段
- del_flag 为 MybatisPlus 逻辑删除字段
sql文件
CREATE TABLE `auto_code_result` (
`code_id` bigint(20) NOT NULL COMMENT '记录ID',
`tenant_id` varchar(20) DEFAULT NULL COMMENT '租户编号',
`part_id` bigint(20) DEFAULT NULL COMMENT '规则ID',
`gen_index` int(11) DEFAULT NULL COMMENT '最后产生的序号',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`code_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='编码生成记录表';
四、后端代码实现
省略连接数据库以及实体类 只展示逻辑方法
- 声明常量
private static final String PART_TYPE_FIXEDCHAR = "FIXEDCHAR";
private static final String PART_TYPE_DATETIME = "DATETIME";
private static final String PART_TYPE_SERIALNUMBER = "SERIALNUMBER";
public static final String CYCLE_METHOD_YEAR = "YEAR";
public static final String CYCLE_METHOD_MONTH = "MONTH";
public static final String CYCLE_METHOD_DAY = "DAY";
- 导入mapper接口
@Autowired
private AutoCodePartMapper autoCodePartMapper;
@Autowired
private AutoCodeResultMapper autoCodeResultMapper;
@Autowired
private AutoCodeRuleMapper autoCodeRuleMapper;
- 编写主方法
/**
* 生成流水号
*
* @param ruleCode 规则编码
* @return 流水号
*/
public String genSerialCodeByCode(String ruleCode) {
// 获取分布式锁
RLock lock = RedisUtils.getClient().getLock("serialCodeLock:" + ruleCode);
try {
// 获取锁
lock.lock();
// 获取规则
AutoCodeRule autoCodeRule = autoCodeRuleMapper.selectOne(
Wrappers.lambdaQuery(AutoCodeRule.class)
.eq(AutoCodeRule::getRuleCode, ruleCode)
);
// 获取规则下的所有规则段
List<AutoCodePart> autoCodePartList = autoCodePartMapper.selectList(
Wrappers.lambdaQuery(AutoCodePart.class)
.eq(AutoCodePart::getRuleId, autoCodeRule.getRuleId())
.orderByAsc(AutoCodePart::getPartIndex)
);
StringBuilder serialCode = new StringBuilder();
// 循环规则段
for (AutoCodePart autoCodePart : autoCodePartList) {
switch (autoCodePart.getPartType()) {
case PART_TYPE_FIXEDCHAR:
// 固定字符
serialCode.append(autoCodePart.getFixedCharacter());
break;
case PART_TYPE_DATETIME:
// 日期时间
String dateFormat = autoCodePart.getDateFormat();
serialCode.append(DateUtil.format(new Date(), dateFormat));
break;
case PART_TYPE_SERIALNUMBER:
// 流水号
serialCode.append(genSerialNumber(autoCodePart));
break;
default:
break;
}
}
return serialCode.toString();
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
- 生成流水号方法
/**
* 生成流水号
*
* @param autoCodePart 规则段
* @return 流水号
*/
private String genSerialNumber(AutoCodePart autoCodePart) {
List<AutoCodeResult> autoCodeResultList = switch (autoCodePart.getCycleMethod()) {
case CYCLE_METHOD_YEAR ->
// 按年循环
genSerialNumberByYear(autoCodePart);
case CYCLE_METHOD_MONTH ->
// 按月循环
genSerialNumberByMonth(autoCodePart);
case CYCLE_METHOD_DAY ->
// 按天循环
genSerialNumberByDay(autoCodePart);
default -> Collections.emptyList();
};
// 最新流水号
long index;
// 获取当前流水号
if (autoCodeResultList != null && !autoCodeResultList.isEmpty()) {
index = autoCodeResultList.get(0).getGenIndex() + autoCodePart.getSeriaStep();
} else {
// 获取流水号
index = autoCodePart.getSeriaStartNo();
}
// 保存流水号
AutoCodeResult autoCodeResult = new AutoCodeResult();
autoCodeResult.setPartId(autoCodePart.getPartId());
autoCodeResult.setGenIndex(index);
autoCodeResultMapper.insert(autoCodeResult);
// 获取流水号长度
long length = autoCodePart.getPartLength();
// 补齐
if (Long.toString(index).length() > length) {
throw new RuntimeException("流水号长度超出限制");
}
return String.format("%0" + length + "d", index);
}
- 循环周期对应生成流水号方法
/**
* 按年循环
*
* @param autoCodePart 规则段
* @return 流水号记录列表
*/
private List<AutoCodeResult> genSerialNumberByYear(AutoCodePart autoCodePart) {
// 获取当前年的开始和结束时间
DateTime startOfYear = DateUtil.beginOfYear(DateUtil.date());
DateTime endOfYear = DateUtil.endOfYear(DateUtil.date());
return autoCodeResultMapper.selectList(
Wrappers.lambdaQuery(AutoCodeResult.class)
.eq(AutoCodeResult::getPartId, autoCodePart.getPartId())
.between(AutoCodeResult::getCreateTime, startOfYear, endOfYear)
.orderByDesc(AutoCodeResult::getCreateTime)
.orderByDesc(AutoCodeResult::getGenIndex)
);
}
/**
* 按月循环
*
* @param autoCodePart 规则段
* @return 流水号记录列表
*/
private List<AutoCodeResult> genSerialNumberByMonth(AutoCodePart autoCodePart) {
// 获取当前年的开始和结束时间
DateTime startOfMonth = DateUtil.beginOfMonth(DateUtil.date());
DateTime endOfMonth = DateUtil.endOfMonth(DateUtil.date());
return autoCodeResultMapper.selectList(
Wrappers.lambdaQuery(AutoCodeResult.class)
.eq(AutoCodeResult::getPartId, autoCodePart.getPartId())
.between(AutoCodeResult::getCreateTime, startOfMonth, endOfMonth)
.orderByDesc(AutoCodeResult::getCreateTime)
.orderByDesc(AutoCodeResult::getGenIndex)
);
}
/**
* 按天循环
*
* @param autoCodePart 规则段
* @return 流水号记录列表
*/
private List<AutoCodeResult> genSerialNumberByDay(AutoCodePart autoCodePart) {
// 获取当前年的开始和结束时间
DateTime startOfDay = DateUtil.beginOfDay(DateUtil.date());
DateTime endOfDay = DateUtil.endOfDay(DateUtil.date());
return autoCodeResultMapper.selectList(
Wrappers.lambdaQuery(AutoCodeResult.class)
.eq(AutoCodeResult::getPartId, autoCodePart.getPartId())
.between(AutoCodeResult::getCreateTime, startOfDay, endOfDay)
.orderByDesc(AutoCodeResult::getCreateTime)
.orderByDesc(AutoCodeResult::getGenIndex)
);
}
- 使用
@Autowired
private AutoCodeService autoCodeService;
String code = autoCodeService.genSerialCodeByCode(code);
五、前端页面效果
六、注意事项
- 市面上绝大多数流水号都由 头中尾 三部分组成,头部大多为固定字符、中部为日期、尾部为流水号
- 本片以分段序号来判断拼接流水号的顺序,分段长度需根据实际业务需要来判断
- 本篇采用 JDK17 的 switch 新语法
- DateTime、DateUtil 均为 hutool 包下
官方文档 - > https://hutool.cn
- 采用 Redisson 作为分布式锁,与确保流水号生成的唯一性
- 采用 MybatisPlus 作为 ORM 框架
官方文档 -> https://baomidou.com