今天我们来讲一下编码规则的功能设计与开发。 在2B的业务系统中,常常会有各种单据、物料等信息都需要使用到编码,如果用硬编码把对应的业务编码写死,修改起来的时候就非常不方便,所以需要一个编码规则的功能,动态去设置各个业务单据生成的编码。
下面我们分开两部分来进行讲解,首先我们先讲解功能,然后再讲解代码层面上是如何实现的。
1. 功能讲解
1.1 编码规则
菜单栏中选择 系统工具 -> 编码规则,获取使用快捷键 ctrl+k快速搜索菜单。
点击【新增】按钮添加一个新的编码规则,填写必要信息
说明
- 最大长度:生成编码的长度
- 是否补齐:如果设置的编码组成长度不够,会自动根据最大长度把编码补全
- 补齐字符:编码长度不够,使用的补齐字符
- 补齐方式:左补齐和右补齐
1.2 编码组成
编辑规则组成:在编码规则表格中,点击规则编码打开规则组成编辑窗口
- 点击【新增】按钮,会自动在表格新增一行,该表格是可编辑表格(之前是使用弹窗的方式一条条新增的,考虑的用户的方便性,后来改成可编辑表格)。
- 删除,直接勾选需要删除的分段信息,点击【删除】可以批量删除。
- 考虑到分段的信息需要调节顺序,表格的行是可以拖动调整顺序的。
分段类型说明:
- 输入字符:生成编码规则的时候需要使用到用户输入的信息,这里定义一个变量名称,在”输入字符“列中填写(后面会根据实际场景优化成下来选择),生成编码的时候会自动获取该定义的变量名称对应的值填充到编码规则中。
- 当前日期时间:获取当前系统时间,根据”日期格式“列选择的格式,把日期时间填充到编码规则中。日期格式分别有:yyyyMMdd、yyyy-MM-dd、yyyy/MM/dd 格式可以在【字典功能】定义。
- 固定字符:固定的自定义字符,例如采购单的固定字符为 ”PO“,生成编码的时候就会自动把PO填充到编码中。
- 流水号:填写列”分段长度“、”流水号初始值“、”流水号步长“、”循环方式“
- 分段长度:流水号的长度
- 流水号初始值:流水号从那个数字开始计算
- 流水号步长:流水号叠加的值
- 循环方式:按天、月、年重新开始计算流水号
以上就是配置编码规则的功能介绍,下面我们来讲解代码是如何实现的。
2. 代码实现
2.1 编码段生成
这里使用了 策略模式 根据不同的编码类型执行不同的生成规则 我们先来看看UML图
PartTypeTemplate接口定义了生成编码的接口方法,PartTypeInputCharHandler、PartTypeNowDateHandler、PartTypeFixCharHandler、PartTypeSerialNoHandler 分别是不同类型生成方式的实现。
PartTypeHandler定义了方法choiceExecute(AutoCodePart sysAutoCodePart)传入的是组成部分的信息,然后根据类型编码执行对应的编码生成逻辑。
下面我们详细看看代码实现:
PartTypeTemplate
/**
* 规则组成类型生成接口
* @author fwj
* @since 2025/5/8 */public interface PartTypeTemplate {
/**
* 分段的处理规则
* @param sysAutoCodePart
* @return
*/ String partHandle(AutoCodePart sysAutoCodePart);
}
PartTypeInputCharHandler
/**
* 输入字符生成规则实现类
* @author fwj
* @since 2025/5/8 */@Component
@Order(0)
public class PartTypeInputCharHandler implements PartTypeTemplate {
@Override
public String partHandle(AutoCodePart autoCodePart) {
String inputCharacter = autoCodePart.getInputCharacter();
Assert.notNull(inputCharacter,"编码规则传入字符不能为空!");
Assert.isTrue(inputCharacter.length() == autoCodePart.getPartLength(),"传入字符的长度错误!");
return inputCharacter;
}
}
PartTypeNowDateHandler
/**
* 编码规则组成日期生成实现类
* @author fwj
* @since 2025/5/8 */@Component
@Order(1)
public class PartTypeNowDateHandler implements PartTypeTemplate{
@Override
public String partHandle(AutoCodePart autoCodePart) {
String formatDate = autoCodePart.getDateFormat();
return DateTimeFormatter.ofPattern(formatDate).format(LocalDateTime.now());
}
}
PartTypeFixCharHandler
/**
* 编码规则组成固定字符生成实现类
* @author fwj
* @since 2025/5/8 */@Component
@Order(2)
public class PartTypeFixCharHandler implements PartTypeTemplate {
@Override
public String partHandle(AutoCodePart autoCodePart) {
return autoCodePart.getFixCharacter();
}
}
PartTypeSerialNoHandler
/**
* 编码规则组成序列号生成实现类
* @author fwj
* @since 2025/5/8 */@Component
@Order(3)
public class PartTypeSerialNoHandler implements PartTypeTemplate{
@Autowired
private AutoCodeResultService autoCodeResultService;
@Override
public String partHandle(AutoCodePart autoCodePart) {
String method = autoCodePart.getCycleMethod();
String param ="";
if(CycleMethodEnum.CYCLE_METHOD_OTHER.getCode().equals(method)){
param = autoCodePart.getInputCharacter();
}else{
switch (CycleMethodEnum.getByCode(method)){
case CYCLE_METHOD_YEAR:
param = DateUtils.format(LocalDateTime.now(),"yyyy");
break;
case CYCLE_METHOD_MONTH:
param = DateUtils.format(LocalDateTime.now(),"yyyyMM");
break;
case CYCLE_METHOD_DAY:
param = DateUtils.format(LocalDateTime.now(),"yyyyMMdd");
break;
case CYCLE_METHOD_HOUR:
param = DateUtils.format(LocalDateTime.now(),"yyyyMMddHH");
break;
case CYCLE_METHOD_MINUTE:
param = DateUtils.format(LocalDateTime.now(),"yyyyMMddHHmm");
break;
default:
break;
}
}
List<AutoCodeResult> rs = getAutoCodeResult(autoCodePart.getRuleId(),param,method);
if(!rs.isEmpty()){
//如果在编码记录表中有记录,则在最后一个流水号上加上步长,返回新的流水号
AutoCodeUtil.threadLocal.set(false);
Long lastSerialNo = rs.get(0).getLastSerialNo();
return String.format("%0"+autoCodePart.getPartLength()+"d",lastSerialNo+autoCodePart.getSerialStep());
}else {
//如果在编码记录表中不存在,则直接返回起始流水号
AutoCodeUtil.threadLocal.set(true);
return String.format("%0"+autoCodePart.getPartLength()+"d",autoCodePart.getSerialStartNo());
}
}
//从编码结果记录表中查找当前指定循环规则的流水号记录
private List<AutoCodeResult> getAutoCodeResult(Long ruleId, String param, String cycleMethod){
LambdaQueryWrapper<AutoCodeResult> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AutoCodeResult::getRuleId, ruleId);
//这里的param将按照 gen_date like #{param}+'%' 的方式进行模糊查询,数据库中记录的永远都是yyyMMddHHmmss格式的
queryWrapper.likeRight(AutoCodeResult::getGenDate, param);
return autoCodeResultService.list(queryWrapper);
}
}
备注: 序列号生成方式需要在数据库查询上一个生成的序列号,这里分两种情况:一种是单个编码生成、一种是批量编码生成。这里要注意的是并发的问题,可能会导致序列号重复,防止并发的问题可以使用redis锁或其他锁来解决。
PartTypeHandler
@Component
public class PartTypeHandler {
@Autowired
List<PartTypeTemplate> partTypeTemplates;
public String choiceExecute(AutoCodePart sysAutoCodePart){
String partType = sysAutoCodePart.getPartType();
return partTypeTemplates.get(PartTypeEnum.getByCode(partType).getBeanIndex()).partHandle(sysAutoCodePart);
}
}
2.2 编码生成工具类
这里只简单介绍主要的方法,详细代码可以查看 项目源码
AutoCodeUtil
...
synchronized
public String genSerialCode(String ruleCode, String inputCharacter){
//查找编码规则
AutoCodeRule rule = autoCodeRuleService.getByCode(ruleCode);
Assert.notNull(rule, String.format("未获取到指定类型:[%s]的业务编码生成规则",ruleCode));
//查找规则组成
List<AutoCodePart> parts = autoCodePartService.getByRuleId(rule.getId());
List<AutoCodePart> collect = parts.stream().filter(part-> PartTypeEnum.PART_TYPE_SERIALNO.getCode().equals(part.getPartType())).collect(Collectors.toList());
Assert.isTrue(collect.size()<2, String.format("编码规则[%s]流水号方式的组成只能存在一个",ruleCode));
StringBuilder buff = new StringBuilder();
parts.forEach(codePart ->{
codePart.setInputCharacter(inputCharacter);
//根据当前组成部分,获取当前组成部分的结果
String partStr = partTypeHandler.choiceExecute(codePart);
//如果是流水号部分,则进行记录
if(StringUtils.equals(codePart.getPartType(),PartTypeEnum.PART_TYPE_SERIALNO.getCode())){
lastSerialNo = partStr;
}
//将获取到的部分组装进整体编码中
buff.append(partStr);
});
Assert.hasText(buff.toString(),String.format("规则:[%s]生成的编码为空!",ruleCode));
String autoCode = paddingStr(rule,buff);
//将生成结果保存到数据库
saveAutoCodeResult(rule,autoCode,inputCharacter);
return autoCode;
}
...
说明:
- 使用synchronized防止并发
- 后期优化使用 redis锁的方式防止并发导致编码重复的问题
- 如果是标签条码这种批量编码生成,需要使用批量生成的方法(之前项目中批量生成使用单个生成的方法性能会很差,单个生成还有数据库频繁读写的性能消耗。)
2.3 业务代码使用
//注入AutoCodeUtil类
@Autowired
private AutoCodeUtil autoCodeUtil;
...
//生成编码, PO_CODE为编码规则的编码
String poCode = autoCodeUtil.genSerialCode("PO_CODE", null)
...
本文源码已上传Gitee 开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!
关注公众号「慧工云创」