springBoot3 生成订单号

1 业务背景

js 复制代码
财务软件 发票号码要求连续且唯一

2 序号生成服务

java 复制代码
package com.okyun.sequence.service.impl;


@Service
@Slf4j
@RequiredArgsConstructor
public class GenerateSequenceService {

    private final StringRedisTemplate stringRedisTemplate;
    private final SequenceMapper sequenceMapper;
    private final SnowflakeIdGenerator snowflakeIdGenerator;
    private final SequenceCacheManager  sequenceCacheManager;

    /**
     * 批量生成序列号
     *
     * @param tenantId  租户ID
     * @param orderType 单据类型
     * @param count     需要生成的序列号数量
     * @return 序列号列表
     */
    @Transactional(rollbackFor = Exception.class)
    public List<String> generateSequenceBatch(Long tenantId, String orderType, Long count) {
        long safeCount = (count == null || count <= 0) ? 1 : count;
        // 1 获取序号配置
        Sequence sequence = getSequenceConfig(tenantId, orderType);
        if (sequence == null || sequence.getSequenceRule() == null) {
            // 生成无序随机序号
            return  generateSequenceRandom(safeCount,null);
        }

        // 2 序号前缀
        String sequencePrefix = StringUtils.defaultIfEmpty(sequence.getSequencePrefix(), "");
        if (Objects.equals(sequence.getSequenceRule(), GenerateSequenceConstants.ORDER_SEQUENCE_RULES_RANDOM))
        {
            // 生成无序随机序号
            return  generateSequenceRandom(safeCount, sequencePrefix);
        }

        // 3 根据日期类型获取日期字符串
        String dateStr = StringUtils.isNull(sequence.getDateType()) ? "" : DateUtils.dateTimeNow(sequence.getDateType());

        // 4 当前年份
        Integer currentYear = LocalDate.now().getYear();
        // 5 当前月份(保持2位)
        String currentMonth = String.format("%02d", LocalDate.now().getMonthValue());
        // 为了自动更新凭证起始编号:精度添加 年 + 月
        String redisKey = buildRedisKey(tenantId, orderType, currentYear, currentMonth);
        // 如果是 年 规则模式,则月份只能是"00"
        if (Objects.equals( GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS, sequence.getSequenceRule()))
        {
            redisKey = buildRedisKey(tenantId, orderType, currentYear, GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
        }

        // 获取或初始化 Redis 的序列值
        long startNumber = initializeOrFetchRedisKey(redisKey, sequence, currentYear, currentMonth, safeCount);
        long endNumber = startNumber + safeCount - 1;

        // 持久化更新数据库
        persistSequenceDetail(sequence, currentYear, currentMonth, endNumber);

        // 生成序列号
        List<String> sequenceList = new ArrayList<>();
        for (long i = startNumber; i <= endNumber; i++) {
            String formattedNumber = String.format("%06d", i);
            sequenceList.add(sequencePrefix + dateStr + formattedNumber);
        }

        return sequenceList;
    }


    /**
     * 批量生成 发票序列号
     *
     * @param tenantId  租户ID
     * @param orderType 单据类型
     * @param count     需要生成的序列号数量
     * @return 序列号列表
     */
    @Transactional(rollbackFor = Exception.class)
    public List<InvoiceNo> batchGenerateInvoiceSequence(Long tenantId, String orderType, Long count) {
        long safeCount = (count == null || count <= 0) ? 1 : count;
        // 1 获取序号配置
        Sequence sequence = getSequenceConfig(tenantId, orderType);
        if (sequence == null || sequence.getSequenceRule() == null) {
            throw new ServiceException("请检查发票序号配置!");
        }

        // 2 序号前缀
        String sequencePrefix = StringUtils.defaultIfEmpty(sequence.getSequencePrefix(), "");
        if (!Objects.equals(sequence.getSequenceRule(), GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS))
        {
            throw new ServiceException("请检查发票序号配置规则!发票序号规则必须是年连续!");
        }

        // 3 根据日期类型获取日期字符串
        String dateStr = StringUtils.isNull(sequence.getDateType()) ? "" : DateUtils.dateTimeNow(sequence.getDateType());

        // 4 当前年份
        Integer currentYear = LocalDate.now().getYear();
        // 5 当前月份(保持2位)
        String currentMonth = String.format("%02d", LocalDate.now().getMonthValue());
        // 为了自动更新凭证起始编号:精度添加 年 + 月
        String redisKey = buildRedisKey(tenantId, orderType, currentYear, currentMonth);
        // 如果是 年 规则模式,则月份只能是"00"
        if (Objects.equals( GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS, sequence.getSequenceRule()))
        {
            redisKey = buildRedisKey(tenantId, orderType, currentYear, GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
        }

        // 获取或初始化 Redis 的序列值
        long startNumber = initializeOrFetchRedisKey(redisKey, sequence, currentYear, currentMonth, safeCount);
        long endNumber = startNumber + safeCount - 1;

        // 持久化更新数据库
        persistSequenceDetail(sequence, currentYear, currentMonth, endNumber);

        // 生成序列号
        List<InvoiceNo> sequenceList = new ArrayList<>();
        for (long i = startNumber; i <= endNumber; i++) {
            InvoiceNo invoiceNo = new InvoiceNo();
            invoiceNo.setInvoiceSerie(sequencePrefix + dateStr);
            invoiceNo.setInvoiceNumero(i);
            sequenceList.add(invoiceNo);
        }
        return sequenceList;
    }

    // 构建redisKey
    private String buildRedisKey(Long tenantId, String orderType, Integer currentYear, String currentMonth) {
        return GenerateSequenceConstants.SEQUENCE_KEY + tenantId + ":" + orderType + ":" + currentYear + ":" + currentMonth;
    }

    /**
     * 批量生成随机无序序号
     * @param count
     */
    private List<String> generateSequenceRandom(long count, String sequencePrefix) {
        List<String> sequenceNoList = new ArrayList<>();
        for (int i = 0; i < count; i++)
        {
            String sequenceNo = snowflakeIdGenerator.generateStringId();
            sequenceNoList.add(sequencePrefix + sequenceNo);
        }
        return sequenceNoList;
    }

    /**
     * 初始化或获取 Redis 键值
     * @param redisKey Redis 键
     * @param sequence  序号对象
     * @param currentYear 当前年份
     * @param currentMonth 当前月份
     * @param incrementBy 自增步长
     * @return 返回开始序号 = 当前值 + 1 或 初始值 1
     */
    private Long initializeOrFetchRedisKey(String redisKey, Sequence sequence, Integer currentYear, String currentMonth, long incrementBy) {
        Long currentNumber;
        if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(redisKey))) {
            // 如果 Redis 中没有此 Key,初始化值
            currentNumber = initializeSequenceDetail(sequence, currentYear, currentMonth,  incrementBy);
            long redisValue = currentNumber - 1L + incrementBy;
            stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(redisValue));
            stringRedisTemplate.expire(redisKey, 365, TimeUnit.DAYS);
            return currentNumber;
        } else {
            // 如果 Key 存在,自增
            currentNumber = stringRedisTemplate.opsForValue().increment(redisKey, incrementBy);
            if (currentNumber == null) {
                throw new ServiceException("Redis 自增操作失败!");
            }
            return currentNumber - incrementBy + 1;
        }
    }

    /**
     * 初始化序列明细
     * @param sequence  序列对象
     * @param currentYear 当前年度
     * @param currentMonth 当前月份
     * @return 开始值 或 初始1
     */
    private Long initializeSequenceDetail(Sequence sequence, Integer currentYear, String currentMonth, long incrementBy) {
        SequenceDetail sequenceDetail = null;
        if (Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_MONTH_CONTINUOUS, sequence.getSequenceRule()) ) {
            sequenceDetail = sequenceMapper.selectSequenceDetailByIdAndCurrentYearAndCurrentMonth(sequence.getSequenceId(), currentYear, currentMonth);
        } else if (Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS, sequence.getSequenceRule()) ) {
            sequenceDetail = sequenceMapper.selectSequenceDetailByIdAndCurrentYearAndCurrentMonth(sequence.getSequenceId(), currentYear, GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
        }

        if (sequenceDetail == null)
        {
            sequenceDetail = new SequenceDetail();
            sequenceDetail.setSequenceId(sequence.getSequenceId());
            sequenceDetail.setPeriodYear(currentYear);
            sequenceDetail.setPeriodMonth( Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_MONTH_CONTINUOUS, sequence.getSequenceRule())  ? currentMonth : GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
            sequenceDetail.setCurrentNumber(incrementBy);

            int res = sequenceMapper.insertSequenceDetail(sequenceDetail);
            if (res <= 0)
            {
                log.error("初始化序列明细失败,sequenceId={},currentYear={},currentMonth={}", sequence.getSequenceId(), currentYear, currentMonth);
                throw new ServiceException("初始化序列明细失败!");
            }

            return 1L;
        }
        return sequenceDetail.getCurrentNumber() + 1L;
    }

    /**
     * 持久化序列明细到数据库
     */
    private void persistSequenceDetail(Sequence sequence, Integer currentYear, String currentMonth, Long currentNumber) {
        SequenceDetail sequenceDetail = new SequenceDetail();
        sequenceDetail.setSequenceId(sequence.getSequenceId());
        sequenceDetail.setPeriodYear(currentYear);
        sequenceDetail.setPeriodMonth(Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_MONTH_CONTINUOUS, sequence.getSequenceRule())  ? currentMonth : GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
        sequenceDetail.setCurrentNumber(currentNumber);

        int res = sequenceMapper.updateSequenceDetail(sequenceDetail);
        if (res <= 0) {
            log.error("更新序列明细失败,sequenceId={},currentYear={},currentMonth={},currentNumber={}", sequence.getSequenceId(), currentYear, currentMonth, currentNumber);
            throw new ServiceException("更新序列明细失败!");
        }
    }

    /**
     * 获取序号配置
     */
    private Sequence getSequenceConfig(Long tenantId, String orderType) {
        if (GenerateSequenceConstants.isInvoiceR1Type(orderType)){
            // 更正发票类型R1-R5 都使用 R1 的配置
            orderType = GenerateSequenceConstants.ORDER_SEQUENCE_TYPE_INVOICE_R1;
        }
        return sequenceCacheManager.getSequenceCache(tenantId, orderType);
    }

    /**
     * 生成一个序列号
     * @param tenantId
     * @param orderType
     * @return
     */
    public String generateSequenceUno(Long tenantId, String orderType){
        return generateSequenceBatch(tenantId, orderType, 1L).get(0);
    }

    /**
     * 生成一个发票序号
     * @param tenantId
     * @param orderType
     * @return
     */
    public InvoiceNo generateInvoiceSequenceUno(Long tenantId, String orderType){
        return batchGenerateInvoiceSequence(tenantId, orderType, 1L).get(0);
    }

    /**
     * 预获取下一个序列号
     * @param tenantId
     * @param orderType
     * @return
     */
    public String preGetNextSequenceUno(Long tenantId, String orderType){
        // 1 获取序号配置
        Sequence sequence = getSequenceConfig(tenantId, orderType);
        if (sequence == null) {
            throw new ServiceException("获取序号配置失败,请设置对应单据的序号配置!");
        }
        Integer sequenceRule = sequence.getSequenceRule();
        Integer currentYear = LocalDate.now().getYear();
        String currentMonth = String.format("%02d", LocalDate.now().getMonthValue());
        String dateType = sequence.getDateType();
        String sequencePrefix = StringUtils.defaultIfEmpty(sequence.getSequencePrefix(), "");
        String date = StringUtils.isNull(dateType) ? "" : DateUtils.dateTimeNow(dateType);

        Long nextSequenceNo = 1l;
        // 2 获取当前序号 redis 键
        String redisKey = GenerateSequenceConstants.SEQUENCE_KEY + tenantId + ":" + orderType + ":" + currentYear + ":" + currentMonth;
        if (Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS, sequenceRule) )
        {
            redisKey = GenerateSequenceConstants.SEQUENCE_KEY + tenantId + ":" + orderType + ":" + currentYear + ":" + GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH;
        }


        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(redisKey)))
        {
            String currentValue = stringRedisTemplate.opsForValue().get(redisKey);
            if (currentValue != null)
            {
                nextSequenceNo = Long.parseLong(currentValue) + 1;
            }
        }
        else
        {
            SequenceDetail sequenceDetail = null;
            if (Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_MONTH_CONTINUOUS, sequenceRule)) {
                sequenceDetail = sequenceMapper.selectSequenceDetailByIdAndCurrentYearAndCurrentMonth(sequence.getSequenceId(), currentYear, currentMonth);
            } else if (Objects.equals(GenerateSequenceConstants.ORDER_SEQUENCE_RULES_YEAR_CONTINUOUS, sequenceRule) ) {
                sequenceDetail = sequenceMapper.selectSequenceDetailByIdAndCurrentYearAndCurrentMonth(sequence.getSequenceId(), currentYear, GenerateSequenceConstants.SEQUENCE_ORDER_YEAR_MONTH);
            }
            if (sequenceDetail != null)
            {
                nextSequenceNo = sequenceDetail.getCurrentNumber() + 1;
            }
        }

        // 返回拼接后的序列号码
        return sequencePrefix + date + String.format("%06d", nextSequenceNo);

    }

    /**
     * 更新序号失败 - 清理redis 中缓存的序列号
     * @param tenantId 租户ID
     * @param orderType 单据类型
     */
    public void clearCache(long tenantId, String orderType) {
        log.info("清除单据类型:{}, 的缓存序号", orderType);
        String redisPrefix = GenerateSequenceConstants.SEQUENCE_KEY + tenantId + ":" + orderType + ":*";
        Set<String> keys = stringRedisTemplate.keys(redisPrefix);
        if (keys != null && !keys.isEmpty()) {
            stringRedisTemplate.delete(keys);
        }
    }

}

3 原理

js 复制代码
1 根据客户配置获取序号的 前缀 + 时间 + 数值;
2 优先redis中获取;
3 如果redis中没有,从数据库获取,然后同步redis;
4 redis更新,同时同步数据库,实现持久化!

4 业务中使用

4.1 插入数据成功后,更新序号

java 复制代码
// 3 插入数据
int rows = verifacInvoiceMapper.insertVerifacInvoice(verifacInvoice);
insertVerifacInvoiceDetail(verifacInvoice);
if (rows > 0){
    // 4 更新发票编号
    updateInvoiceNo(verifacInvoice);
}

4.2 更新失败,回滚序号、清除缓存

java 复制代码
// 更新发票号
private void updateInvoiceNo(VerifacInvoice invoice) {
    try {
        log.info( "订单号:{},生成发票,开始更新发票号...", invoice.getOrderInitNo());
        if (invoice.getTenantId() == null || invoice.getInvoiceTipo() == null){
            throw new ServiceException("获取发票序号,发票信息不完整!获取失败!");
        }
        if (invoice.getInvoiceNumero() == null || invoice.getInvoiceSerie() == null){
            InvoiceNo invoiceNo = generateSequenceService.generateInvoiceSequenceUno(invoice.getTenantId(), invoice.getInvoiceTipo());
            invoice.setInvoiceSerie(invoiceNo.getInvoiceSerie());
            invoice.setInvoiceNumero(invoiceNo.getInvoiceNumero());
            log.info( "订单号:{},生成发票,更新发票号成功:{}", invoice.getOrderInitNo(), invoice.getInvoiceSerie() + invoice.getInvoiceNumero());
        }
        // 检查发票号的唯一性
        checkInvoiceNoUnique(invoice);
        int rows = verifacInvoiceMapper.updateVerifacInvoiceNo(invoice);
        if (rows <= 0){
            throw new ServiceException("更新发票序号异常!");
        }
    } catch (Exception e){
        // 清除序号缓存
        generateSequenceService.clearCache(invoice.getTenantId(), invoice.getInvoiceTipo());
        throw new ServiceException("更新发票序号异常!异常原因:" + e.getMessage());
    }
}
相关推荐
大学生资源网15 分钟前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记24 分钟前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记42 分钟前
windows系统搭建kafka环境
后端
爬山算法1 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai1 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌1 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
9号达人2 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net