开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
前言
最近开发遇到的一个业务场景,需要定时去扫表实现我们的业务单据的生成,关于定时任务自然使用的是热度比较高的开源定时job框架Xxl-job,开箱即用很方便,下面在说开发中的业务场景,我需要根据上游生成的业务订单,来生成我负责系统的业务单据,属于需要在一定的条件下去定时扫多张表,如果扫表生成业务单据失败,还需要开启一个定时补偿任务去重试生成,乍一看不能很难,但是要设计的比较合理且好扩展就需要好好好的思考一下。
业务场景
分别为上游系统和下游系统的交互。
设计思路
数据库表结构设计
首先,两个定时任务一个正常生成、一个失败补偿,定时任务在某个时间的频率段去定时扫表生成单子,生成成功下游的业务表里会增加对应的业务数据,同时也需要去更新上游订单的状态,成功后更新一下标识,上游订单生成状态是已完成的单子不会再去重复生成下游单据。
如果生成失败则需要写入retry补偿表,失败补偿的定时任务会去定时扫这张表,根据记录的上游数据生成失败的订单号,继续去扫表生成下游数据,当然这里也不能说是一直失败就一直生成,retry_num(补偿次数)字段最多补偿3次,每次生成失败的数据都会将这个次数+1,超过三次就不会去扫描表生成,此时就确定是上游数据的问题就需要开发人员自己去排查数据问题。
重试表结构
CREATE TABLE `order_fee_fail_retry` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`order_type` int(1) NOT NULL COMMENT '订单类型 1-xxx订单 2-xxx订单 3-xxx订单 4-xxx订单 5-xxx订单',
`fee_type` int(1) NOT NULL COMMENT '费用类型 ',
`fee_status` int(1) NOT NULL DEFAULT '4' COMMENT '费用状态 1-费用未生成 2-费用生成中 3-费用生成成功 4-费用生成失败',
`retry_num` int(1) NOT NULL DEFAULT '0' COMMENT '补偿次数',
`fail_reason` varchar(255) DEFAULT NULL COMMENT '失败原因',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否删除(0-已删除,1-有效)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_user_no` varchar(32) NOT NULL DEFAULT 'SYS' COMMENT '创建人编号',
`create_user_name` varchar(50) NOT NULL DEFAULT '系统' COMMENT '创建人姓名',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间(最新操作时间)',
`update_user_no` varchar(32) DEFAULT NULL COMMENT '修改人编号',
`update_user_name` varchar(50) DEFAULT NULL COMMENT '修改人姓名(最新操作人)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
主要关注:上游订单号、上游订单类型、费用状态、补偿次数这个四个字段。
目前业务上游订单是四种,需要根据不同的订单类型生成不同的下游计费单。
上游的订单表里面需要加入fee_status(费用状态 1-费用未生成 2-费用生成中 3-费用生成成功 4-费用生成失败),这是一个新加的字段只由我做的定时生成计费单来维护,不会去动其他业务字段,毕竟是两个系统,我们不能去破坏上游数据。
代码实现
首先我需要去查上游订单组装下游生成的业务数据,为了不在一个方法里写很多查询去组装数据,这里我使用的是策略模式来依次组装我需要的数据,废话不多说先上代码。
Xxljob定时入口:
/**
* 订单&计费单定时任务
*/
@Slf4j
@Component
public class OrderFeeXxlJob {
@Autowired
private CommonBillingOrderService commonBillingOrderService;
/**
* 根据订单生成计费单
*
*/
@XxlJob("orderCreateFee")
public ReturnT<String> orderCreateFee(String param) {
XxlJobLogger.log("根据订单生成计费单任务开始!");
log.info("根据订单生成计费单任务开始!");
try {
//生成计费单
commonBillingOrderService.orderCreateFee();
XxlJobLogger.log("根据订单生成计费单任务结束!");
log.info("根据订单生成计费单任务结束!");
return ReturnT.SUCCESS;
}catch (Exception e){
XxlJobLogger.log("根据订单生成计费单任务异常!"+e);
log.error("根据订单生成计费单任务异常!",e);
return ReturnT.FAIL;
}
}
/**
* 失败重试生成计费单
*
*/
@XxlJob("retryCreateFee")
public ReturnT<String> retryCreateFee(String param) {
XxlJobLogger.log("失败重试生成计费单任务开始!");
log.info("失败重试生成计费单任务开始!");
try {
//失败补偿生成计费单
commonBillingOrderService.retryCreateFee();
XxlJobLogger.log("失败重试生成计费单任务结束!");
log.info("失败重试生成计费单任务结束!");
return ReturnT.SUCCESS;
}catch (Exception e){
XxlJobLogger.log("失败重试生成计费单任务异常!"+e);
log.error("失败重试生成计费单任务异常!",e);
return ReturnT.FAIL;
}
}
}
策略调用链组装数据:
@Component
public class OrderFeeHandlerClient {
@Autowired
private ProductSalesOrderHandler productSalesOrderHandler;
@Autowired
private ProductPurchaseOrderHandler productPurchaseOrderHandler;
@Autowired
private OrderMaterialSellHandler orderMaterialSellHandler;
@Autowired
private OrderMaterialBuyHandler orderMaterialBuyHandler;
@Autowired
private OrderReturnHandler orderReturnHandler;
public OrderFeeHandler getAndInitFirstHandler() {
productSalesOrderHandler.setNext(productPurchaseOrderHandler);
productPurchaseOrderHandler.setNext(orderMaterialSellHandler);
orderMaterialSellHandler.setNext(orderReturnHandler);
orderReturnHandler.setNext(orderMaterialBuyHandler);
return productSalesOrderHandler;
}
}
OrderFeeHandler调用链抽象父类
主要定义组装数据和更新结果的抽象方法。
@Data
public abstract class OrderFeeHandler {
/**
* 下一个处理类
*/
private OrderFeeHandler next;
/**
* 获取数据
*
* @param receivableFeeSaveDTOList
* @param payableFeeSaveDTOList
* @param minutesAgo
*/
public abstract void getOrderFeeData(List<ReceivableFeeSaveDTO> receivableFeeSaveDTOList, List<PayableFeeSaveDTO> payableFeeSaveDTOList,
List<FarmerPayableFeeDTO> farmerPayableFeeDTOList, List<FarmerReceivableFeeDTO> farmerReceivableFeeDTOList, String minutesAgo);
public void getOrderFeeDataProcess(List<ReceivableFeeSaveDTO> receivableFeeSaveDTOList, List<PayableFeeSaveDTO> payableFeeSaveDTOList,
List<FarmerPayableFeeDTO> farmerPayableFeeDTOList, List<FarmerReceivableFeeDTO> farmerReceivableFeeDTOList, String minutesAgo) {
this.getOrderFeeData(receivableFeeSaveDTOList, payableFeeSaveDTOList, farmerPayableFeeDTOList, farmerReceivableFeeDTOList, minutesAgo);
if (next != null) {
next.getOrderFeeDataProcess(receivableFeeSaveDTOList, payableFeeSaveDTOList, farmerPayableFeeDTOList, farmerReceivableFeeDTOList, minutesAgo);
}
}
/**
* 更新结果
*
* @param feeSaveResultVOList
*/
public abstract void updateResult(List<FeeSaveResultVO> feeSaveResultVOList);
public void updateResultProcess(List<FeeSaveResultVO> feeSaveResultVOList) {
this.updateResult(feeSaveResultVOList);
if (next != null) {
next.updateResultProcess(feeSaveResultVOList);
}
}
}
继承抽象父类,可以重写getOrderFeeData和updateResult方法,针对订单的不同派生出不同的子类。
updateResult更新结果的方法也一样,这里定义了一个泛型类FeeResultUtil<T, K> ,对应的“T”代表实体类,“k”代表orm层对应的mapper,也就是需要更新的mapper对应的实体类。
@Component
public class FeeResultUtil<T, K> {
@Autowired
private OrderFeeFailRetryService orderFeeFailRetryService;
/**
* 生成计费单后,更新订单的计费结果
* @param feeSaveResultVOList
* @param tClass
* @param orderType
* @param orderNoField
* @param k
* @param methodName
*/
public void handlerResult(List<FeeSaveResultVO> feeSaveResultVOList, Class<T> tClass, Integer orderType, String orderNoField, K k, String methodName) {
if (CollectionUtil.isEmpty(feeSaveResultVOList)) {
return;
}
//获取原材料采购订单
List<FeeSaveResultVO> resultVOList = feeSaveResultVOList.stream().filter(f -> orderType.equals(f.getOrderType())).collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(resultVOList)) {
List<T> tList = Lists.newArrayList();
List<OrderFeeFailRetry> orderFeeFailRetryList = Lists.newArrayList();
Date now = new Date();
//订单号分组
Map<String, List<FeeSaveResultVO>> collect = resultVOList.stream().collect(Collectors.groupingBy(FeeSaveResultVO::getOrderNo));
for (Map.Entry<String, List<FeeSaveResultVO>> entry : collect.entrySet()) {
T t = ReflectUtil.newInstance(tClass);
String orderNo = entry.getKey();
List<FeeSaveResultVO> saveResultVOList = entry.getValue();
boolean b = saveResultVOList.stream().anyMatch(f -> !f.getResult());
if (b) {
//说明至少有一条失败,则结果为失败
ReflectUtil.setFieldValue(t, "feeStatus", FeeStatusEnum.FEE_STATUS_4.getCode());
//获取计费结果失败的订单
List<FeeSaveResultVO> resultVOS = saveResultVOList.stream().filter(f -> !f.getResult()).collect(Collectors.toList());
for (FeeSaveResultVO resultVO : resultVOS) {
OrderFeeFailRetry orderFeeFailRetry = new OrderFeeFailRetry();
orderFeeFailRetry.setOrderNo(orderNo);
orderFeeFailRetry.setOrderType(resultVO.getOrderType());
orderFeeFailRetry.setFeeType(resultVO.getFeeType());
orderFeeFailRetry.setFeeStatus(FeeStatusEnum.FEE_STATUS_4.getCode());
orderFeeFailRetry.setFailReason(null);
orderFeeFailRetryList.add(orderFeeFailRetry);
}
} else {
//说明全部成功才算最终成功
ReflectUtil.setFieldValue(t, "feeStatus", FeeStatusEnum.FEE_STATUS_3.getCode());
}
ReflectUtil.setFieldValue(t, orderNoField, orderNo);
ReflectUtil.setFieldValue(t, "updateTime", now);
tList.add(t);
}
//更新结果
if (CollectionUtil.isNotEmpty(tList)) {
List<List<T>> partition = Lists.partition(tList, 50);
for (List<T> list : partition) {
//更新计费结果
ReflectUtil.invoke(k, methodName, list, 2);
}
}
if (CollectionUtil.isNotEmpty(orderFeeFailRetryList)){
orderFeeFailRetryList = orderFeeFailRetryList.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(OrderFeeFailRetry::getOrderNo))), ArrayList::new));
}
//保存失败重试记录
if (CollectionUtil.isNotEmpty(orderFeeFailRetryList)) {
orderFeeFailRetryService.saveBatch(orderFeeFailRetryList);
}
}
}
至于需要更新的结果是根据通过feign接口调用下游生成业务数据的结果来更新的,其他的一些业务代码这里就不做一一介绍了。
这里通过反射获取实体对象,批量更新实体,这里用的cn.hutool.core.util 包下的ReflectUtil工具类,进行反射获取,如果一次性获取需要更新的实体条数过多对数据库造成压力,可以使用Lists./partition/(tList, 50);对结果条数进行分组每50条更新一次。
T t = ReflectUtil./newInstance/(tClass);
关于重试补偿的job的实现基本跟生成的结构基本相同,可以借鉴。
总结
运用策略模式+责任链,在后期如果还需要增加另外一种订单类型,只需要在父类的基础上在增加一个子类的handler,增加代码的可读性,减少一个方法里面过多的代码量,做到代码可读性、可扩展、可维护,这才是一个优秀的程序员所应该具备的,如有需要优化的地方请铁子们在评论区指出🙏。