定时生成订单 —策略模式+责任链

402 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

最近开发遇到的一个业务场景,需要定时去扫表实现我们的业务单据的生成,关于定时任务自然使用的是热度比较高的开源定时job框架Xxl-job,开箱即用很方便,下面在说开发中的业务场景,我需要根据上游生成的业务订单,来生成我负责系统的业务单据,属于需要在一定的条件下去定时扫多张表,如果扫表生成业务单据失败,还需要开启一个定时补偿任务去重试生成,乍一看不能很难,但是要设计的比较合理且好扩展就需要好好好的思考一下。

业务场景

65B96B8B-2C99-4AB9-B05D-D1A1D7BB29EB.png

分别为上游系统和下游系统的交互。

设计思路

数据库表结构设计

首先,两个定时任务一个正常生成、一个失败补偿,定时任务在某个时间的频率段去定时扫表生成单子,生成成功下游的业务表里会增加对应的业务数据,同时也需要去更新上游订单的状态,成功后更新一下标识,上游订单生成状态是已完成的单子不会再去重复生成下游单据。

如果生成失败则需要写入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方法,针对订单的不同派生出不同的子类。

45CB28EF-066A-4C6E-AE9C-E059481437C9.png

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,增加代码的可读性,减少一个方法里面过多的代码量,做到代码可读性、可扩展、可维护,这才是一个优秀的程序员所应该具备的,如有需要优化的地方请铁子们在评论区指出🙏。