019:基于模板方法模式重构异步回调

  • 1 基于策略+模板方法实现异步回调重构
  • 2 异步回调通知实现的原理
  • 3 支付宝官方demo异步回调代码的实现
  • 4 聚合支付项目如何采用模板+策略重构
  • 5 基于模板模式重构聚合支付平台
  • 6 异步回调重构验证签名代码
  • 7 基于策略模式id找到模板类
  • 8 异步回调更改订单状态的信息
  • 9 支付异步回调代码继续重构


1 基于策略+模板方法实现异步回调重构

今日课程任务

  1. 第三方支付异步回调实现的原理
  2. 基于模板+策略模式实现异步回调
  3. 基于ThreadLocal传递参数信息内容
  4. 采用多线程/MQ异步写入支付的日志信息参数
  5. 如何防止支付金额不一致的问题
  6. 如何防止支付回调幂等性问题
  7. 支付服务存在那些分布式事务难题

2 异步回调通知实现的原理

同步回调:以浏览器重定向方式跳转
异步回调:第三方支付以post请求格式通知给商户端 修改订单状态

支付宝官网文档:https://opendocs.alipay.com/open/270/105899

zabbix 模板 聚合图形_zabbix 模板 聚合图形


调用流程如下:

  1. 商户系统调用 alipay.trade.page.pay(统一收单下单并支付页面接口)向支付宝发起支付请求,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。
  2. 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
  3. 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
  4. 若由于网络等原因,导致商户系统没有收到异步通知,商户可自行调用alipay.trade.query(统一收单线下交易查询)接口查询交易以及支付信息(商户也可以直接调用该查询接口,不需要依赖异步通知)。

先触发异步回调,再触发同步回调。

注意:

  1. 由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
  2. 商户系统接收到异步通知以后,必须通过验签(验证通知中的sign参数)来确保支付通知是由支付宝发送的。详细验签规则请参见 异步通知验签。
  3. 接收到异步通知并验签通过后,请务必核对通知中的app_id、out_trade_no、total_amount等参数值是否与请求中的一致,并根据trade_status进行后续业务处理。
  4. 在支付宝端,partnerId与out_trade_no唯一对应一笔单据,商户端保证不同次支付out_trade_no不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。

3 支付宝官方demo异步回调代码的实现

异步回调中的商户订单号码与支付宝交易号码有哪些区别?
商户订单号码—支付的id
支付宝交易号码—支付宝流水号码 补偿

如果商户端没有及时返回success给支付宝的情况下,支付宝可能会触发重试策略,重试过程中可能会发生幂等性问题。
注意:同步回调以浏览器重定向形式返回,商户端只是解析报文验证签名成功,将支付结果告诉给用户,但是不能修改订单状态。

4 聚合支付项目如何采用模板+策略重构

如何对聚合支付回调进行设计重构?
策略+模板方法组合
策略模式:解决多重if判断的问题
模板方法模式:定义共同骨架(代码)统一都放入在父类,不同的代码让子类实现。

异步回调代码
1 记录日志
2 验证签名(不同)
3 修改订单状态为已支付
4 调用积分接口增加积分

5 基于模板模式重构聚合支付平台

模板方法骨架类

@Slf4j
public abstract class AbstractPayCallbackTemplate {

    @Autowired
    private PayThreadInfoHolder payThreadInfoHolder;

    public String asyncCallBack() {
        // 1. 验证签名
        boolean isSign = verifySignature();
        // 2. 记录日志 注意问题:一定要异步处理(为了快速响应给支付宝避免重试造成的幂等性问题)
        addPayLog();
        if (!isSign) {
            // 银联支付的时候 必须返回ok;支付宝必须返回success
            return resultFail();
        }
        // 更改订单状态
        return asyncService();
    }

    /**
     * 3.更改订单状态已经支付
     * 4.调用积分服务接口增加积分
     * @return
     */
    protected abstract String asyncService();

    /**
     * 交给子类实现
     *
     * @return
     */
    protected abstract String resultFail();


    /**
     * 返回成功
     *
     * @return
     */
    protected abstract String resultSuccess();

    /**
     * 定义成抽象方法让子类实现
     * 不管验证签名成功还是失败都会将参数放入到ThreadLocal中
     * @return
     */
    protected abstract boolean verifySignature();

    /**
     * 将该方法单独放入在一个类中调用,@Async不经过代理类不会生效
     */
    public void addPayLog() {
        // 从ThreadLocal中获取
        Map<String, String> requestParams = payThreadInfoHolder.getRequestParams();
        log.info(">>>记录支付的日志requestParams:<<<" + requestParams);
    }

}

使用ThreadLocal存放参数

@Component
public class PayThreadInfoHolder {
    private ThreadLocal<Map<String, String>> payThreadLocal = new ThreadLocal();

    public void setRequestParams(Map<String, String> requestParams) {
        payThreadLocal.set(requestParams);
    }

    public Map<String, String> getRequestParams() {
        return payThreadLocal.get();
    }
}

6 异步回调重构验证签名代码

验证签名

@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
    @Autowired
    private PayThreadInfoHolder payThreadInfoHolder;

    @Override
    protected String resultFail() {
        return "fail";
    }

    @Override
    protected String resultSuccess() {
        return "success";
    }

    @Override
    protected boolean verifySignature(PaymentChannelEntity pce) {
        // 根据当前线程获取Request
        HttpServletRequest request = ServletUtils.getCurrentRequest();
        HttpServletResponse response = ServletUtils.getCurrentResponse();
        //获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        ServletOutputStream out = null;
        try {
            out = response.getOutputStream();
            for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
                String name = (String) iter.next();
                String[] values = (String[]) requestParams.get(name);
                String valueStr = "";
                for (int i = 0; i < values.length; i++) {
                    valueStr = (i == values.length - 1) ? valueStr + values[i]
                            : valueStr + values[i] + ",";
                }
                //乱码解决,这段代码在出现乱码时使用
                valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
                params.put(name, valueStr);
                // 防止验证签名失败
                params.remove("channelId");
            }
            boolean signVerified = AlipaySignature.rsaCheckV1(
                    params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
            if (signVerified) {
                //验证成功
                //商户订单号
                String outTradeNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
                //支付宝交易号
                String tradeNo = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
                //交易状态
                String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
                if (tradeStatus.equals("TRADE_FINISHED")) {

                } else if (tradeStatus.equals("TRADE_SUCCESS")) {

                }
                out.println("success");
                return true;
            } else {
                //验证失败
                return false;
            }
        } catch (Exception e) {
            log.error("verifySignature()>> errorMsg:" + e.getMessage());
            return false;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {

            }
            payThreadInfoHolder.setRequestParams(params);
        }
    }

    @Override
    protected String asyncService() {
        return null;
    }
}

7 基于策略模式id找到模板类

数据库payment_channel表新增字段callback_bean_id,aliPayCallbackTemplate对应支付回调模板子类AliPayCallbackTemplate。

异步回调方法

public interface PayCallbackService {

    /**
     * 异步回调
     *
     * @return
     */
    @RequestMapping("/asynCallback")
    Object asynCallback(String channelId);
}
@RestController
public class PayCallbackServiceImpl extends BaseApiService implements PayCallbackService {
    @Autowired
    private PaymentChannelMapper paymentChannelMapper;

    @Override
    public Object asynCallback(String channelId) {
        if (StringUtils.isEmpty(channelId)) {
            return setResultError("channelId不能为空");
        }
        // 1.根据渠道id查询渠道信息
        PaymentChannelEntity pce = paymentChannelMapper.selectBychannelId(channelId);
        if (pce == null) {
            return setResultError("该渠道已关闭或不存在,请联系管理员");
        }
        //2.获取模板的baenid
        String callbackBeanId = pce.getCallbackBeanId();
        if (StringUtils.isEmpty(callbackBeanId)) {
            return setResultError("没有配置callbackBeanId");
        }
        //3.直接从Spring容器上下文获取
        AbstractPayCallbackTemplate payCallbackTemplate =
                SpringContextUtils.getBean(callbackBeanId, AbstractPayCallbackTemplate.class);
        String result = payCallbackTemplate.asyncCallBack(pce);
        return result;
    }
}

8 异步回调更改订单状态的信息

阿里支付回调模板实现类

@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
    @Autowired
    private PayThreadInfoHolder payThreadInfoHolder;
    @Autowired
    private PaymentTransactionMapper paymentTransactionMapper;

    @Override
    protected String resultFail() {
        return "fail";
    }

    @Override
    protected String resultSuccess() {
        return "success";
    }

    @Override
    protected boolean verifySignature(PaymentChannelEntity pce) {
        // 根据当前线程获取Request
        HttpServletRequest request = ServletUtils.getCurrentRequest();
        HttpServletResponse response = ServletUtils.getCurrentResponse();
        //获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        ServletOutputStream out = null;
        try {
            out = response.getOutputStream();
            for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
                String name = (String) iter.next();
                String[] values = (String[]) requestParams.get(name);
                String valueStr = "";
                for (int i = 0; i < values.length; i++) {
                    valueStr = (i == values.length - 1) ? valueStr + values[i]
                            : valueStr + values[i] + ",";
                }
                //乱码解决,这段代码在出现乱码时使用
                valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
                params.put(name, valueStr);
                // 防止验证签名失败
                params.remove("channelId");
            }
            boolean signVerified = AlipaySignature.rsaCheckV1(
                    params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
            if (signVerified) {
                out.println("success");
                return true;
            } else {
                //验证失败
                return false;
            }
        } catch (Exception e) {
            log.error("verifySignature()>> errorMsg:" + e.getMessage());
            return false;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {

            }
            payThreadInfoHolder.setRequestParams(params);
        }
    }

    @Override
    protected String asyncService() {
        // 1. 根据支付订单号码查询数据库 ,是否已经支付过
        Map<String, String> requestParams = currentRequestParams();
        // 支付订单号码
        String outTradeNo = requestParams.get("out_trade_no");
        PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(outTradeNo);
        // 以下代码可以重构进入父类里面
        if ("1".equals(pte.getPaymentStatus())) {
            // 返回成功告诉给支付不要再继续重试
            return resultSuccess();
        }
        // 2.判断支付的金额 是否一致
        Long payAmount = pte.getPayAmount();
        String totalAmount = requestParams.get("total_amount");
        Long aliPayAmount = yuanToFen(totalAmount);
        if (!payAmount.equals(aliPayAmount)) {
            // 返回success,后台将该订单状态修改为异常订单状态,然后分析异常原因用定时任务处理
            return resultSuccess();
        }
        // 3. 在修改为已经支付
        int result = paymentTransactionMapper.updatePaymentStatus("1", outTradeNo, "ali_pay");
        if (result < 0) {
            // 返回失败告诉支付宝重试
            return resultFail();
        }
        // 调用积分服务接口增加积分 存在分布式事务的问题
        return resultSuccess();
    }

    private static Long yuanToFen(String amount) {
        NumberFormat format = NumberFormat.getInstance();
        try {
            Number number = format.parse(amount);
            double temp = number.doubleValue() * 100.0;
            format.setGroupingUsed(false);
            // 设置返回数的小数部分所允许的最大位数
            format.setMaximumFractionDigits(0);
            amount = format.format(temp);
            return Long.parseLong(amount);
        } catch (ParseException e) {
            return null;
        }

    }
}

测试效果:

zabbix 模板 聚合图形_ide_02

9 支付异步回调代码继续重构

重构asyncService()方法 -修改订单状态是通用的逻辑,可以放在父类中实现

@Slf4j
public abstract class AbstractPayCallbackTemplate {

    @Autowired
    private PayThreadInfoHolder payThreadInfoHolder;
    @Autowired
    private PaymentTransactionMapper paymentTransactionMapper;

    public String asyncCallBack(PaymentChannelEntity pce) {
        // 1. 验证签名
        boolean isSign = verifySignature(pce);
        // 2. 记录日志 注意问题:一定要异步处理(为了快速响应给支付宝避免重试造成的幂等性问题)
        addPayLog();
        if (!isSign) {
            // 银联支付的时候 必须返回ok;支付宝必须返回success
            return resultFail();
        }
        // 更改订单状态 获取订单号码,支付金额
        String orderNo = getPayOrderNo();
        Long totalAmount = getTotalAmount();
        return asyncService(orderNo, totalAmount);
    }

    /**
     * 3.更改订单状态已经支付
     * 4.调用积分服务接口增加积分
     *
     * @return
     */
    private String asyncService(String outTradeNo, Long totalAmount) {
        // 支付订单号码
        PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(outTradeNo);
        // 以下代码可以重构进入父类里面
        if (PayConstant.PAY_PAID_STATUS.equals(pte.getPaymentStatus())) {
            // 返回成功告诉给支付不要再继续重试
            return resultSuccess();
        }
        // 2.判断支付的金额 是否一致
        Long payAmount = pte.getPayAmount();
        if (!payAmount.equals(totalAmount)) {
            // 返回success,后台将该订单状态修改为异常订单状态,然后分析异常原因用定时任务处理
            return resultSuccess();
        }
        // 3. 在修改为已经支付
        int result = paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_PAID_STATUS + "", outTradeNo, "ali_pay");
        if (result < PayConstant.DB_FAIL) {
            // 返回失败告诉支付宝重试
            return resultFail();
        }
        // 调用积分服务接口增加积分 存在分布式事务的问题
        return resultSuccess();
    }

    /**
     * 获取真实的支付订单号码
     *
     * @return
     */
    protected abstract String getPayOrderNo();

    /**
     * 获取真实的支付金额
     *
     * @return
     */
    protected abstract Long getTotalAmount();

    /**
     * 交给子类实现
     *
     * @return
     */
    protected abstract String resultFail();


    /**
     * 返回成功
     *
     * @return
     */
    protected abstract String resultSuccess();

    /**
     * 定义成抽象方法让子类实现
     * 不管验证签名成功还是失败都会将参数放入到ThreadLocal中
     *
     * @return
     */
    protected abstract boolean verifySignature(PaymentChannelEntity pce);

    /**
     * 将该方法单独放入在一个类中调用,@Async不经过代理类不会生效
     */
    public void addPayLog() {
        // 从ThreadLocal中获取
        Map<String, String> requestParams = currentRequestParams();
        log.info(">>>记录支付的日志requestParams:<<<" + requestParams);
    }

    protected Map<String, String> currentRequestParams() {
        return payThreadInfoHolder.getRequestParams();
    }

}
@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
    @Autowired
    private PayThreadInfoHolder payThreadInfoHolder;


    @Override
    protected String getPayOrderNo() {
        // 1. 根据支付订单号码查询数据库 ,是否已经支付过
        Map<String, String> requestParams = currentRequestParams();
        // 支付订单号码
        String outTradeNo = requestParams.get("out_trade_no");
        return outTradeNo;
    }

    @Override
    protected Long getTotalAmount() {
        // 1. 根据支付订单号码查询数据库 ,是否已经支付过
        Map<String, String> requestParams = currentRequestParams();
        String totalAmount = requestParams.get("total_amount");
        Long aliPayAmount = yuanToFen(totalAmount);
        return aliPayAmount;
    }

    @Override
    protected String resultFail() {
        return "fail";
    }

    @Override
    protected String resultSuccess() {
        return "success";
    }

    @Override
    protected boolean verifySignature(PaymentChannelEntity pce) {
        // 根据当前线程获取Request
        HttpServletRequest request = ServletUtils.getCurrentRequest();
        HttpServletResponse response = ServletUtils.getCurrentResponse();
        //获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        ServletOutputStream out = null;
        try {
            out = response.getOutputStream();
            for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
                String name = (String) iter.next();
                String[] values = (String[]) requestParams.get(name);
                String valueStr = "";
                for (int i = 0; i < values.length; i++) {
                    valueStr = (i == values.length - 1) ? valueStr + values[i]
                            : valueStr + values[i] + ",";
                }
                //乱码解决,这段代码在出现乱码时使用
                valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
                params.put(name, valueStr);
                // 防止验证签名失败
                params.remove("channelId");
            }
            boolean signVerified = AlipaySignature.rsaCheckV1(
                    params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
            if (signVerified) {
                out.println("success");
                return true;
            } else {
                //验证失败
                return false;
            }
        } catch (Exception e) {
            log.error("verifySignature()>> errorMsg:" + e.getMessage());
            return false;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {

            }
            payThreadInfoHolder.setRequestParams(params);
        }
    }

    private static Long yuanToFen(String amount) {
        NumberFormat format = NumberFormat.getInstance();
        try {
            Number number = format.parse(amount);
            double temp = number.doubleValue() * 100.0;
            format.setGroupingUsed(false);
            // 设置返回数的小数部分所允许的最大位数
            format.setMaximumFractionDigits(0);
            amount = format.format(temp);
            return Long.parseLong(amount);
        } catch (ParseException e) {
            return null;
        }

    }
}