【Java】单号创建服务
需求:ERP项目存在若干个业务功能,每个业务的单子的单号创建规则需要被统一规划
1、每个业务有自己对应的标识
2、业务单号必须以英文字母为前缀,长度在2 - 4个字符
3、单号的组成 = [ 前缀 ] + [ 日期单位(8) ] + [ 当前序列 ]
4、日期单位可以灵活设置,按年,月,日为单位,即中间的8位精确到什么日期单位
5、结尾对【当前序列】自增后进行补位填充,拼接后就是新的单号
设计的表结构:
1、业务标识 + 前缀 + 日期单位(年,月,日) 组合为唯一键
2、表的维护全靠建单逻辑执行(只有新增和更新操作)
CREATE TABLE `sys_co_servcode` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '系统编码 主键,初始值为“100001”', `sc_serv_ident` varchar(32) NOT NULL COMMENT '业务标识 参见业务标识对照表<类:ServiceIdentEnum>', `sc_prifix` varchar(4) NOT NULL COMMENT '编码前缀 默认用两位,最长四位;', `sc_year` char(4) NOT NULL COMMENT '所属年', `sc_month` char(2) NOT NULL COMMENT '所属月', `sc_day` char(2) NOT NULL COMMENT '所属日', `sc_num` int(11) NOT NULL COMMENT '当前编号', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100012 DEFAULT CHARSET=utf8 COMMENT='业务编码表';
设计的接口服务:
1、接口设定了单号长度设置,需要校验单号长度是否不足
2、日期根据字段信息,需要自己计算年月日,所以这里弄成枚举类型作为入参
package cn.hyite.amerp.system.common.servcode.service; import cn.hyite.amerp.system.common.servcode.dto.DateUnit; /** * sys_co_servcode 业务编码表 服务类 * * @author OnCloud9 * @version 1.0 * @project server * @date 2022-12-15 */ public interface ISysCoServcodeService { /** * 根据 [业务标识] + [前缀定义] + [日期精度] + [单号总长度] 创建最新的单号 * @param servIdent 业务标识 * @param codePrefix 编号前缀定义(2 ~ 4)字符长度 * @param dateUnit 日期单位 [year, month, day] * @param length 单号总长度 * @return String 最新业务编号 * @author OnCloud9 * @date 2022/12/15 14:08 * */ String createServiceCode(String servIdent, String codePrefix, DateUnit dateUnit, Integer length); }
考虑到单号可变长度,每个日期枚举对应设置了长度限制,方便接口实现做长度计算校验
package cn.hyite.amerp.system.common.servcode.dto; import lombok.Getter; /** * 日期单位枚举类 * * @author OnCloud9 * @version 1.0 * @project server * @date 2022年12月15日 14:36 */ @Getter public enum DateUnit { YEAR(4), MONTH(6), DAY(8) ; public static final int TOTAL_LENGTH = 8; private final int length; DateUnit(int length) { this.length = length; } }
接口实现:
1、一个新的业务建单规则没有使用时,调用后记录规则,返回单号
2、再次调用时,根据记录获取最新单号,更新记录的单号后,返回单号
package cn.hyite.amerp.system.common.servcode.service.impl; import cn.hyite.amerp.common.ResultMessage; import cn.hyite.amerp.common.service.BaseService; import cn.hyite.amerp.common.util.Assert; import cn.hyite.amerp.system.common.servcode.dao.SysCoServcodeDAO; import cn.hyite.amerp.system.common.servcode.dto.DateUnit; import cn.hyite.amerp.system.common.servcode.dto.SysCoServcodeDTO; import cn.hyite.amerp.system.common.servcode.service.ISysCoServcodeService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.text.NumberFormat; import java.time.LocalDateTime; import java.util.Date; import java.util.Objects; /** * sys_co_servcode 业务编码表 服务实现类 * * @project server * @author OnCloud9 * @date 2022-12-15 * @version 1.0 */ @Service("sysCoServcodeService") public class SysCoServcodeServiceImpl extends BaseService<SysCoServcodeDAO, SysCoServcodeDTO> implements ISysCoServcodeService { private static final Integer PREFIX_LIMIT = 4; private static final String SPACER = "00"; private static final String FIELD_YEAR = "sc_year"; private static final String FIELD_MONTH = "sc_month"; private static final String FIELD_DAY = "sc_day"; @Override public String createServiceCode(String servIdent, String codePrefix, DateUnit dateUnit, Integer length) { /* Step1 校验入参合法性 */ Assert.isTrue(StringUtils.isBlank(servIdent), ResultMessage.NULL_ERROR, "业务标识"); Assert.isTrue(StringUtils.isBlank(codePrefix), ResultMessage.NULL_ERROR, "业务前缀"); final int prefixLen = codePrefix.length(); Assert.isTrue(prefixLen > PREFIX_LIMIT, ResultMessage.CUSTOM_ERROR, "业务前缀长度超出限制!" + prefixLen); final int dateUnitLength = DateUnit.TOTAL_LENGTH; Assert.isTrue(prefixLen + dateUnitLength + 1 > length, ResultMessage.CUSTOM_ERROR, "单号总长溢出!"); /* Step2 组建查询条件, 封装日期单位条件 */ QueryWrapper<SysCoServcodeDTO> condition = Wrappers.query(); condition.eq("sc_serv_ident", servIdent); condition.eq("sc_prifix", codePrefix); LocalDateTime nowTime = LocalDateTime.now(); String year = String.valueOf(nowTime.getYear()); String month = String.valueOf(nowTime.getMonth().getValue()); String day = String.valueOf(nowTime.getDayOfMonth()); switch (dateUnit) { case YEAR: condition.eq(FIELD_YEAR, year); condition.eq(FIELD_MONTH, SPACER); condition.eq(FIELD_DAY, SPACER); break; case MONTH: condition.eq(FIELD_YEAR, year); condition.eq(FIELD_MONTH, month); condition.eq(FIELD_DAY, SPACER); break; case DAY: condition.eq(FIELD_YEAR, year); condition.eq(FIELD_MONTH, month); condition.eq(FIELD_DAY, day); break; } /* Step3 查询是否存在此规则的建单记录 */ SysCoServcodeDTO serviceCode = baseMapper.selectOne(condition); boolean isEmpty = Objects.isNull(serviceCode); if (isEmpty) { /* 二次校验总长度 */ int totalLength = prefixLen + dateUnitLength + 1; Assert.isTrue(totalLength > length, ResultMessage.CUSTOM_ERROR, "单号总长溢出!(新建时)"); /* 新建记录,新记录时,设置新的日期单位 */ int newNum = 1; String dateCode = ""; SysCoServcodeDTO newCode = new SysCoServcodeDTO(); newCode.setScServIdent(servIdent); newCode.setScPrifix(codePrefix); newCode.setCreateTime(new Date()); newCode.setScNum(newNum); newCode.setScYear(year); switch (dateUnit) { case YEAR: newCode.setScMonth(SPACER); newCode.setScDay(SPACER); dateCode += (year + SPACER + SPACER); break; case MONTH: newCode.setScMonth(month); newCode.setScDay(SPACER); dateCode += (year + month + SPACER); break; case DAY: newCode.setScMonth(month); newCode.setScDay(day); dateCode += (year + month + day); break; } baseMapper.insert(newCode); int remainLen = length - (prefixLen + dateUnitLength); /* 返回单号 */ return codePrefix + dateCode + formatNumDigit(remainLen, newNum); } else { /* 取当前单号 */ Integer currentNum = serviceCode.getScNum(); ++ currentNum; /* 更新最新单号 */ serviceCode.setScNum(currentNum); baseMapper.updateById(serviceCode); String numStr = String.valueOf(currentNum); final int numLength = numStr.length(); /* 二次校验总长度 */ int totalLength = prefixLen + dateUnitLength + numLength; Assert.isTrue(totalLength > length, ResultMessage.CUSTOM_ERROR, "单号总长溢出!(更新时)"); int remainLen = length - (prefixLen + dateUnitLength); /* 返回单号 */ return codePrefix + serviceCode.getScYear() + serviceCode.getScMonth() + serviceCode.getScDay() + formatNumDigit(remainLen, currentNum); } } /** * 计数值补位 * @param remainLen 剩余长度 * @param serial 序列计数值 * @return java.lang.String 补位后的序列值 * @author OnCloud9 * @date 2022/12/15 16:49 * */ private String formatNumDigit(int remainLen, int serial) { NumberFormat formatter = NumberFormat.getNumberInstance(); formatter.setMinimumIntegerDigits(remainLen); formatter.setGroupingUsed(false); return formatter.format(serial); } }
2023年01月10日 更新
业务调整,追加了如下几个需求:
- 年月日 不再占位, 年4位,月6位(年 + 月),日8位 (年 + 月 + 日) - 支持空项,不填写年月日, - 年可以支持2位 - 业务标识前缀支持1 - 4位
原来的日期单位枚举类需要扩展枚举项:
package cn.hyite.amerp.system.common.servcode.dto; import lombok.Getter; /** * 日期单位枚举类 * * @author OnCloud9 * @version 1.0 * @project amerp-server * @date 2022年12月15日 14:36 */ @Getter public enum DateUnit { EMPTY(0), /* 空日期,不占用 */ YEAR_DIGIT2(2), /* 2位年份 */ MONTH_DIGIT2(4), /* 2位年份 + 2位月份 */ DAY_DIGIT2(6), /* 2位年份 + 2位月份 + 2日期 */ YEAR(4), /* 4位完整年份 */ MONTH(6), /* 4位完整年份 + 2位月份 */ DAY(8), /* 4位完整年份 + 2位月份 + 2日期 */ ; public static final int TOTAL_LENGTH = 8; private final int length; DateUnit(int length) { this.length = length; } }
同样,需要对应这些新的枚举项追加扩展支持:
package cn.hyite.amerp.system.common.servcode.service.impl; import cn.hyite.amerp.common.ResultMessage; import cn.hyite.amerp.common.service.BaseService; import cn.hyite.amerp.common.util.Assert; import cn.hyite.amerp.system.common.servcode.dao.SysCoServcodeDAO; import cn.hyite.amerp.system.common.servcode.dto.DateUnit; import cn.hyite.amerp.system.common.servcode.dto.SysCoServcodeDTO; import cn.hyite.amerp.system.common.servcode.service.ISysCoServcodeService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.text.NumberFormat; import java.time.LocalDateTime; import java.util.Date; import java.util.Objects; /** * sys_co_servcode 业务编码表 服务实现类 * * @project amerp-server * @author OnCloud9 * @date 2022-12-15 * @version 1.0 */ @Service("sysCoServcodeService") public class SysCoServcodeServiceImpl extends BaseService<SysCoServcodeDAO, SysCoServcodeDTO> implements ISysCoServcodeService { private static final Integer PREFIX_LIMIT = 4; private static final String SPACER = "00"; private static final String FIELD_YEAR = "sc_year"; private static final String FIELD_MONTH = "sc_month"; private static final String FIELD_DAY = "sc_day"; @Override public String createServiceCode(String servIdent, String codePrefix, DateUnit dateUnit, Integer length, int initialNum) { /* Step1 校验入参合法性 */ Assert.isTrue(StringUtils.isBlank(servIdent), ResultMessage.NULL_ERROR, "业务标识"); Assert.isTrue(StringUtils.isBlank(codePrefix), ResultMessage.NULL_ERROR, "业务前缀"); final int prefixLen = codePrefix.length(); Assert.isTrue(prefixLen > PREFIX_LIMIT, ResultMessage.CUSTOM_ERROR, "业务前缀长度超出限制!限制:" + prefixLen); final int requireLength = prefixLen + dateUnit.getLength() + 2; Assert.isTrue(requireLength > length, ResultMessage.CUSTOM_ERROR, "单号总长不足! 需要:" + requireLength + ", 提供:" + length); /* Step2 组建查询条件, 封装日期单位条件 */ QueryWrapper<SysCoServcodeDTO> condition = buildQueryWrapper(servIdent, codePrefix, dateUnit); /* Step3 查询是否存在此规则的建单记录 */ SysCoServcodeDTO serviceCode = baseMapper.selectOne(condition); boolean isEmpty = Objects.isNull(serviceCode); if (isEmpty) { /* 新建记录,新记录时,设置新的日期单位 */ SysCoServcodeDTO dto = buildNewServCodeDTO(servIdent, codePrefix, dateUnit, initialNum); baseMapper.insert(dto); /* 返回单号 */ int remainLen = length - (prefixLen + dateUnit.getLength()); return codePrefix + dto.getDateCode() + formatNumDigit(remainLen, dto.getScNum()); } else { return doServCodeUpdate(serviceCode, dateUnit, length); } } /** * 计数值补位 * @param remainLen 剩余长度 * @param serial 序列计数值 * @return java.lang.String 补位后的序列值 * @author OnCloud9 * @date 2022/12/15 16:49 * */ private String formatNumDigit(int remainLen, int serial) { NumberFormat formatter = NumberFormat.getNumberInstance(); formatter.setMinimumIntegerDigits(remainLen); formatter.setGroupingUsed(false); return formatter.format(serial); } /** * 构建查询条件 * @param servIdent 业务标识 * @param codePrefix 单号前缀 * @param dateUnit 日期单位枚举 * @return QueryWrapper<SysCoServcodeDTO> 查询条件 * @author OnCloud9 * @date 2023/1/9 16:01 * */ private QueryWrapper<SysCoServcodeDTO> buildQueryWrapper(String servIdent, String codePrefix, DateUnit dateUnit) { QueryWrapper<SysCoServcodeDTO> condition = Wrappers.query(); condition.eq("sc_serv_ident", servIdent); condition.eq("sc_prifix", codePrefix); final LocalDateTime nowTime = LocalDateTime.now(); final String year = String.valueOf(nowTime.getYear()); final String yearDigit2 = year.substring(2); final String month = formatNumDigit(2, nowTime.getMonth().getValue()); final String day = formatNumDigit(2, nowTime.getDayOfMonth()); switch (dateUnit) { /* 空选项 */ case EMPTY: condition.eq(FIELD_YEAR, SPACER).eq(FIELD_MONTH, SPACER).eq(FIELD_DAY, SPACER); break; /* 完整年月日选项 */ case YEAR: condition.eq(FIELD_YEAR, year).eq(FIELD_MONTH, SPACER).eq(FIELD_DAY, SPACER); break; case MONTH: condition.eq(FIELD_YEAR, year).eq(FIELD_MONTH, month).eq(FIELD_DAY, SPACER); break; case DAY: condition.eq(FIELD_YEAR, year).eq(FIELD_MONTH, month).eq(FIELD_DAY, day); break; /* 2数位年月日选项 */ case YEAR_DIGIT2: condition.eq(FIELD_YEAR, yearDigit2).eq(FIELD_MONTH, SPACER).eq(FIELD_DAY, SPACER); break; case MONTH_DIGIT2: condition.eq(FIELD_YEAR, yearDigit2).eq(FIELD_MONTH, month).eq(FIELD_DAY, SPACER); break; case DAY_DIGIT2: condition.eq(FIELD_YEAR, yearDigit2).eq(FIELD_MONTH, month).eq(FIELD_DAY, day); break; } return condition; } /** * 构建一个新的单号规则记录 * @param servIdent 业务标识 * @param codePrefix 单号前缀 * @param dateUnit 日期单位枚举 * @return cn.hyite.amerp.system.common.servcode.dto.SysCoServcodeDTO * @author OnCloud9 * @date 2023/1/9 16:37 * */ private SysCoServcodeDTO buildNewServCodeDTO(String servIdent, String codePrefix, DateUnit dateUnit, int initialNum) { final LocalDateTime nowTime = LocalDateTime.now(); final String year = String.valueOf(nowTime.getYear()); final String yearDigit2 = year.substring(2); final String month = formatNumDigit(2, nowTime.getMonth().getValue()); final String day = formatNumDigit(2, nowTime.getDayOfMonth()); String dateCode = null; SysCoServcodeDTO newCode = new SysCoServcodeDTO(); newCode.setScServIdent(servIdent); newCode.setScPrifix(codePrefix); newCode.setCreateTime(new Date()); newCode.setScNum(initialNum); switch (dateUnit) { case EMPTY: /* 空选项 日期部分不填充 */ newCode.setScYear(SPACER).setScMonth(SPACER).setScDay(SPACER); dateCode = ""; break; case YEAR: /* 完整年份 日期部分 2023 */ newCode.setScYear(year).setScMonth(SPACER).setScDay(SPACER); dateCode = year; break; case MONTH: /* 完整月份 日期部分 202301 */ newCode.setScYear(year).setScMonth(month).setScDay(SPACER); dateCode = year + month; break; case DAY: /* 完整日期 日期部分 20230109 */ newCode.setScYear(year).setScMonth(month).setScDay(day); dateCode = year + month + day; break; case YEAR_DIGIT2: /* 位数2年份 日期部分 23 */ newCode.setScYear(yearDigit2).setScMonth(SPACER).setScDay(SPACER); dateCode = yearDigit2; break; case MONTH_DIGIT2: /* 位数2月份 日期部分 2301 */ newCode.setScYear(yearDigit2).setScMonth(month).setScDay(SPACER); dateCode = yearDigit2 + month; break; case DAY_DIGIT2: /* 位数2日期 日期部分 230109 */ newCode.setScYear(yearDigit2).setScMonth(month).setScDay(day); dateCode = yearDigit2 + month + day; break; } newCode.setDateCode(dateCode); return newCode; } /** * 更新单号记录操作 * @param serviceCode 单号记录实体 * @param dateUnit 日期枚举 * @param length 总长度 * @return java.lang.String 单号 * @author OnCloud9 * @date 2023/1/9 16:44 * */ private String doServCodeUpdate(SysCoServcodeDTO serviceCode, DateUnit dateUnit, int length) { int prefixLen = serviceCode.getScPrifix().length(); /* 取当前单号 */ Integer currentNum = serviceCode.getScNum(); ++ currentNum; /* 取单号位数值 */ int numDigits = String.valueOf(currentNum).length(); /* 二次校验总长度 */ int estimateLength = prefixLen + dateUnit.getLength() + numDigits; Assert.isTrue(estimateLength > length, ResultMessage.CUSTOM_ERROR, "单号总长溢出!(更新时) 需要:" + estimateLength + ", 提供:" + length); /* 更新最新单号 */ serviceCode.setScNum(currentNum); baseMapper.updateById(serviceCode); /* 返回单号 */ int remainLen = length - (prefixLen + dateUnit.getLength()); String dateCode = null; switch (dateUnit) { case EMPTY: dateCode = ""; break; case YEAR: case YEAR_DIGIT2: dateCode = serviceCode.getScYear(); break; case MONTH: case MONTH_DIGIT2: dateCode = serviceCode.getScYear() + serviceCode.getScMonth(); break; case DAY: case DAY_DIGIT2: dateCode = serviceCode.getScYear() + serviceCode.getScMonth() + serviceCode.getScDay(); break; } return serviceCode.getScPrifix() + dateCode + formatNumDigit(remainLen, currentNum); } }