引言
本文代码已提交至Github(版本号:
f4411d9b640e53982578382055b82a8d5d9036e0
),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop
阅读本文前,有兴趣的同学可以参考我之前写的聚合支付的文章:
- 《淘东电商项目(52) -聚合支付开篇》
- 《淘东电商项目(53) -银联支付案例源码分析》
- 《淘东电商项目(54) -银联支付案例(同步与异步)》
- 《淘东电商项目(55) -支付系统核心表设计》
- 《淘东电商项目(56) -支付系统分布式事务的解决方案》
- 《淘东电商项目(57) -聚合支付(支付令牌接口)》
- 《淘东电商项目(58) -聚合支付(基于设计模式自动跳转支付接口)》
- 《淘东电商项目(59) -聚合支付(集成银联支付)》
- 《淘东电商项目(60) -聚合支付(集成支付宝)》
目前「淘东电商项目」的聚合支付模块,已经完成了银联支付以及支付宝的集成。接下来本文主要讲解基于"模板设计模式"的方式来管理银联支付和支付宝支付结果回调接口。
本文目录结构:
1. 什么是模板设计模式?
关于模板设计模式,之前我有写过博客记录过,童鞋们有兴趣的可以参阅下:
抽象模板(Abstract Template)角色有如下责任:
- 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
- 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
具体模板(Concrete Template)角色又如下责任:
- 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
- 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
好了,下面开始讲解核心的代码。
2. 项目核心代码
①定义抽象操作,让子类去实现(AbstractPayCallbackTemplate
):
/** * description: 使用模版方法重构异步回调代码 * create by: YangLinWei * create time: 2020/5/18 9:34 上午 */ @Slf4j public abstract class AbstractPayCallbackTemplate { /** * 获取所有请求的参数,封装成Map集合 并且验证是否被篡改 * * @param req * @param resp * @return */ public abstract Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp); /** * description: 同步回调执行业务逻辑 * param: * * @return */ public abstract String syncService(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException; /** * 异步回调执行业务逻辑 * * @param verifySignature */ public abstract String asyncService(Map<String, String> verifySignature); public abstract String failResult(); public abstract String successResult(); /** * *1. 将报文数据存放到es <br> * 1. 验证报文参数<br> * 2. 将日志根据支付id存放到数据库中<br> * 3. 执行的异步回调业务逻辑<br> */ public String asyncCallBack(HttpServletRequest req, HttpServletResponse resp) { // 1. 验证报文参数 相同点 获取所有的请求参数封装成为map集合 并且进行参数验证 Map<String, String> verifySignature = verifySignature(req, resp); // 2.将日志根据支付id存放到数据库中 String paymentId = verifySignature.get("paymentId"); if (StringUtils.isEmpty(paymentId)) { return failResult(); } // 3.采用异步形式写入日志到数据库中 payLog(paymentId, verifySignature); String result = verifySignature.get(PayConstant.RESULT_NAME); // 4.201报文验证签名失败 if (result.equals(PayConstant.RESULT_PAYCODE_201)) { return failResult(); } // 5.执行的异步回调业务逻辑 return asyncService(verifySignature); } /** * 采用多线程技术或者MQ形式进行存放到数据库中 * * @param paymentId * @param verifySignature */ private void payLog(String paymentId, Map<String, String> verifySignature) { log.info(">>paymentId:{paymentId},verifySignature:{}", verifySignature); } }
②子类实现(UnionPayCallbackTemplate
):
/** * description: 银联支付回调模版实现 * create by: YangLinWei * create time: 2020/5/18 9:32 上午 */ @Component public class UnionPayCallbackTemplate extends AbstractPayCallbackTemplate { @Autowired private PaymentTransactionMapper paymentTransactionMapper; @Override public Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp) { LogUtil.writeLog("BackRcvResponse接收后台通知开始"); String encoding = req.getParameter(SDKConstants.param_encoding); // 获取银联通知服务器发送的后台通知参数 Map<String, String> reqParam = getAllRequestParam(req); LogUtil.printRequestLog(reqParam); // 重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过 if (!AcpService.validate(reqParam, encoding)) { LogUtil.writeLog("验证签名结果[失败]."); reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_201); } else { LogUtil.writeLog("验证签名结果[成功]."); // 【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态 String orderId = reqParam.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取 reqParam.put("paymentId", orderId); reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_200); } LogUtil.writeLog("BackRcvResponse接收后台通知结束"); return reqParam; } @Override public String syncService(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException { LogUtil.writeLog("FrontRcvResponse前台接收报文返回开始"); String encoding = req.getParameter(SDKConstants.param_encoding); LogUtil.writeLog("返回报文中encoding=[" + encoding + "]"); String pageResult = ""; Map<String, String> respParam = getAllRequestParam(req); // 打印请求报文 LogUtil.printRequestLog(respParam); Map<String, String> valideData = null; StringBuffer page = new StringBuffer(); if (null != respParam && !respParam.isEmpty()) { Iterator<Map.Entry<String, String>> it = respParam.entrySet() .iterator(); valideData = new HashMap<String, String>(respParam.size()); while (it.hasNext()) { Map.Entry<String, String> e = it.next(); String key = (String) e.getKey(); String value = (String) e.getValue(); value = new String(value.getBytes(encoding), encoding); page.append("<tr><td width=\"30%\" align=\"right\">" + key + "(" + key + ")</td><td>" + value + "</td></tr>"); valideData.put(key, value); } } if (!AcpService.validate(valideData, encoding)) { page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>失败</td></tr>"); LogUtil.writeLog("验证签名结果[失败]."); } else { page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>成功</td></tr>"); LogUtil.writeLog("验证签名结果[成功]."); System.out.println(valideData.get("orderId")); //其他字段也可用类似方式获取 String respCode = valideData.get("respCode"); //判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。 } return page.toString(); } // 异步回调中网络尝试延迟,导致异步回调重复执行 可能存在幂等性问题 // @Override public String asyncService(Map<String, String> verifySignature) { String orderId = verifySignature.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取 String respCode = verifySignature.get("respCode"); // 判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。 System.out.println("orderId:" + orderId + ",respCode:" + respCode); // 1.判断respCode是否为已经支付成功断respCode=00、A6后, if (!(respCode.equals("00") || respCode.equals("A6"))) { return failResult(); } // 根据日志 手动补偿 使用支付id调用第三方支付接口查询 PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId); if (paymentTransaction.getPaymentStatus().equals(PayConstant.PAY_STATUS_SUCCESS)) { // 网络重试中,之前已经支付过 return successResult(); } // 2.将状态改为已经支付成功 paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "", orderId); // 3.调用积分服务接口增加积分(处理幂等性问题) return successResult(); } @Override public String failResult() { return PayConstant.YINLIAN_RESULT_FAIL; } @Override public String successResult() { return PayConstant.YINLIAN_RESULT_SUCCESS; } /** * 获取请求参数中所有的信息 当商户上送frontUrl或backUrl地址中带有参数信息的时候, * 这种方式会将url地址中的参数读到map中,会导多出来这些信息从而致验签失败, * 这个时候可以自行修改过滤掉url中的参数或者使用getAllRequestParamStream方法。 * * @param request * @return */ public static Map<String, String> getAllRequestParam(final HttpServletRequest request) { Map<String, String> res = new HashMap<String, String>(); Enumeration<?> temp = request.getParameterNames(); if (null != temp) { while (temp.hasMoreElements()) { String en = (String) temp.nextElement(); String value = request.getParameter(en); res.put(en, value); // 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段> if (res.get(en) == null || "".equals(res.get(en))) { // System.out.println("======为空的字段名===="+en); res.remove(en); } } } return res; } /** * 获取请求参数中所有的信息。 * 非struts可以改用此方法获取,好处是可以过滤掉request.getParameter方法过滤不掉的url中的参数。 * struts可能对某些content-type会提前读取参数导致从inputstream读不到信息,所以可能用不了这个方法。 * 理论应该可以调整struts配置使不影响,但请自己去研究。 * 调用本方法之前不能调用req.getParameter("key");这种方法,否则会导致request取不到输入流。 * * @param request * @return */ public static Map<String, String> getAllRequestParamStream(final HttpServletRequest request) { Map<String, String> res = new HashMap<String, String>(); try { String notifyStr = new String(IOUtils.toByteArray(request.getInputStream()), UnionPayBase.encoding); LogUtil.writeLog("收到通知报文:" + notifyStr); String[] kvs = notifyStr.split("&"); for (String kv : kvs) { String[] tmp = kv.split("="); if (tmp.length >= 2) { String key = tmp[0]; String value = URLDecoder.decode(tmp[1], UnionPayBase.encoding); res.put(key, value); } } } catch (UnsupportedEncodingException e) { LogUtil.writeLog("getAllRequestParamStream.UnsupportedEncodingException error: " + e.getClass() + ":" + e.getMessage()); } catch (IOException e) { LogUtil.writeLog("getAllRequestParamStream.IOException error: " + e.getClass() + ":" + e.getMessage()); } return res; } /** * 回调机制 必须遵循规范 重试机制都是采用间隔新 错开的话 必须 */ }
③定义模板工厂(TemplateFactory
):
/** * description: 获取具体实现的模版工厂方案 * create by: YangLinWei * create time: 2020/5/18 9:30 上午 */ public class TemplateFactory { public static AbstractPayCallbackTemplate getPayCallbackTemplate(String beanId) { return (AbstractPayCallbackTemplate) SpringContextUtil.getBean(beanId); } }
④定义回调接口:
@RestController public class PayAsynCallbackService { private static final String UNIONPAYCALLBACK_TEMPLATE = "unionPayCallbackTemplate"; /** * 银联同步回调接口执行代码 * * @param req * @param resp * @return */ @RequestMapping("/unionPaySynCallback") public String unionPaySynCallback(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException { AbstractPayCallbackTemplate abstractPayCallbackTemplate = TemplateFactory .getPayCallbackTemplate(UNIONPAYCALLBACK_TEMPLATE); return abstractPayCallbackTemplate.syncService(req, resp); } /** * 银联异步回调接口执行代码 * * @param req * @param resp * @return */ @RequestMapping("/unionPayAsynCallback") public String unionPayAsynCallback(HttpServletRequest req, HttpServletResponse resp) { AbstractPayCallbackTemplate abstractPayCallbackTemplate = TemplateFactory .getPayCallbackTemplate(UNIONPAYCALLBACK_TEMPLATE); return abstractPayCallbackTemplate.asyncCallBack(req, resp); } }
3. 测试
首先启动项目(Eureka注册中心、单点服务中心、会员服务、支付服务、支付门户):
①配置银联支付的同步和异步回调地址,其SQL语句如下:
INSERT INTO `taodong-pay`.`payment_channel`(`ID`, `CHANNEL_NAME`, `CHANNEL_ID`, `MERCHANT_ID`, `SYNC_URL`, `ASYN_URL`, `PUBLIC_KEY`, `PRIVATE_KEY`, `CHANNEL_STATE`, `REVISION`, `CREATED_BY`, `CREATED_TIME`, `UPDATED_BY`, `UPDATED_TIME`, `CLASS_ADDRES`) VALUES (1, '银联支付', 'yinlian_pay', '777290058110048', 'http://localhost:8600/unionPaySynCallback', 'http://7z4nwp.natappfree.cc/unionPayAsynCallback', NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, 'com.ylw.service.pay.strategy.impl.UnionPayStrategy');
②浏览器模拟提交订单:请求地址http://localhost:8600/cratePayToken?payAmount=999&orderId=20200513141452&userId=27&productName=玉米香肠
数据库订单状态为未支付:
③模拟使用银联支付,浏览器请求:http://localhost:8079/pay?payToken=pay_f78c020ba2ec4c278e2e47a72e5c7c57
④.点击银联支付,按照提示一步一步支付至完成:
可以看到订单状态为已支付:
点击返回商户按钮,可以看到同步回调也是成功的:
支付宝回调与银联支付也是一样的原理,有有兴趣的同学可以下载代码相信看看。本文完!