一、背景
抽象类,包含抽象方法和实例方法,抽象方法待继承类去实例化,正是利用该特性,以满足不同支付渠道的差异化需求。
我们在做多渠道支付的时候,接收支付或退款的回调报文,然后去处理。这就意味着,我们往往会定义多组回调接口,把微信官方、支付宝官方、杭州银行等区分开来。
同时,他们之间又存在着许多共性,比如都需要验签,对比回调金额和本地金额是否一致,以及更新本地支付记录的状态等。
本文先会梳理,处理回调的一般逻辑,配合代码设计,尝试让你体会到在编程中,使用抽象类的魅力所在。
二、系统设计
我们针对不同的支付渠道,定义不同的回调接口,以区分报文的差异。
这里,以微信官方、支付宝官方和杭州银行三个渠道为示例。其实,我们实际对接的支付渠道比这多得多。
三、回调处理流程
四、抽象类的设计
- 源码截图见下:
五、支付渠道的实现
支付回调处理和退款回调处理,不同的支付渠道会有不同的处理逻辑。有些支付渠道返回的报文,可能需要先进行解密。
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;
- 退款回调处理
因为杭州银行的退款是同步的,所以这里没有对应实现。
- 验签
@Overrideprotected boolean doSign(Map<String, Object> resultMap, String account) {return HzBankSignUtil.dataVerifyByAccount(resultMap, CharsetUtil.UTF_8, account);}
- 其他实例方法
@Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "HzBank";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("txnOrderId");}/*** 获取第三方支付流水号** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("respTxnSsn");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return Integer.parseInt((String) resultMap.get("settleAmt"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {return DateUtils.getDate(String.valueOf(resultMap.get("respTxnTime")), DateUtils.DATE_FORMAT_1);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected 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>类型
-
验签
@Overrideprotected 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());}
- 其他实例方法
@Overrideprotected boolean enableSign() {return wxPayConfiguration.isSignEnabled();}@Overrideprotected String payChannelName() {return "WX";}@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("out_trade_no");}@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("transaction_id");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("outRefundNo");}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return (String) resultMap.get("refundId");}@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return Integer.parseInt((String) resultMap.get("total_fee"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return String.valueOf(resultMap.get("refundFee"));}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {return DateUtils.getDate(String.valueOf(resultMap.get("time_end")), DateUtils.DATE_FORMAT_1);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return (String) resultMap.get("refundStatus");}@Overrideprotected 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("支付回调处理失败"));}
- 退款回调处理
农行的退款是同步的,不是采用异步通知的方式。
- 验签
@Overrideprotected boolean doSign(Map<String, Object> trxResponseMap, String account) {String msg = (String) trxResponseMap.get(AbcBankConfig.MSG);return AbcBankSignUtil.verifySignByAccount(new XMLDocument(msg), account);}
- 其他实例方法
@Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "ABC";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("OrderNo");}/*** 获取第三方支付流水号.* <p>* <p>upay流水号</p>** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("iRspRef");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {String amount = (String) resultMap.get("Amount");Precondition.notEmpty(amount, "支付回调金额不能为空");return AmountUtils.changeY2F(amount);}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {//格式: YYYY/MM/DDString txDate = String.valueOf(resultMap.get("HostDate")).replaceAll("/", "-");//格式:HH:MM:SSString txTime = String.valueOf(resultMap.get("HostTime"));return DateUtil.parseDateTime(txDate + " " + txTime);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected 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-urlencodedString 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;}}
- 其他实例方法
@Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "ICBC";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("orderNo");}/*** 获取第三方支付流水号.* <p>* <p>upay流水号</p>** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("serialNo");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return AmountUtils.changeY2F((String) resultMap.get("totalAmount"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected 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);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected 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);}
七、总结
本文以支付和退款回调的实际业务为例,在使用抽象类的情况下,程序代码变得更加易懂,且大大提升了程序的拓展性。
每次接入新的支付渠道,对程序改动的影响和风险降低不少,比如你要接入连连支付,只需要新定义一个连连支付的实现类,并不会改动到其他原有支付的代码逻辑。
其实,我们在实现对账逻辑的时候,也会使用大量的设计模式。(有空梳理下对账逻辑的程序实现)
换言之,抽象类的使用,正是设计模式的一个基石。
我们使用了抽象类,却搞不清是使用了什么设计模式。这倒没什么,怕的是,你没想去减少代码的冗余。