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());
    }
}
相关推荐
Java水解几秒前
Spring Boot 事务详解
spring boot·后端
gauch几秒前
vscode 调试 Go 的配置解释(photoprism / Docker / 远程调试 / Delve)
后端·ai编程·visual studio code
轻松Ai享生活36 分钟前
详解Linux LVM (Logical Volume Manager)
linux·后端
华仔啊44 分钟前
别再问了!Java里这几种场景,用抽象类就对了
java·后端
guojl1 小时前
Gateway源码分析
后端·微服务
tingting01191 小时前
Spring Boot 外部配置指定不生效的原因与解决
java·spring boot·后端
2501_909686701 小时前
基于SpringBoot的网上点餐系统
java·spring boot·后端
天天摸鱼的java工程师1 小时前
聊聊线程池中哪几种状态,分别表示什么?8 年 Java 开发:从业务踩坑到源码拆解(附监控实战)
java·后端
杨杨杨大侠1 小时前
第4篇:AOP切面编程 - 无侵入式日志拦截
java·后端·开源