如果说研发团队是一个可以随意替换的工具,那么所有代码都会来自需求。
先交待下需求: 在之前渠道1退款的基础上,再增加一个退款渠道。
需求分析:
退款是一个高可靠性操作。收到退款请求后,需要进行如下 操作:
- 校验数据合法性
- 校验业务合法性
- 执行退款
其中,校验业务合法性是个性化逻辑,不同的渠道校验的办法不同。
做过支付的同学都知道,支付操作会分为发起支付、支付中、支付完成 这三个状态。
退款也一样,执行退款后,并不知道结果。具体的结果需要由支付来回调来更新最终的退款状态。
技术分析:
操作流程固定,只是不同场景时,“校验业务合法性”的具体逻辑不同。
看到这的粉丝朋友,是不是也想到了相同部分提取到抽象父类,不同的部分各自实现。
是的。是这个场景,虽然"组合大于继承",就流程执行的连贯性、可读性、可理解性,还是使用模板 抽象类更合适一些。
上类图
退款渠道1:
退款渠道2:
上代码
要干的事:退款
代码语言:javascript复制import java.math.BigDecimal;
public interface AfterSalesRefundService {
void refund(String soNo, String afterSaleNo, BigDecimal amount);
}
退款的动作拆解:
代码语言:javascript复制
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中的个性化校验逻辑:
代码语言:javascript复制
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中的个性化校验逻辑:
代码语言:javascript复制
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、代替魔法字符串的常量:
代码语言:javascript复制public class RefundServiceNameConstant {
public static final String VPI_SERVICE = "VPIRefund";
public static final String SELF_RUN_SERVICE = "SelfRunRefund";
}
是的,上面两个策略的自定义Bean名就是这两个常量。
2、作为调用参数的标识:枚举类
代码语言:javascript复制
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;
}
}
上面这两个类是铺垫,正戏“策略上下文”到了:
代码语言:javascript复制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名字的方法
消费策略的客户端【调用方】:
代码语言:javascript复制
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: null
循环到第538次时StackOverflowError
当然,使用while(true)时也有坑,踩了同学可以分享一下