编码规则(订单流水号生成)

一、前言

在电商、金融等业务场景中,订单流水号的作用不言而喻。它不仅能唯一标识一个订单,还能方便地追踪订单状态。本文将分享一种使用Java编写的编码规则,用于生成订单流水号。

二、编码规则设计

在设计订单流水号编码规则时,我们需要考虑以下几个要素:

  • 唯一性:确保每个订单流水号在系统中内唯一
  • 可读性:流水号应具有一定的可读性,便于人工识别 例如 ( DD202410250002 ) 代表订单2024年0月25日的第二个订单
  • 规律性:流水号应具有一定的规律性,方便后续数据检查

三、数据库设计

1、编码生成规则 ( auto_code_rule )

在这里插入图片描述

  1. 如果不存在多租户可删除 tenant_id 字段
  2. create_dept、create_by、create_time、update_by、update_time 均为 Entity 基类字段
  3. status 为字典格式
  4. 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 )

在这里插入图片描述

  1. 如果不存在多租户可删除 tenant_id 字段
  2. rule_id 为逻辑外键
  3. create_dept、create_by、create_time、update_by、update_time 均为Entity基类字段
  4. 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 )

在这里插入图片描述

  1. 如果不存在多租户可删除 tenant_id 字段
  2. create_dept、create_by、create_time、update_by、update_time 均为 Entity 基类字段
  3. 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='编码生成记录表';

四、后端代码实现

省略连接数据库以及实体类 只展示逻辑方法
  1. 声明常量
    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";
  1. 导入mapper接口
	@Autowired
    private AutoCodePartMapper autoCodePartMapper;
    @Autowired
    private AutoCodeResultMapper autoCodeResultMapper;
    @Autowired
    private AutoCodeRuleMapper autoCodeRuleMapper;
  1. 编写主方法
	/**
     * 生成流水号
     *
     * @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();
            }
        }
    }
  1. 生成流水号方法
	/**
     * 生成流水号
     *
     * @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);
    }
  1. 循环周期对应生成流水号方法
	/**
     * 按年循环
     *
     * @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)
        );
    }
  1. 使用
	@Autowired
	private AutoCodeService autoCodeService;

	String code = autoCodeService.genSerialCodeByCode(code);

五、前端页面效果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

六、注意事项

  • 市面上绝大多数流水号都由 头中尾 三部分组成,头部大多为固定字符、中部为日期、尾部为流水号
  • 本片以分段序号来判断拼接流水号的顺序,分段长度需根据实际业务需要来判断
  • 本篇采用 JDK17 的 switch 新语法

jdk17新特性——Switch表达式增强

  • DateTime、DateUtil 均为 hutool 包下

官方文档 - > https://hutool.cn

  • 采用 Redisson 作为分布式锁,与确保流水号生成的唯一性
  • 采用 MybatisPlus 作为 ORM 框架

官方文档 -> https://baomidou.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值