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());
    }
}
相关推荐
IManiy5 分钟前
总结之Vibe Coding:后端骨架
后端
ikoala7 分钟前
Codex 怎么买、怎么充值?先把这两套计费搞清楚
前端·javascript·后端
前端Hardy38 分钟前
一个时代结束了:npm 终于对 install 脚本下手了
前端·javascript·后端
damaoyou39 分钟前
Cog3DRangeImagePlaneEstimatorTool完全指南
后端
Nturmoils1 小时前
分页别写太顺手,LIMIT 背后还有排序和边界
数据库·后端
神奇小汤圆1 小时前
国产版“Codex”初体验,智谱ZCode很强啊!
后端
站大爷IP1 小时前
Python里的“赋值”到底是什么意思?
后端
鹅城剑仙2 小时前
Spring Boot 微服务架构设计与最佳实践
spring boot·后端·微服务
Full Stack Developme3 小时前
Spring Integration 教程
java·后端·spring
爱勇宝3 小时前
AI 时代,前端工程师的话语权正在下降?
前端·后端