一、背景
抽象类,包含抽象方法和实例方法,抽象方法待继承类去实例化,正是利用该特性,以满足不同支付渠道的差异化需求。
我们在做多渠道支付的时候,接收支付或退款的回调报文,然后去处理。这就意味着,我们往往会定义多组回调接口,把微信官方、支付宝官方、杭州银行等区分开来。
同时,他们之间又存在着许多共性,比如都需要验签,对比回调金额和本地金额是否一致,以及更新本地支付记录的状态等。
本文先会梳理,处理回调的一般逻辑,配合代码设计,尝试让你体会到在编程中,使用抽象类的魅力所在。
二、系统设计
我们针对不同的支付渠道,定义不同的回调接口,以区分报文的差异。
这里,以微信官方、支付宝官方和杭州银行三个渠道为示例。其实,我们实际对接的支付渠道比这多得多。
三、回调处理流程
四、抽象类的设计
- 源码截图见下:
五、支付渠道的实现
支付回调处理和退款回调处理,不同的支付渠道会有不同的处理逻辑。有些支付渠道返回的报文,可能需要先进行解密。
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);
}
七、总结
本文以支付和退款回调的实际业务为例,在使用抽象类的情况下,程序代码变得更加易懂,且大大提升了程序的拓展性。
每次接入新的支付渠道,对程序改动的影响和风险降低不少,比如你要接入连连支付,只需要新定义一个连连支付的实现类,并不会改动到其他原有支付的代码逻辑。
其实,我们在实现对账逻辑的时候,也会使用大量的设计模式。(有空梳理下对账逻辑的程序实现)
换言之,抽象类的使用,正是设计模式的一个基石。
我们使用了抽象类,却搞不清是使用了什么设计模式。这倒没什么,怕的是,你没想去减少代码的冗余。