如果说研发团队是一个可以随意替换的工具,那么所有代码都会来自需求。

先交待下需求:
在之前渠道1退款的基础上,再增加一个退款渠道。

需求分析:

退款是一个高可靠性操作。收到退款请求后,需要进行如下 操作:

  1. 校验数据合法性

  2. 校验业务合法性

  3. 执行退款

其中,校验业务合法性是个性化逻辑,不同的渠道校验的办法不同。

做过支付的同学都知道,支付操作会分为发起支付、支付中、支付完成 这三个状态。

退款也一样,执行退款后,并不知道结果。具体的结果需要由支付来回调来更新最终的退款状态。

技术分析:

操作流程固定,只是不同场景时,“校验业务合法性”的具体逻辑不同。

看到这的粉丝朋友,是不是也想到了相同部分提取到抽象父类,不同的部分各自实现。

是的。是这个场景,虽然"组合大于继承",就流程执行的连贯性、可读性、可理解性,还是使用模板+抽象类更合适一些。

上类图

退款渠道1:

图片

退款渠道2:

图片

上代码

要干的事:退款


import java.math.BigDecimal;

public interface AfterSalesRefundService {

    void refund(String soNo, String afterSaleNo, BigDecimal amount);

}

退款的动作拆解:

  
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service
@Slf4j
public abstract class AbstractAfterSalesRefundService implements AfterSalesRefundService {


    @Override
    public void refund(String soNo, String afterSaleNo, BigDecimal amount) {
        // TODO: 2024/7/22 数据合法性检查
        /**
         * 具体渠道业务校验
         */
        doubleCheckValid(soNo, afterSaleNo, amount);
        // TODO: 2024/7/22 执行具体的退款操作 
    }

    /**
     * 退款时,不同渠道传不同的标识,方便后期业务梳理,也方便研发排查问题
     *
     * @return
     */
    abstract String getFastRefundPrefix();

    abstract void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount);

}

渠道1中的个性化校验逻辑:

  


import com.alibaba.fastjson.JSON;
import com.payment.core.domain.dto.ResultDTO;
import com.payment.core.exception.GlobalCode;
import com.payment.payment.api.manager.order.OrderFeignClient;
import com.payment.business.refund.enums.RefundServiceNameConstant;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service(RefundServiceNameConstant.SELF_RUN_SERVICE)
@Slf4j
public class AfterSalesSelfRunRefundServiceImpl extends AbstractAfterSalesRefundService {

    public static final String SELF_RUN_FAST_REFUND_PREFIX = "自营售后快速退款";

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Override
    String getFastRefundPrefix() {
        return SELF_RUN_FAST_REFUND_PREFIX;
    }

    @Override
    void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount) {
        ResultDTO<String> resultDTO = orderFeignClient.selfRunRefundCheck(soNo, afterSaleNo, amount);
        if (!resultDTO.getSuccess()) {
            log.info(" 自营售后单退款 soNo {} afterSaleNo {} 售后单校验失败 {} ", soNo, afterSaleNo, JSON.toJSONString(resultDTO));
            throw new PaymentAfterSaleException(GlobalCode.BAD_REQUEST.setMsg("自营售后单校验失败 " + afterSaleNo));
        }
    }


}

渠道2中的个性化校验逻辑:

  
import com.alibaba.fastjson.JSON;
import com.payment.core.domain.dto.ResultDTO;
import com.payment.core.exception.GlobalCode;
import com.payment.adapter.http.vc.order.VCOrderFeign;
import com.payment.business.refund.enums.RefundServiceNameConstant;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @Auther: cheng.tang
 * @Date: 2024/6/11
 * @Description: zkh-gbb-payment
 */
@Service(RefundServiceNameConstant.VPI_SERVICE)
@Slf4j
public class AfterSalesVPIRefundServiceImpl extends AbstractAfterSalesRefundService {

    public static final String VPI_FAST_REFUND_PREFIX = "售后快速退款";

    @Autowired
    private VCOrderFeign vcOrderFeign;

    @Override
    String getFastRefundPrefix() {
        return VPI_FAST_REFUND_PREFIX;
    }

    @Override
    void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount) {
        ResultDTO<String> refundCheck = vcOrderFeign.refundCheck(soNo, afterSaleNo, amount);
        if (!refundCheck.getSuccess()) {
            log.info("VPI售后单退款 soNo {} afterSaleNo {} 售后单校验失败 {} ", soNo, afterSaleNo, JSON.toJSONString(refundCheck));
            throw new PaymentAfterSaleException(GlobalCode.BAD_REQUEST.setMsg("VPI售后单校验失败 " + afterSaleNo));
        }
    }

}

服务已经有了,如何提供一个友好的调用入口呢?

根据不同的入参灵活地切换算法或操作的场景,适合哪种设计模式?
工厂?策略?
对的,是策略模式。

来一起回顾下策略模式的用法:

策略模式包含策略、上下文、客户端。

具体,有下面几个角色:

角色1:抽象得到的策略接口 

角色2:具体策略 

角色3:上下文 

角色4:客户端

唐成,公众号:的数字化之路如果策略模式的代码有段位,你的是白银?黄金?还是王者?

策略接口和具体策略【退款渠道】已经有了,缺策略上下文和客户端。

在写策略上下文和调用策略的客户端之前,先做个CleanCode方面的准备:
1、代替魔法字符串的常量:


public class RefundServiceNameConstant {

    public static final String VPI_SERVICE = "VPIRefund";
    public static final String SELF_RUN_SERVICE = "SelfRunRefund";

}

是的,上面两个策略的自定义Bean名就是这两个常量。

2、作为调用参数的标识:枚举类

  
import lombok.Getter;

@Getter
public enum RefundEnums {
    VPI("VPI退款", RefundServiceNameConstant.VPI_SERVICE),
    SELF_RUN("自营退款", RefundServiceNameConstant.SELF_RUN_SERVICE);

    private final String memo;
    private final String serviceName;

    RefundEnums(String memo, String serviceName) {
        this.memo = memo;
        this.serviceName = serviceName;
    }

}

上面这两个类是铺垫,正戏“策略上下文”到了:

import com.payment.core.exception.GlobalCode;
import com.payment.business.refund.enums.RefundEnums;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class AfterSalesRefundContext {

    private final Map<String, AfterSalesRefundService> serverName2Service = new HashMap<>();

    @Autowired
    public AfterSalesRefundContext(List<AfterSalesRefundService> afterSalesRefundServiceList) {
        if (CollectionUtils.isEmpty(afterSalesRefundServiceList)) {
            log.warn(" AfterSalesRefundContext noService ");
            return;
        }
        for (AfterSalesRefundService afterSalesRefundService : afterSalesRefundServiceList) {
            serverName2Service.put(afterSalesRefundService.getClass().getAnnotation(Service.class).value(), afterSalesRefundService);
        }
    }

    public AfterSalesRefundService getRefundService(RefundEnums refundEnums) {
        if (refundEnums == null) {
            log.warn("未指定RefundService");
            throw new PaymentAfterSaleException(GlobalCode.NOT_EXIST.setMsg("未指定Service"));
        }
        AfterSalesRefundService afterSalesRefundService = serverName2Service.get(refundEnums.getServiceName());
        if (afterSalesRefundService == null) {
            log.warn(" RefundService不存在 refundEnums {} ", refundEnums);
            throw new PaymentAfterSaleException(GlobalCode.NOT_EXIST.setMsg("RefundService不存在"));
        }
        return afterSalesRefundService;
    }


}

易错点:afterSalesRefundService.getClass().getAnnotation(Service.class).value()

获取自定义Bean名字的方法

消费策略的客户端【调用方】:

  

import com.payment.business.refund.enums.RefundEnums;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
@Slf4j
public class AfterSalesRefundClient {

    @Autowired
    private AfterSalesRefundContext afterSalesRefundContext;

    public void doRefund(RefundEnums refundEnums, String soNo, String afterSaleNo, BigDecimal amount) {
        log.info(" doRefund refundEnums {} soNo {} afterSaleNo {} amount {} ", refundEnums, soNo, afterSaleNo, amount);
        afterSalesRefundContext.getRefundService(refundEnums).refund(soNo, afterSaleNo, amount);
        log.info(" doRefund finish  soNo {} afterSaleNo {} ", soNo, afterSaleNo);
    }

}

在控制器消费这些策略:

图片

图片

完工。


最后,分享另外一个踩的坑:

需求:刷一批历史数据,需要循环遍历所有数据。

这个场景是不是想到根据ID循环遍历所有数据然后逐一处理?是的,是这样。
那使用递归,还是while(true)循环?
要使用while(true)循环,否则数据量一大,就:
java.lang.StackOverflowError:空

图片

图片

循环到第538次时StackOverflowError 图片

当然,使用while(true)时也有坑,踩了同学可以分享一下 图片 图片