应用开发平台业务支撑功能——单据编号、流水号功能方案、设计与实现

背景

业务系统中通常存在生成具有业务含义的流水号功能,该流水号要求唯一,通常由以下三部分组成:具备业务含义的字符串(可选)+年月日(年或年月或年月日时等,可选)+若干位流水号(从1开始),如HT201910080003,表示的是2019年10月8日拟定的第3份合同。
流水号的设置由IT管理员进行维护和管理。

方案选择

流水号跟常规实体还是有一定差别,一定程度上从属于实体,怎么来管理,主要有两种方案,具体如下:
方案1:独立管理,配置流水号规则,然后实体模型配置时选择流水号实体记录进行关联
方案2:实体配置时进行配置,从属于实体模型
这两种方案各有优缺点。方案1需要是在实体配置前,先创建该实体对应的流水号规则,先后次序上略别扭。方案2是按照常规次序进行,但是完全变成了实体模型的从属,不再有独立的查看页面。

从业务功能而言,流水号的配置需要设置前缀、时间段和流水号,在实体模型配置过程中进行流水号的配置,有一点重,单独配置,作为前置工作更合理一些,实体模型配置时直接使用即可。
此外,流水号的记录不仅仅记录了生成规则,同时还记录了当前流水号,并且会按照重置规则去重置,当系统异常排查问题时,通过独立的查看页面去查看,如果是从属,打开实体配置去看流水号当前规则及值,反而操作上显得诡异。
特别重要的一点是,流水号规则会随着业务变化调整,变动频率要远高于实体自身配置,单独维护更好一些。
还有一种业务场景,就是多个单据公用一个流水号,这时候,如果使用方案2从属业务实体模式,需求上无法满足,功能上无法实现。

综上,采用方案1,还是独立管理,在实体配置前预配置而已,不算问题。这种情况下,流水号就变成了普通的实体了,有独立的菜单,进行列表展示,增删改查。

功能设计与实现

基础功能

新增:创建单据流水号数据记录,配置流水号生成规则及重置策略
编辑:对单据流水号进行调整,只对将来需要生成的流水号起作用,不追溯调整已经生成的历史单据流水号
删除:逻辑删除
查询:根据单据类型和单据名称查询流水号生成规则

基于实体配置模块
image.png
运行效果
image.png

流水号生成规则

最多三段,分别为前缀(可选)+日期时间格式字符串(,可选)+若干位流水号,可选择连接字符串,默认为空字符串
前缀:具备业务含义的字符串,通常为业务单据的英文单词或汉语拼音首字母缩写
日期时间格式字符串:用户可以灵活输入符合yyyyMMddHHmmss格式的字符串或符合部分起始相同的字符串(如仅输入yyyyMMdd),由程序根据当前时间进行转换
流水号:从1开始编号,位数不足前面补零

image.png

扩展功能

获取单个流水号:后端服务,传入单据类型,获取单个流水号
批量获取流水号:后端服务,传入单据类型和数量,获取指定数量的流水号
重置流水号:根据重置策略,自动将流水号号重置为1

重置策略

按年、按月、按日,使用任务调度功能实现
注:不考虑按小时等更细的时间维度,业务上通常到不了这么细

package tech.abc.platform.support.scheduler;


import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import tech.abc.platform.common.annotation.SystemLog;
import tech.abc.platform.common.constant.CommonConstant;
import tech.abc.platform.common.enums.LogTypeEnum;
import tech.abc.platform.support.service.SerialNoService;


/**
 * 重置流水号
 *
 * @author wqliu
 * @date 2023-05-30
 */
@Component
@Slf4j
@DisallowConcurrentExecution
public class ResetSerialNo extends QuartzJobBean {
    
    

    @Autowired
    private SerialNoService serialNoService;


    @Override
    @SystemLog(value = "重置流水号", logType = LogTypeEnum.SCHEDULER, logRequestParam = false, executeResult =
            CommonConstant.NO)
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    
    

        try {
    
    
            serialNoService.resetSerialNo();
        } catch (Exception e) {
    
    
            log.error("重置流水号失败", e);
            throw new JobExecutionException(e);
        }

    }


}

image.png

如何保证流水号唯一性?

作为业务单据的唯一性编号,流水号需要保证不重复,否则会严重影响业务和系统正常运行。这里技术方案上并没有使用传统加锁方式,主要是基于性能方面的考虑。使用乐观锁+自动重试机制,来进行流水号的生成。乐观锁是平台通过深度集成MybatisPlus的实现的,不需要额外处理。自动重试使用了Spring的Retry功能组件。
添加组件依赖

<!--重试组件-->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

在需要重试的类上加注解@Retryable

/**
 * 批量获取流水号
 * @param code  编码
 * @param count 数量
 * @return {@link List}<{@link String}>
 */
@Override
@Retryable(value = {
    
    Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 100L, multiplier = 2))
public List<String> generateBatch(String code, int count) {
    
           
    // 先根据编码获取当前流水号
    SerialNo entity = this.lambdaQuery().eq(SerialNo::getCode, code).one();
    if (entity == null) {
    
    
        throw new CustomException(SerialNoExceptionEnum.CODE_NOT_EXIST);
    }
    int currentSerialNo = entity.getCurrentValue();
    // 计算更新后的流水号
    int updateSerialNo = currentSerialNo + count;
    // 更新数据
    entity.setCurrentValue(updateSerialNo);
    boolean updateFlag = modify(entity);
    if (!updateFlag) {
    
    
        throw new CustomException(CommonException.UPDATE_ERROR);
    }
    // 组织返回
    String template = generateNoTemplate(entity);
    List<String> result = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
    
    
        result.add(String.format(template, StringUtils.leftPad(String.valueOf(currentSerialNo), entity.getLength(),
                '0')));
        currentSerialNo++;
    }
    return result;
}

注解参数及含义如下:

  • value:抛出指定异常才会重试
  • include:和value一样,默认为空,当exclude也为空时,默认所有异常
  • exclude:指定不处理的异常
  • maxAttempts:最大重试次数,默认3次
  • backoff:重试等待策略,
  • 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为100; 以毫秒为单位的延迟(默认 1000)
  • multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。

在同一个类中,需要增加一个@Recover注解的方法,用于达到最大重试次数后处理。


    /**
     * 达到最大重试次数
     *
     * @param e 异常
     */
    @Recover
    public List<String> recoverBatch(Exception e) {
    
    
        log.error("生成单据流水号达到最大重试次数", e);
        throw new CustomException(CommonException.TRY_MAX_COUNT);
    }

最后,记得需要在SpringBoot启动类上加上注解@EnableRetry。

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。

猜你喜欢

转载自blog.csdn.net/seawaving/article/details/131186960