从支付或退款之回调处理的设计,看一看抽象类的使用场景

一、背景

抽象类,包含抽象方法和实例方法,抽象方法待继承类去实例化,正是利用该特性,以满足不同支付渠道的差异化需求。
我们在做多渠道支付的时候,接收支付或退款的回调报文,然后去处理。这就意味着,我们往往会定义多组回调接口,把微信官方、支付宝官方、杭州银行等区分开来。
同时,他们之间又存在着许多共性,比如都需要验签,对比回调金额和本地金额是否一致,以及更新本地支付记录的状态等。

本文先会梳理,处理回调的一般逻辑,配合代码设计,尝试让你体会到在编程中,使用抽象类的魅力所在。

二、系统设计

在这里插入图片描述
我们针对不同的支付渠道,定义不同的回调接口,以区分报文的差异。

这里,以微信官方、支付宝官方和杭州银行三个渠道为示例。其实,我们实际对接的支付渠道比这多得多。

三、回调处理流程

在这里插入图片描述

四、抽象类的设计

在这里插入图片描述

  • 源码截图见下:

在这里插入图片描述

五、支付渠道的实现

支付回调处理和退款回调处理,不同的支付渠道会有不同的处理逻辑。有些支付渠道返回的报文,可能需要先进行解密。

0、验签

入参必须有account,然后我们会根据account取出所需要的密钥等信息,去对回调报文进行计算签名。 计算出来的签名和回调报文中的签名,如果不一致,则说明验签失败。

1、杭州银行

  • 支付回调处理
private static final String ERROR_CODE = "comm error";
private static final String SUCCESS_CODE = "got it";
    
String requestResultJson = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
        if (log.isInfoEnabled()) {
            log.info("杭州银行支付回调通知, 回调报文内容是:{}", requestResultJson);
        }
        if (StringUtils.isEmpty(requestResultJson)) {
            return ERROR_CODE;
        }
        Map<String, Object> resultMap = JSON.parseObject(requestResultJson, HashMap.class);

        if (HzBankSignUtil.SUCC_CODE.equalsIgnoreCase((String) resultMap.get(HzBankSignUtil.RESP_CODE))) {
            return lockPayNotify(resultMap);
        }
        return ERROR_CODE;
  • 退款回调处理

因为杭州银行的退款是同步的,所以这里没有对应实现。

  • 验签
    @Override
    protected boolean doSign(Map<String, Object> resultMap, String account) {
        return HzBankSignUtil.dataVerifyByAccount(resultMap, CharsetUtil.UTF_8, account);
    }
  • 其他实例方法
    @Override
    protected boolean enableSign() {
        return true;
    }

    @Override
    protected String payChannelName() {
        return "HzBank";
    }

/**
     * 获取平台支付流水号
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getChannelTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("txnOrderId");
    }

    /**
     * 获取第三方支付流水号
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getOutTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("respTxnSsn");
    }

    @Override
    protected String getRefundTradeNo(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected String getOutRefundNo(Map<String, Object> resultMap) {
        return null;
    }

    /**
     * 获取支付金额
     *
     * @param resultMap
     * @return
     */
    @Override
    protected Integer getPayAmt(Map<String, Object> resultMap) {
        return Integer.parseInt((String) resultMap.get("settleAmt"));
    }

    @Override
    protected String getRefundAmt(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getPayOkDate(Map<String, Object> resultMap) {
        return DateUtils.getDate(String.valueOf(resultMap.get("respTxnTime")), DateUtils.DATE_FORMAT_1);
    }

    @Override
    protected String getRefundStatus(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getRefundOkDate(Map<String, Object> resultMap) {
        return null;
    }

2、微信官方

它是一个xml格式的报文,我们使用到了一个三方jar包, com.github.binarywang 下的一个工具包weixin-java-pay。

  • 支付回调处理

    • 1.打印回调报文
    • 2.判断返回状态码
    • 3.统一转换为Map<String,Object>类型
  • 退款回调处理

    • 1.打印回调报文
    • 2.判断返回状态码
    • 3.根据返回报文中的mch_id查询出对应的商户
    • 4.根据上一步的商户密钥,将xml转换为bean对象
    • 5.如果退款成功,则锁定该退款记录,准备处理
    • 6.统一转换为Map<String,Object>类型
  • 验签

    @Override
    protected boolean doSign(Map<String, Object> paramMap, String account) {
        //根据account查询商户api密钥
        ChannelAccount channelAccount = channelAccountService.findByAccount(account);
        if (Objects.isNull(channelAccount)) {
            log.error("微信支付回调处理, 交易记录中的账户未配置账户的支付信息, [channelAccount={}]", account);
            return false;
        }

        return SignStrengthenUtils.checkSign(WxPayOrderNotifyResult.fromXML((String) paramMap.get("xmlString")), "MD5", channelAccount.getMchApiSecret());
    }
  • 其他实例方法
    @Override
    protected boolean enableSign() {
        return wxPayConfiguration.isSignEnabled();
    }

    @Override
    protected String payChannelName() {
        return "WX";
    }

    @Override
    protected String getChannelTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("out_trade_no");
    }

    @Override
    protected String getOutTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("transaction_id");
    }

    @Override
    protected String getRefundTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("outRefundNo");
    }

    @Override
    protected String getOutRefundNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("refundId");
    }

    @Override
    protected Integer getPayAmt(Map<String, Object> resultMap) {
        return Integer.parseInt((String) resultMap.get("total_fee"));
    }

    @Override
    protected String getRefundAmt(Map<String, Object> resultMap) {
        return String.valueOf(resultMap.get("refundFee"));
    }

    @Override
    protected Date getPayOkDate(Map<String, Object> resultMap) {
        return DateUtils.getDate(String.valueOf(resultMap.get("time_end")), DateUtils.DATE_FORMAT_1);
    }

    @Override
    protected String getRefundStatus(Map<String, Object> resultMap) {
        return (String) resultMap.get("refundStatus");
    }

    @Override
    protected Date getRefundOkDate(Map<String, Object> resultMap) {
        return DateUtils.getDate(String.valueOf(resultMap.get("successTime")), DateUtils.DATE_FORMAT_2);
    }

3、农行

  • 支付回调处理
private String doAbcPayRes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestMsg = request.getParameter(AbcBankConfig.MSG);
        if (log.isInfoEnabled()) {
            log.info("农业银行支付回调通知, 回调报文内容是, 解密前:{}", requestMsg);
        }
        if (StringUtils.isEmpty(requestMsg)) {
            return JSON.toJSONString(IcbcNotifyResponseDTO.error("回调报文不能为空"));
        }

        final String decodeMessage = Base64Code.Decode64(requestMsg);
        if (log.isInfoEnabled()) {
            log.info("农业银行支付回调通知, 回调报文内容是, 解密后:{}", decodeMessage);
        }

        Map<String, Object> resultMap = XmlUtil.xmlToMap(decodeMessage);

        Map<String, Object> messageMap = (Map<String, Object>) resultMap.get(AbcBankConfig.MESSAGE);

        Map<String, Object> trxResponseMap = (Map<String, Object>) messageMap.get(AbcBankConfig.TRX_RESPONSE);

        //将原文透传下去,供校验签名
        trxResponseMap.put(AbcBankConfig.MSG, decodeMessage);

        if (AbcBankConfig.RC_SUCCESS.equalsIgnoreCase((String) trxResponseMap.get(AbcBankConfig.RETURN_CODE))) {
            return lockPayNotify(trxResponseMap);
        }
        return JSON.toJSONString(IcbcNotifyResponseDTO.error("支付回调处理失败"));
    }
  • 退款回调处理

农行的退款是同步的,不是采用异步通知的方式。

  • 验签
@Override
    protected boolean doSign(Map<String, Object> trxResponseMap, String account) {
        String msg = (String) trxResponseMap.get(AbcBankConfig.MSG);
        return AbcBankSignUtil.verifySignByAccount(new XMLDocument(msg), account);
    }
  • 其他实例方法
    @Override
    protected boolean enableSign() {
        return true;
    }

    @Override
    protected String payChannelName() {
        return "ABC";
    }

    /**
     * 获取平台支付流水号
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getChannelTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("OrderNo");
    }

    /**
     * 获取第三方支付流水号.
     * <p>
     * <p>upay流水号</p>
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getOutTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("iRspRef");
    }

    @Override
    protected String getRefundTradeNo(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected String getOutRefundNo(Map<String, Object> resultMap) {
        return null;
    }

    /**
     * 获取支付金额
     *
     * @param resultMap
     * @return
     */
    @Override
    protected Integer getPayAmt(Map<String, Object> resultMap) {
        String amount = (String) resultMap.get("Amount");
        Precondition.notEmpty(amount, "支付回调金额不能为空");

        return AmountUtils.changeY2F(amount);
    }

    @Override
    protected String getRefundAmt(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getPayOkDate(Map<String, Object> resultMap) {
        //格式: YYYY/MM/DD
        String txDate = String.valueOf(resultMap.get("HostDate")).replaceAll("/", "-");
        //格式:HH:MM:SS
        String txTime = String.valueOf(resultMap.get("HostTime"));

        return DateUtil.parseDateTime(txDate + " " + txTime);
    }

    @Override
    protected String getRefundStatus(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getRefundOkDate(Map<String, Object> resultMap) {
        return null;
    }

4、工行

  • 支付回调处理

注意,工行的回调参数,是在query参数里,不是在requestBody。虽然是post接口,但是接口的Content-Type是application/x-www-form-urlencoded。这一点和其他支付方式的回调有较大差异。

    /**
     * 支付回调的参数
     */
    public final static String APIGW_RSPDATA = "apigw_rspdata";
    /**
     * 支付回调的签名
     */
    public final static String APIGW_SIGN = "apigw_sign";
    /**
     * 支付回调的证书ID
     */
    public final static String APIGW_CERTID = "apigw_certid";
    
private String doIcbcPayRes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Content-Type:application/x-www-form-urlencoded
        String sign = request.getParameter(IcbcConfig.APIGW_SIGN);
        String certId = request.getParameter(IcbcConfig.APIGW_CERTID);
        String rspData = request.getParameter(IcbcConfig.APIGW_RSPDATA);

        if (log.isInfoEnabled()) {
            log.info("工商银行支付回调通知, 回调报文内容是:[rspData={}, sign={}, certId={}]", rspData, sign, certId);
        }
        if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(certId) || StringUtils.isEmpty(rspData)) {
            return JSON.toJSONString(IcbcNotifyResponseDTO.error("回调报文不能为空"));
        }

        Map<String, Object> resultMap = JSON.parseObject(rspData, HashMap.class);

        resultMap.put(IcbcConfig.APIGW_SIGN, sign);
        resultMap.put(IcbcConfig.APIGW_CERTID, certId);
        resultMap.put(IcbcConfig.APIGW_RSPDATA, rspData);

        if (IcbcConfig.SUCC_CODE.equalsIgnoreCase((String) resultMap.get(IcbcConfig.RESULT_CODE))) {
            return lockPayNotify(resultMap);
        }
        return JSON.toJSONString(IcbcNotifyResponseDTO.error("支付回调处理失败"));
    }
  • 退款回调处理

  • 验签
    protected boolean doSign(Map<String, Object> resultMap, String account) {
        // 校验签名
        ApiClient apiClient = IcbcBankApiClientCache.getApiClientByAccount(account);
        try {
            return apiClient.doVerifyWithExit((String) resultMap.get(IcbcConfig.APIGW_RSPDATA),
                    (String) resultMap.get(IcbcConfig.APIGW_CERTID),
                    (String) resultMap.get(IcbcConfig.APIGW_SIGN),
                    "UTF-8");
        } catch (Exception e) {
            log.error("{}签名出现异常,[resultMap={}, certId={}]", payChannelName(),
                    JSON.toJSONString(resultMap), resultMap.get(IcbcConfig.APIGW_CERTID), e);
            return false;
        }
    }
  • 其他实例方法
@Override
    protected boolean enableSign() {
        return true;
    }

    @Override
    protected String payChannelName() {
        return "ICBC";
    }

    /**
     * 获取平台支付流水号
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getChannelTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("orderNo");
    }

    /**
     * 获取第三方支付流水号.
     * <p>
     * <p>upay流水号</p>
     *
     * @param resultMap
     * @return
     */
    @Override
    protected String getOutTradeNo(Map<String, Object> resultMap) {
        return (String) resultMap.get("serialNo");
    }

    @Override
    protected String getRefundTradeNo(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected String getOutRefundNo(Map<String, Object> resultMap) {
        return null;
    }

    /**
     * 获取支付金额
     *
     * @param resultMap
     * @return
     */
    @Override
    protected Integer getPayAmt(Map<String, Object> resultMap) {
        return AmountUtils.changeY2F((String) resultMap.get("totalAmount"));
    }

    @Override
    protected String getRefundAmt(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getPayOkDate(Map<String, Object> resultMap) {
        String txDate = String.valueOf(resultMap.get("txDate"));
        String txTime = String.valueOf(resultMap.get("txTime"));

        return DateUtils.getDate(txDate + txTime, DateUtils.DATE_FORMAT_1);
    }

    @Override
    protected String getRefundStatus(Map<String, Object> resultMap) {
        return null;
    }

    @Override
    protected Date getRefundOkDate(Map<String, Object> resultMap) {
        return null;
    }

六、处理支付/退款记录

上文列举了杭州银行、微信官方、农行、工行等四种支付渠道的实例,相信你后续接入其他支付渠道也是轻轻松松。

下面,我们将介绍公共的处理实现,因为退款逻辑和支付逻辑大同小异,所以我这里只说下支付的实现。

1、抽象回调的返回报文

    /**
     * 封装回调响应失败的报文
     *
     * @param msg
     * @return
     */
    protected abstract String assemblerResponseErrorMsg(String msg);

    /**
     * 封装回调响应成功的报文
     *
     * @param msg
     * @return
     */
    protected abstract String assemblerResponseSuccessMsg(String msg);

2、非空校验

public String lockPayNotify(Map<String, Object> paramMap) {
        // 平台订单号
        String channelTradeNo = getChannelTradeNo(paramMap);
        String outTradeNo = getOutTradeNo(paramMap);
        Integer payAmt = getPayAmt(paramMap);

        if (StringUtils.isEmpty(channelTradeNo) || StringUtils.isEmpty(outTradeNo) || payAmt <= 0) {
            log.error("{}支付回调通知失败, 平台支付流水号/第三方支付流水号/回调金额均不能为空![channelTradeNo={},outTradeNo={},payAmt={}]",
                    payChannelName(), channelTradeNo, outTradeNo, payAmt);
            return assemblerResponseErrorMsg("outTradeNo is null Or channelTradeNo is null Or settleAmt is null");
        }

        try {
            return handlePayResult(paramMap, channelTradeNo);
        } catch (Exception e) {
            log.error("{}支付回调通知, 处理出现异常,详细错误:", payChannelName(), e);
            return assemblerResponseErrorMsg(e.getMessage());
        }
    }

3、分布式锁

在这里插入图片描述

4、核心逻辑

try {
            String outTradeNo = getOutTradeNo(paramMap);
            Integer payAmt = getPayAmt(paramMap);

            // 支付成功时间
            Date notifyPayOkDate = getPayOkDate(paramMap);

            //查找支付订单和判断支付状态
            PayTrade payTrade = checkPayTradeIsExist(channelTradeNo);
            if (null == payTrade) {
                return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] not exist");
            }
            if (PayConstants.TRADESTATUS.SUCCESS == payTrade.getStatus()) {
                return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] has paid, please do not repeat invoke");
            }

            //校验签名
            if (!checkSign(paramMap, payTrade, outTradeNo)) {
                return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] sign error");
            }

            //校验金额
            if (!checkAmountEqual(payAmt, payTrade, outTradeNo)) {
                return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] amount not equal");
            }

            // 处理订单
            if (payTradeAppService.handlePayStatus(payTrade, channelTradeNo, outTradeNo, notifyPayOkDate)) {
                return assemblerResponseSuccessMsg("deal payNotify Success");
            }
            return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] update payTrade fail");
        } catch (Exception e) {
            if (log.isWarnEnabled()) {
                log.warn("处理支付回调出现异常", e);
            }
            throw new IllegalArgumentException("处理支付回调出现异常", e);
        }

七、总结

本文以支付和退款回调的实际业务为例,在使用抽象类的情况下,程序代码变得更加易懂,且大大提升了程序的拓展性。
每次接入新的支付渠道,对程序改动的影响和风险降低不少,比如你要接入连连支付,只需要新定义一个连连支付的实现类,并不会改动到其他原有支付的代码逻辑。

其实,我们在实现对账逻辑的时候,也会使用大量的设计模式。(有空梳理下对账逻辑的程序实现)
换言之,抽象类的使用,正是设计模式的一个基石。

我们使用了抽象类,却搞不清是使用了什么设计模式。这倒没什么,怕的是,你没想去减少代码的冗余。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天草二十六_简村人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值