当前位置: 首页>后端>正文

设计一个统一算费流程

为什么我们需要一个统一的算费流程。

我在第二家公司的时候,从从事的是医疗领域的电商,当时我们的优惠相关的计算嵌入在订单的代码里面。

public abstract class OrderSubmitHandler {

    /**
     * 1. 初始化订单信息
     *    init: storeVO,orderMainDO,orderSubDOList,freeFlag
     */
    public abstract OrderSubmitDTO initOrder(OrderSubmitRequest request);

    /**
     * 2. 组装优惠券优惠,同时初始化实物订单的order_sub
     */
    public abstract void handleCoupon(OrderSubmitDTO result);

    /**
     * 3. DTC + VIP
     */
    public abstract void handleDTCAndVIP(OrderSubmitDTO result);

    /**
     * 4. 计算orderMain金额
     */
    public abstract void computeMainOrderPay(OrderSubmitDTO result);

    /**
     * 5. 插入订单主表、子订单、快照信息、支付单信息
     */
    public abstract void insertDB(OrderSubmitDTO result);

    /**
     * 6. 生成预约信息
     */
    public abstract void insertReserve(OrderSubmitDTO result);

    /**
     * 7. 生成返回值
     */
    public abstract OrderAddResponse getResponse(OrderSubmitDTO result);

    /**
     *  提交流程
     * */
    @Transactional
    public OrderAddResponse submitOrder(OrderSubmitRequest request){
        log.info("request:{}",JSON.toJSONString(request));

        //1. 初始化
        OrderSubmitDTO result = this.initOrder(request);
        log.info( "initOrder:{}", JSON.toJSONString(result) );

        //2. 组装优惠券优惠
        this.handleCoupon(result);
        log.info( "handleCoupon:{}", JSON.toJSONString(result) );

        //先计算dtc,vip只优惠首次支持金额的88折,即7%的88折扣
        //3. DTC + vip的优惠
        this.handleDTCAndVIP(result);
        log.info( "handleDTCAndVIP:{}", JSON.toJSONString(result) );

        //4. 计算order_main金额
        this.computeMainOrderPay(result);
        //this.handleVipCoupon(result);
        //5. 插入订单主表、子订单、快照信息、支付单信息
        this.insertDB(result);

        //6. 如果免费单,生成预约信息
        this.insertReserve(result);

        OrderAddResponse response = this.getResponse(result);
        return response ;
    }

}

每当优惠扩展的时候我们不得不增加流程或者增加优惠计算的代码,由于优惠代码又是平铺式的编写,导致后期复杂的代码难以维护,可读写性也大大降低。

于是我希望设计这样一段代码,每一个扣减逻辑是一个单独的算子,可以分开实现,费用的计算可以由多个算子组成,算子可以排序实现任意流程的编排。算子需要足够的抽象,不止支持优惠卷,还是支持其他抵扣的虚拟货币。我希望可以保存每种计算方式的金额,这样就可以在订单里展示优惠详情,每一项抵扣了多少金额。

设计

首先我们需要设计一个算子接口

public interface FeeCalculate<O> {

    /**
     * 根据费用项计算每个费用项明细
     *
     * @param list
     * @return
     */
    Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list);

    /**
     * 每个支付方式waitPay
     *
     * @param list
     * @return
     */
    Map<FeeItemType, BigDecimal> calculateWaitPay(List<FeeItem<O>> list);

    /**
     * 获取计算器的唯一编码
     *
     * @return
     */
    Unique getUnique();

}

其中FeeItem代表一个单独的费用项,必须邮费,打包费之类的。

public interface FeeItem<O> {

    /**
     * 原始金额
     *
     * @return
     */
    BigDecimal getFeeItemOriginMoney();

    /**
     * 费用类型
     *
     * @return
     */
    FeeItemType getFeeItemType();

    /**
     * 获取订单原始信息
     *
     * @return
     */
    O getOrderInfo();

}

不用的费用需要不同的计算方式,所以我们需要不同的type来表示

/**
 * 为了区分不同费用不同计算方式
 */
public enum FeeItemType implements BaseEnum<FeeItemType> {

    SERVICE_FEE(1, "服务费"),
    ELECTRIC_FEE(2, "电费"),
    OVER_WEIGHT_FEE(3, "超重费"),
    OVER_TIME_FEE(4, "超时费");

    FeeItemType(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    private Integer code;
    private String name;

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public static Optional<FeeItemType> of(Integer code) {
        return Optional.ofNullable(BaseEnum.parseByCode(FeeItemType.class, code));
    }

}

不同的费用由不同的方式支付或者抵扣

public interface PayItem {

  BigDecimal getMoney();

  PayGroup getPayGroup();

  PayType getPayType();

}

PayGroup代表不同的支付方式

public enum PayGroup implements BaseEnum<PayGroup> {

    THIRD_PAY(1, "三方支付"),
    PLATFORM_PAY(2, "平台支付"),
    VIRTUAL_PROPERTY(3, "虚拟资产"),
    BANK(4, "银行卡支付"),
    COUPON(4, "优惠劵");

    PayGroup(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    private Integer code;
    private String name;

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public static Optional<PayGroup> of(Integer code) {
        return Optional.ofNullable(BaseEnum.parseByCode(PayGroup.class, code));
    }

}

PayType代表具体的支付类型,比如三方支付下的微信支付。

public enum PayType implements BaseEnum<PayType> {

  WECHAT(1, "微信支付"),
  ALIPAY(2,"支付宝"),
  COIN(3,"虚拟币"),
  ACTIVITY(4,"活动")
  ;

  PayType(Integer code, String name) {
    this.code = code;
    this.name = name;
  }

  private Integer code;
  private String name;

  @Override
  public Integer getCode() {
    return this.code;
  }

  @Override
  public String getName() {
    return this.name;
  }

  public static Optional<PayType> of(Integer code) {
    return Optional.ofNullable(BaseEnum.parseByCode(PayType.class, code));
  }

}

在上面的需求中,我希望算子只需要实现自己的业务逻辑,不需要关注算子的编排和具体的执行逻辑,当前算子需要上一个算子的计算结果,所以我需要使用装饰器模式来增强算子的能力,并使用责任链链接多个算子,规划算子执行的流程。

public abstract class AbstractCalculator<O> implements FeeCalculate<O> {

    private final FeeCalculate<O> feeCalculate;

    private final Unique unique;

    protected AbstractCalculator(FeeCalculate<O> feeCalculate, Unique unique) {
        this.feeCalculate = feeCalculate;
        this.unique = unique;
    }

    @Override
    public Unique getUnique() {
        return unique;
    }

    /**
     * 当前抵扣
     */
    protected abstract Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left, O o);

    /**
     * 当前抵扣的明细
     */
    protected abstract Map<FeeItemType, List<PayItem>> payItemList();

    @Override
    public Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list) {
        //初始化算子的支付项
        Map<FeeItemType, List<PayItem>> map;
        if (Objects.nonNull(feeCalculate) && Objects.nonNull(feeCalculate.payItemList(list))) {
            map = feeCalculate.payItemList(list);
        } else {
            map = Maps.newHashMap();
        }
        //获取已经存在的支付项
        Map<FeeItemType, List<PayItem>> currentList = payItemList();
        //合并算子付费项
        if (Objects.nonNull(currentList) && !currentList.isEmpty()) {
            currentList.forEach((key, value) -> {
                List<PayItem> tempList = map.getOrDefault(key, Lists.newArrayList());
                tempList.addAll(value);
                map.put(key, tempList);
            });
        }
        return map;
    }

    @Override
    public Map<FeeItemType, BigDecimal> calculateWaitPay(List<FeeItem<O>> list) {
        //如果没有上层包装,那么直接返回订单的实际金额减去当前抵扣的金额
        if (Iterables.isEmpty(list)) {
            //计费项为空
            throw new RuntimeException(FeeEnum.FEE_ITEM_EMPTY.getName());
        }
        Map<FeeItemType, BigDecimal> leftMap = Maps.newHashMap();
        if (Objects.isNull(feeCalculate)) {
            for (FeeItem<O> item : list) {
                leftMap.put(item.getFeeItemType(), item.getFeeItemOriginMoney());
            }
            Map<FeeItemType, BigDecimal> currentDeduct = currentPayItem(leftMap,
                    list.get(0).getOrderInfo());
            //合并费用
            currentDeduct.forEach(
                    (key, value) -> leftMap.put(key, NumberUtil.sub(leftMap.get(key), value))
            );
            return leftMap;
        } else {
            //存在下一个算子,流程未完
            Map<FeeItemType, BigDecimal> left = feeCalculate.calculateWaitPay(list);
            //如果有任何一个
            Optional<BigDecimal> greaterThanZero = left.values().stream()
                    .toList().stream()
                    //过滤出所有不为零的费用
                    .filter(s -> NumberUtil.isGreater(s, BigDecimal.ZERO))
                    .findFirst();
            //算子无抵扣项直接返回
            if (greaterThanZero.isEmpty()) {
                return left;
            }
            Map<FeeItemType, BigDecimal> current = currentPayItem(left, list.get(0).getOrderInfo());
            Map<FeeItemType, BigDecimal> temp = Maps.newHashMap();
            for (FeeItem<O> item : list) {
                //如果当前有抵扣
                if (Objects.nonNull(current.get(item.getFeeItemType()))) {
                    //超过剩余支付金额,抛出异常
                    if (NumberUtil.isGreater(current.get(item.getFeeItemType()),
                            left.get(item.getFeeItemType()))) {
                        throw new RuntimeException(FeeEnum.AMOUNT_GREATER_ERROR.getName());
                    }
                    //正常抵扣
                    temp.put(item.getFeeItemType(),
                            NumberUtil.sub(left.get(item.getFeeItemType()), current.get(item.getFeeItemType())));
                } else {
                    //如果当前没有抵扣,直接返回剩余金额
                    temp.put(item.getFeeItemType(), left.get(item.getFeeItemType()));
                }
            }
            return temp;
        }
    }
}

测试用例

1.当前订单是一个简单的服务费支付订单,用户参加了一个活动,使用了一个优惠券,希望计算出需要支付的实际金额.

初始化抵扣算子

/**
 * 服务费抵扣活动
 */
public class ActivityCalculator extends AbstractCalculator<OrderInfo> {

  public ActivityCalculator(FeeCalculate<OrderInfo> feeCalculate) {
    super(feeCalculate, CalculateType.ACTIVITY);
  }

  /**
   * 算子抵扣金额
   */
  @Override
  protected Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left,
                                                        OrderInfo o) {
    Map<FeeItemType, BigDecimal> map = Maps.newHashMap();
    map.put(FeeItemType.SERVICE_FEE, new BigDecimal("4"));
    System.out.println("活动抵扣了4元费用");
    return map;
  }

  @Override
  protected Map<FeeItemType, List<PayItem>> payItemList() {
    Map<FeeItemType, List<PayItem>> map = Maps.newHashMap();
    List<PayItem> payItems = Lists.newArrayList();
    ActivityPayItem ap = new ActivityPayItem(new BigDecimal(4));
    ap.setActivityName("节日活动");
    payItems.add(ap);
    map.put(FeeItemType.SERVICE_FEE, payItems);
    return map;
  }
}
/**
 * 付费用优惠券
 */
public class CouponCalculator extends AbstractCalculator<OrderInfo> {

  public CouponCalculator(FeeCalculate<OrderInfo> feeCalculate) {
    super(feeCalculate, CalculateType.COUPON);
  }

  @Override
  protected Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left,
      OrderInfo o) {
    Map<FeeItemType, BigDecimal> map = Maps.newHashMap();
    map.put(FeeItemType.SERVICE_FEE, new BigDecimal("5"));
    System.out.println("劵抵扣了5元费用");
    return map;
  }

  @Override
  protected Map<FeeItemType, List<PayItem>> payItemList() {
    Map<FeeItemType, List<PayItem>> map = Maps.newHashMap();
    List<PayItem> payItems = Lists.newArrayList();
    CouponPayItem cp = new CouponPayItem(new BigDecimal(5));
    cp.setCouponCode("C1234528");
    payItems.add(cp);
    map.put(FeeItemType.SERVICE_FEE, payItems);
    return map;
  }
}

初始化抵扣项

public class ActivityPayItem extends AbstractPayItem {

  public ActivityPayItem(BigDecimal money) {
    super(money, PayType.ACTIVITY, PayGroup.COUPON);
  }

  private String activityName;
}
public class CouponPayItem extends AbstractPayItem {

  public CouponPayItem(BigDecimal money) {
    super(money, PayType.COIN, PayGroup.COUPON);
  }

  private String couponCode;

  private String source;
}

一个普通的例子

public class CalculateTest {

    public static void main(String[] args) {
        //初始化一个简单的订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTradeFlowNo("T0323423432");
        orderInfo.setOrderType("普通订单");
        orderInfo.setPayAmount(new BigDecimal(0));
        orderInfo.setServiceFee(new BigDecimal(20));
        //初始化服务费用
        List<FeeItem<OrderInfo>> list = Lists.newArrayList();
        list.add(new ServiceFeeItem(orderInfo, FeeItemType.SERVICE_FEE, orderInfo.getServiceFee()));
        //编排算子
        FeeCalculate<OrderInfo> feeCalculate = new ActivityCalculator(new CouponCalculator(null));
        //获取待支付金额
        Map<FeeItemType, BigDecimal> leftPay = feeCalculate.calculateWaitPay(list);
        leftPay.forEach((k, v) -> {
            System.out.println("待支付项:" + k.getName() + v.toPlainString() + "元");
        });
        //抵扣项展示
        Map<FeeItemType, List<PayItem>> payItemList = feeCalculate.payItemList(list);
        payItemList.forEach((k, v) -> {
            StringBuffer sb = new StringBuffer();
            v.forEach(p -> {
                sb.append("支付类型:").append(p.getPayType().getName());
                sb.append("支付金额:").append(p.getMoney()).append("元");
                sb.append(" ! ").append("\n");
            });
            System.out.println("已经抵扣:\n" + k.getName()+"\n" + sb);
        });
    }

}

一个复杂的例子

当我们希望把一些计算的规则配置暴露给运营人员,当运营配置好规则然后查询出来生成算子。

规则相关

public interface FeeRule {

  /**
   * 获取配置的数值
   * @return
   */
  BigDecimal getConfigValue();

  /**
   * 获取规则类型
   * @return
   */
  FeeRuleType getRuleType();

  /**
   * 规则的顺序
   * @return
   */
  Integer getOrder();

}

一个简单的工厂类用来生成算子

public class CalculatorFactory {

  public static FeeCalculate<OrderInfo> getFeeCalculateByRuleType(FeeCalculate<OrderInfo> calculate, FeeRule rule) {
    if (Objects.equals(FeeRuleType.FREE_TIME, rule.getRuleType())) {
      FreeTimeRule time = (FreeTimeRule) rule;//这里可以强制转化
      return new FreeTimeCalculator(calculate, CalculatorType.FREE_TIME, time.getConfigValue().intValue());
    } else if (Objects.equals(FeeRuleType.FREE_TIMES, rule.getRuleType())) {
      FreeTimesRule timesRule = (FreeTimesRule) rule;
      return new FreeTimesCalculator(calculate, CalculatorType.FREE_TIMES, timesRule.getConfigValue().intValue());
    } else if (Objects.equals(FeeRuleType.PLUS_RULE, rule.getRuleType())) {
      //不需要可以不转
      return new PlusRuleCalculator(calculate, CalculatorType.PLUS_DISCOUNT, rule.getConfigValue());
    } else if (Objects.equals(FeeRuleType.MAX_LIMIT, rule.getRuleType())) {
      return new MaxLimitCalculator(calculate, CalculatorType.MAX_LIMIT, rule.getConfigValue());
    }
    return null;
  }


}

编写流程

public class FeeCalculateTest {


    public void testFee() {
        //初始化规则
        List<FeeRule> ruleList = Lists.newArrayList();
        FreeTimesRule freeTimesRule = new FreeTimesRule(new BigDecimal(0), FeeRuleType.FREE_TIMES, 3);
        FreeTimeRule freeTimeRule = new FreeTimeRule(new BigDecimal(1), FeeRuleType.FREE_TIME, 1);
        PlusRule plusRule = new PlusRule(new BigDecimal("0.95"), FeeRuleType.PLUS_RULE, 4);
        MaxLimitRule maxLimitRule = new MaxLimitRule(new BigDecimal("1.4"), FeeRuleType.MAX_LIMIT, 5);

        ruleList.add(freeTimesRule);
        ruleList.add(freeTimeRule);
        ruleList.add(plusRule);
        ruleList.add(maxLimitRule);

        //排序规则
        List<FeeRule> sortRules = ruleList.stream().sorted(Comparator.comparingInt(FeeRule::getOrder))
                .toList();

        //初始化支付项
        List<FeeItem<OrderInfo>> payItemList = Lists.newArrayList();
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCarNo("dddd");
        orderInfo.setParkTimes(3);
        orderInfo.setUserId(4L);
        orderInfo.setTotalMoney(new BigDecimal("30"));
        ParkingFeeItem parkingFeeItem = new ParkingFeeItem(orderInfo);
        payItemList.add(parkingFeeItem);

        //核心流程
        FeeCalculate<OrderInfo> calculate = null;
        for (FeeRule feeRule : sortRules) {
            //根据规则类型获取对应的计算器类型,生成FeeCalculate
            calculate = CalculatorFactory.getFeeCalculateByRuleType(calculate, feeRule);
        }
        //计算费用
        assert calculate != null;
        Map<FeeItemType, BigDecimal> waitPay = calculate.calculateWaitPay(payItemList);

        BigDecimal waitPayMoney = waitPay.get(FeeItemType.SERVICE_FEE);
        System.out.println("待支付金额" + waitPayMoney);

        Map<FeeItemType, List<PayItem>> map = calculate.payItemList(payItemList);

        MapUtils.debugPrint(System.out, "console", map);
        List<PayItem> payList = map.get(FeeItemType.SERVICE_FEE);
        payList.forEach(payItem -> {
                    System.out.println(payItem.getMoney());
                    System.out.println(payItem.getPayType());
                    System.out.println(payItem.getPayGroup());
                });
    }

    public static void main(String[] args) {
        FeeCalculateTest test = new FeeCalculateTest();
        test.testFee();
    }

}

写在最后

  1. 完整代码
    MyTest/src/main/java/com/fee at master · wty4427300/MyTest (github.com)

  2. 使用的设计模式
    装饰器
    工厂
    责任链


https://www.xamrdz.com/backend/3k41994387.html

相关文章: