业务单据号每日重置后从1开始
概述
如果你从事过类似B端进销存系统相关的开发工作,一定会遇到一个需求:
业务单据号每天重置,后几位每天从1开始递增。
比如说门店每日的订货单:
- DH
20240701
0001 - DH
20240701
0002 - DH
20240701
0003 - DH
20240701
0004
头两个字母DH表示订货;
中间6位是年份和日期
后四位是自增的
到了第二天的时候,订货单据号会重置重新从1开始:
- DH
20240702
0001 - DH
20240702
0002 - DH
20240702
0003 - DH
20240702
0004
有线下门店的各个餐饮、茶饮店门店端系统的单据,基本都是使用如上的规则。
解决方案讨论
处理这个需求之前,需要先了解这个需求背后隐含的2个问题:
- 单据号当天不能重复,就算在【分布式环境】且有【并发】的情况下也不能重复;
- 单据号每天都需要重置。
如果你到网络上查询解决方案,可能搜索到的大概方案如下:
- 使用Redis或其他缓存系统中的原子操作;
- 使用分布式ID生成器,如Snowflake算法;
- 使用存储过程、触发器等;
- 等等。。。。。。。
这些我都觉得要么太复杂要么太贵了,就比如说使用Redis和MQ的,完全没有必要因为这个小需求,把重量级的Redis和MQ引入进来,且买Redis和MQ示例也是需要钱的。
需要结合当时的业务实际情况和技术团队技术栈情况,使用合适的技术。
解决方案:使用JAVA+mysql+定时任务
由于公司的门店不多,几百家,流量不大,就算有并发,瞬间并发数也是非常低的。是可以直接使用mysql来实现的。
主要的设计思路如下:
- 用一张mysql表,建立自增id,确保在【当天】不重复;
- 使用定时任务,每天凌晨的时候清理掉(TRUNCATE)数据,确保第二天id又从1开始。
这套方案虽然简单粗暴,但它已稳定运行快一年了,暂时未出现过任何问题。算是一套价格便宜又实惠稳定的技术方案了。
具体的实操代码
目前团队用的技术是基于SpringCloud Alibaba +DDD的,会使用到阿里相关的技术组件和涉及到DDD相关的内容。
建立docs_day_id_generator表以及PO对象
CREATE TABLE `docs_day_id_generator` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id',`created_date` date NOT NULL COMMENT '创建日期',`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),KEY `idx_create_date` (`created_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单据id生成'
对应的po对象:
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;/*** 单据id生成器PO*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("docs_day_id_generator")
public class DocsDayIdGeneratorPO {/*** 自增id*/private Long id;/*** 创建日期*/private Date createdDate;
}
DDD仓储层和领域层实现
剩下的只要在DDD的仓储层和domain层定义相关的接口和实现即可。
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;/*** 单据id生成器-仓储接口层实现类*/
@RequiredArgsConstructor
@Repository
public class DocsDayIdGeneratorRepositoryImpl implements DocsDayIdGeneratorRepository {private final DocsDayIdGeneratorMapper docsDayIdGeneratorMapper;@Overridepublic Long createDocsSequence(Date currentDate) {DocsDayIdGeneratorPO poInsert = DocsDayIdGeneratorPO.builder().createdDate(currentDate).build();docsDayIdGeneratorMapper.insert(poInsert);return poInsert.getId();}
}
仓储层的实现比较简单,只需要构建一个DocsDayIdGeneratorPO记录,插入到表里即可,生成的自增id会存储在po对象的id字段里,直接返回即可。
领域服务层则需要接收一个模块编码,并生成对应模块的单据号,比如订货单据号,盘点单据号,退货单据号等。
import lombok.AllArgsConstructor;
import lombok.Getter;/*** 模块枚举*/
@Getter
@AllArgsConstructor
public enum ModuleEnum {ORDER("order", "订货","DH"),CHECK("check", "盘点","PD"),REFUND("refund", "退货","TH");/*** 编码*/private final String code;/*** 描述*/private final String desc;/*** 单据前缀*/private final String prefix;
}
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.Date;/*** id生成器-领域层接口实现类*/
@RequiredArgsConstructor
@Slf4j
@Service
public class DocsDayIdGeneratorDomainServiceImpl implements DocsDayIdGeneratorDomainService {public final DocsDayIdGeneratorRepository docsDayIdGeneratorRepository;@Overridepublic String createDocsCode(ModuleEnum moduleEnum) {Long code = docsDayIdGeneratorRepository.createDocsSequence(new Date());/*单据号规则:前两位是模块编码,接下来六位是日期,后四位是自增id,不足4位,则在前面补零*/return moduleEnum.getPrefix() + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%04d", code);}
}
定时任务每天凌晨重置
市面上定时任务的产品非常多,出于统一性,我这边的团队用的是阿里的Schedulex2.0。只要阿里有相关的产品,都是优先选择阿里体系的。
定时任务处理器
import com.alibaba.schedulerx.worker.domain.JobContext;
import com.alibaba.schedulerx.worker.processor.JavaProcessor;
import com.alibaba.schedulerx.worker.processor.ProcessResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;/*** 重置单据号自增id,每天从1开始*/@RequiredArgsConstructor
@Component
@Slf4j
public class DocsIdResetJobProcessor extends JavaProcessor {private final DocsDayIdGeneratorDomainService docsDayIdGeneratorDomainService;@Overridepublic ProcessResult process(JobContext context) {try {docsDayIdGeneratorDomainService.resetdDocsSequence();return new ProcessResult(true);}catch (Exception e){return new ProcessResult(false,e.getMessage());}}
}
resetdDocsSequence方法的实现超级简单,就是一个truncate一下table即可。
<update id="resetdDocsSequence">truncate table docs_day_id_generator;</update>
定时任务的cron表达式如下:
0 0 0 * * ?
至于阿里Schedulex2.0界面上相关的配置,这个在网上很容易找到,这里就不赘述了。
总结
选择符合当前自己团队实际情况的技术即可,不一定要高大上的。