如何提高代码可读性-复杂的下单流程为例

引言

Hello 大家好,这里是Anyin。

在上篇文章 基于Vue3+TS+ElementPlus+Qiankun构建微应用项目 说到一个点餐微信小程序,而这个点餐肯定会涉及下单流程。这个下单流程关于金额计算会涉及到以下业务:

  1. 计算未扣除任何优惠的应付金额
  2. 判断是否使用优惠券或者参与折扣活动,优惠券和折扣活动互斥
  3. 计算加购商品,加购商品价格会比商品设置的价格优惠,并且有限购条件
  4. 计算积分抵扣,积分可以抵扣商品金额
  5. 判断是否余额支付,如果余额支付则余额金额需要比当前支付金额大
  6. 判断是否是0元,0元也可以付款

以上就是点餐微信小程序的一个下单金额计算的基本流程,当然下单流程还有其他计算,例如:判断商品库存、判断门店状态、判断优惠券状态等等,这里只是以金额计算为例;另外,这里是经过排序的业务逻辑,在实际开发过程中,业务实现的顺序可能就不是这样子的。

OK,接下来我们来看看基于这个复杂的业务,如何实现从而提高代码的可读性。

实现

在实现业务之前,我们先做一个简单的分析。

  1. 前端会输入一个商品列表
  2. 我们需要根据前端选择的商品计算应付金额
  3. 如果有优惠券或者商品参与折扣活动,则需要计算优惠金额或者折扣金额:应付金额 = 应付金额 - 该金额
  4. 计算加购商品的金额,应付金额 = 应付金额 + 该金额
  5. 计算积分抵扣金额,应付金额 = 应付金额 - 积分抵扣金额
  6. 判断是否余额支付,如果余额支付并且余额大于应付金额,则应付金额重置为0

基于以上,我们会设计一个计算各个业务金额的行为接口:

public interface ComputeAppOrderPayAmount {
    AppOrderPayAmountResp getAppOrderPayAmount();
}
复制代码

AppOrderPayAmountResp 实体类就是我们要返回给下单逻辑包含的所有关于金额的计算信息,它会包含以下信息:

@Data
public class AppOrderPayAmountResp {
    /**
     * 订单实付金额
     * 应付金额减去优惠金额 discountAmount
     */
    private BigDecimal orderAmount;
    /**
     * 订单应付金额
     * 商品单价*数量 + 规格加价
     */
    private BigDecimal dueAmount;
    /**
     * 优惠金额 = 优惠券金额 或者 折扣金额 或者 积分抵现金额
     */
    private BigDecimal discountAmount = BigDecimal.ZERO;


    /**
     * 积分抵扣 - 积分值
     * @since v1.2.5
     */
    private Long deductionScore = 0L;

    /**
     * 积分抵扣 - 金额
     * @since v1.2.5
     */
    private BigDecimal deductionAmount = BigDecimal.ZERO;

    /**
     * 余额支付金额
     * @since v1.2.7.0
     */
    private BigDecimal balancePayAmount = BigDecimal.ZERO;

}
复制代码

计算商品的应付金额

对于商品的应付金额,前端会传递商品ID和数量,而金额需要我们从数据库中获取,然后进行计算,核心逻辑如下:

    @Override
    public AppOrderPayAmountResp getAppOrderPayAmount() {
        // 前端传递的商品ID列表
        List<Long> spuIdList = CollectionUtils.map(this.payAmountReq.getReq().getSpuList(), AppOrderPayAmountReq.Spu::getSpuId);
        // 查询数据库的商品信息
        List<FoodSpu> spuList = spuService.listByIds(spuIdList);
        
        // 根据商品信息计算商品总和,包含加料的金额
        PayableAmountHandler handler = new PayableAmountHandler(this.payAmountReq.getReq().getSpuList(), spuList);
        BigDecimal amount = handler.handle().amount();
        // 设置回相应的信息
        AppOrderPayAmountResp resp = new AppOrderPayAmountResp();
        resp.setDueAmount(amount);
        resp.setPayType(this.payAmountReq.getReq().getPayType());
        return resp;
    }
复制代码

其中PayableAmountHandler 是具体的计算逻辑处理器。PayableAmountHandler#handle 方法返回的是一个Amount接口,它是一个金额的抽象,可能是应付金额、可能是优惠金额、可能是积分抵扣金额等等。

这个时候,在主逻辑代码就可以使用我们实现的类来计算应付金额,如下:

        ComputeAppOrderPayAmount dueOrderPayAmount = new DueOrderPayAmount(new PayAmountReq(contextReq.getReq()), spuService);
        AppOrderPayAmountResp resp = dueOrderPayAmount.getAppOrderPayAmount();
复制代码

计算优惠券或者折扣金额

在业务上,规定了优惠券和折扣活动是互斥的,如果用户选择的商品有参与折扣活动,则优惠券不进行计算,所以在计算金额的前置我们会先计算好本次订单是否是折扣订单。

扫描二维码关注公众号,回复: 13671494 查看本文章

接着我们看看如果是优惠券订单的处理逻辑,其实优惠券订单还区分2中情况:有优惠券和没有优惠券,我们直接看代码:

    @Override
    public AppOrderPayAmountResp getAppOrderPayAmount() {
        // 获取用户可用的优惠券列表
        List<AppCouponListItemResp> couponList = this.getUsableCouponList(this.payAmountReq.getAmount().amount(),
                this.payAmountReq.getCustomerId().getCustomerId(),
                this.payAmountReq.getSysUserId().getSysUserId(), this.payAmountReq.getReq());

        // 是否有优惠券进行不同的计算
        Handler handler = CollectionUtils.isEmpty(couponList) ?
                new NotCouponHandler(this.payAmountReq.getAmount().amount()) :
                new CouponHandler(this.payAmountReq.getAmount().amount(), couponList, this.payAmountReq.getReq());

        return handler.handle();
    }
复制代码

NotCouponHandlerCouponHandler 都实现了Handler接口,就是没有优惠券和有优惠券是不同的实现,我们看看有优惠券是如何实现的:

        @Override
        public AppOrderPayAmountResp handle() {
            // 初始化信息
            AppOrderPayAmountResp resp = new AppOrderPayAmountResp();
            resp.setDueAmount(this.amount);
            resp.setCouponList(this.couponList);
            resp.setHasDiscountSpu(AppOrderPayAmountResp.HasDiscountSpuEnum.NOT_DISCOUNT.getVal());
            // 计算优惠券金额
            FoodCouponCustomer couponCustomer = this.req.getCouponId() == null ? null : couponService.getFoodCouponCustomer(this.req.getCouponId());
            CouponAmountHandler couponAmountHandler = new CouponAmountHandler(this.couponList, couponCustomer);
            CouponAmountHandler.CouponAmount couponAmount = couponAmountHandler.handle();
            // 应付金额 - 优惠券优惠券金额
            BigDecimal orderAmount = this.amount.subtract(couponAmount.amount());

            // 0元也可以支付(优惠券的优惠券金额可能大于订单金额)
            if(orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
                orderAmount = BigDecimal.ZERO;
            }
            resp.setOrderAmount(orderAmount);
            resp.setDiscountAmount(resp.getDueAmount().subtract(orderAmount));
            return resp;
        }
复制代码

以上就是优惠券订单的处理逻辑。而对于参与折扣活动的处理逻辑会比较复杂,因为折扣活动会有2种情况:限定商品和不限定商品,也就说有可能2个商品参与了不同的折扣活动;另外折扣活动还会有2种计算方式:直接打折和间接打折,直接打折就是商品直接打几折这样子,间接打折则是购买第几件会是第几折。

整个折扣活动的计算逻辑比较复杂,这里就不贴代码了,具体可以见源码,文末附有地址。

当我们实现了相关的优惠券订单或者折扣订单的计算之后,在主逻辑的代码如下:

        // 判断是优惠券计算还是折扣计算
        // 优惠券计算和折扣计算互斥
        ComputeAppOrderPayAmount couponOrDiscountOrderPayAmount = contextReq.getIsCouponOrder() ?
                new CouponOrderPayAmount(payAmountReq, couponService) : new DiscountOrderPayAmount(payAmountReq, contextReq.getAcDiscountMap());
        resp = couponOrDiscountOrderPayAmount.getAppOrderPayAmount();
复制代码

计算加购商品的金额

在业务上,在用户下单的界面还会出现商品加购的场景(参见瑞幸咖啡APP),当用户选择了某个商品,那与之配套推荐的商品会出现在订单界面供用户选择,并且价格会比正常的商品选购界面便宜。

所以,当用户选择了加购商品,则还需要计算加购商品的金额,并且加购上是不参与优惠券或者折扣活动的计算的,并且可能会限购。核心代码如下:

 @Override
    public AppOrderPayAmountResp getAppOrderPayAmount() {
        // 判断商户是否开启加购
        if(!moreBuyService.isOpenMoreBuyFlag(payAmountReq.getSysUserId().getSysUserId())){
            return this.resp;
        }
        // 判断加购商品列表是否为空
        if(CollectionUtils.isEmpty(payAmountReq.getReq().getMoreBuySpuList())){
            return this.resp;
        }
        // 查询加购商品的信息
        Map<Long,MoreBuySpuOrderDto> moreBuySpuList = CollectionUtils.toMap(payAmountReq.getReq().getMoreBuySpuList(), MoreBuySpuOrderDto::getId);
        MoreBuyQryService moreBuyQryService = moreBuyService.getMoreBuyQryService();
        List<FoodMoreBuySpu> modelMoreBuySpuList = moreBuyQryService.getMoreBuySpuList(Lists.newArrayList(moreBuySpuList.keySet()));

        // 校验限购
        for(FoodMoreBuySpu entity : modelMoreBuySpuList){
            MoreBuySpuOrderDto moreBuySpu = moreBuySpuList.get(entity.getId());
            if(moreBuySpu != null){
                this.checkLimitNum(moreBuySpu, entity);
            }
        }

        // 计算价格
        BigDecimal total = BigDecimal.ZERO;
        for(FoodMoreBuySpu entity : modelMoreBuySpuList){
            MoreBuySpuOrderDto moreBuySpu = moreBuySpuList.get(entity.getId());
            if(moreBuySpu != null){
                BigDecimal price = new BigDecimal(moreBuySpu.getQuantity()).multiply(entity.getPrice());
                total = total.add(price);
            }
        }

        // 添加加购商品的价格
        BigDecimal orderAmount = resp.getOrderAmount().add(total);
        BigDecimal dueAmount = resp.getDueAmount().add(total);
        resp.setOrderAmount(orderAmount);
        resp.setDueAmount(dueAmount);
        return resp;
    }
复制代码

在主逻辑的代码如下:

        ComputeAppOrderPayAmount moreBuySpuOrderPayAmount = new MoreBuySpuOrderPayAmount(payAmountReq, resp, moreBuyService);
        resp = moreBuySpuOrderPayAmount.getAppOrderPayAmount();
复制代码

计算积分抵扣金额

在业务上,如果商户开启了积分抵扣,那么用户在下单界面还会出现积分抵扣选择。用户购买了商品之后,会进行积分,然后在下一次购买的时候,积分可以抵扣一部分的金额。积分抵扣相关逻辑如下:

    @Override
    public AppOrderPayAmountResp getAppOrderPayAmount() {
        // 没有积分抵扣直接返回
        if(!ScoreDeductionFlag.DEDUCTION.getVal().equals(payAmountReq.getReq().getDeductionScore())){
            return resp;
        }
        // 查询积分配置
        ScoreQryService scoreQryService = scoreService.getScoreQryService();
        FoodScoreConfig config = scoreQryService.getScoreConfig(this.payAmountReq.getSysUserId().getSysUserId());

        // 是否有配置
        if(config == null){
            return this.resp;
        }
        // 积分配置是否开
        if(FoodScoreOpenFlagEnum.CLOSE.getVal().equals(config.getOpenFlag())){
            return this.resp;
        }
        // 积分抵扣配置是否开
        if(FoodScoreDeduFlagEnum.CLOSE.getVal().equals(config.getDeduFlag())){
            return this.resp;
        }
        // 获取用户可用积分
        Long usableScore = scoreService.getScoreUsable(this.payAmountReq.getSysUserId().getSysUserId(),
                this.payAmountReq.getCustomerId().getCustomerId());
        ScoreAmountHandler amountHandler = new ScoreAmountHandler(usableScore,
                this.payAmountReq.getAmount().amount(),
                config.getDeduRule());
        // 计算积分抵扣
        ScoreAmountHandler.ScoreAmount scoreAmount = amountHandler.handle();
        this.resp.setOrderAmount(resp.getOrderAmount().subtract(scoreAmount.getAmount()));
        this.resp.setDiscountAmount(resp.getDueAmount().subtract(resp.getOrderAmount()));
        this.resp.setDeductionScore(scoreAmount.getScore());
        this.resp.setDeductionAmount(scoreAmount.getAmount());
        return this.resp;
    }
复制代码

在主逻辑如下

        // 积分抵扣计算
        ComputeAppOrderPayAmount scoreDeduction = new ScoreComputeAppOrderPayAmount(scoreService, payAmountReq, resp);
        resp = scoreDeduction.getAppOrderPayAmount();
复制代码

计算余额支付金额

在业务上,当以上所有的业务都计算完成之后,如果商户开启了可以使用余额支付,则用户在下单界面可以选择使用余额支付,在使用余额支付的时候,我们需要把应付金额重置为0,因为该字段会用来判断前端和后端是否进行唤起微信支付和微信预下单操作。

余额支付的场景是只有当余额大于等于应付金额才可以进行余额支付,因为这里会涉及到退款,如果出现部分支付,退款的场景会比较麻烦,所以当时业务规定了余额支付只能全额支付。

    @Override
    public AppOrderPayAmountResp getAppOrderPayAmount() {
        // 订单金额小于0,直接返回
        if(resp.getOrderAmount().compareTo(BigDecimal.ZERO) <=0 ){
            return resp;
        }
        BigDecimal balanceAmount = this.getCustomerBalance();
        // 余额比订单金额小,直接返回
        if(balanceAmount.compareTo(resp.getOrderAmount()) < 0){
            return resp;
        }
        // 非余额支付,直接返回
        if(!PayTypeEnum.BALANCE.getVal().equals(req.getPayType())){
            return resp;
        }
        // 余额支付会全部抵扣订单金额
        resp.setBalancePayAmount(resp.getOrderAmount());
        resp.setOrderAmount(BigDecimal.ZERO);
        return resp;
    }
复制代码

在主业务逻辑如下:

        // 余额支付
        BalancePayAmountReq balancePayAmountReq = BalancePayAmountReq.builder().customerId(customerId).sysUserId(sysUserId)
                .payType(contextReq.getReq().getPayType())
                .build();
        ComputeAppOrderPayAmount balanceOrderPay = new BalanceOrderPayAmount(resp, balancePayAmountReq, balanceV2Service);
        resp = balanceOrderPay.getAppOrderPayAmount();
复制代码

主业务逻辑

至此,基本的金额计算逻辑我们已经处理完,我们可以看看主业务逻辑的代码,很明细清晰了很多,不同的业务都划分到对应的类去处理了,而且消除了很多的if情况。

image.png

最后

以上就是在复杂业务情况下,通过基本的需求分析,把不同业务的处理逻辑归纳到不同的类里面去,通过统一的行为处理方法进行抽象,从而使得主逻辑的代码清晰明了,为后续业务如果需要添加其他逻辑打下坚实基础。

当然,以上业务处理以及整个项目还是有很多需要优化的地方,但是代码嘛总是一步一步来的,这段主逻辑也是经过2-3次的迭代才优化成这样子,如果有认为哪里不对的,请轻喷。

相关源码地址:gitee.com/anyin/anyin…

如果您觉得该项目对您有价值,记得点个赞哦。

猜你喜欢

转载自juejin.im/post/7053807707047854117