赞
踩
前提
在申请测试的时候一定要点开测试
在线支付平台分为:支付宝、平安支付、微信支付、小米支付、银联支付等。
聚合支付平台: ping++、mustPay 等。
聚合支付与第三方支付的区别
1.支付回调接口中,产生延迟通知?如何解决支付状态不一致问题
2.重试支付回调接口时,如何保证回调接口通知幂等性问题
3.支付金额与商品金额如果不一致时,如何处理
4.第三方支付平台如何与对接系统保证分布式事务问题
5.支付服务如何与其他系统保证分布式事务问题
同步回调:
异步回调:
https://open.unionpay.com/tjweb/acproduct/dictionary
https://open.unionpay.com/tjweb/acproduct/list?apiservId=448
在线html格式化 http://tool.oschina.net/codeformat/html
实现步骤:
1.下载对应的银联demo
2.创建D:\certs目录
3.下载对应测试证书,放入D:\certs
测试账号信息:
卡号:6216261000000000018
姓名:全渠道
身份证号码: 341126197709218366
NatApp
natapp -authtoken=2fb5f28e255be404
查看附件SQL
构建支付系统
meite-shop-service-pay
public interface PayMentTransacService { /** * 创建支付令牌 * * @return */ @GetMapping("/cratePayToken") public BaseResponse<JSONObject> cratePayToken(@Validated PayCratePayTokenDto payCratePayTokenDto); } @RestController public class PayMentTransacTokenServiceImpl extends BaseApiService<JSONObject> implements PayMentTransacTokenService { @Autowired private PaymentTransactionMapper paymentTransactionMapper; @Autowired private GenerateToken generateToken; @Override public BaseResponse<JSONObject> cratePayToken(PayCratePayTokenDto payCratePayTokenDto) { String orderId = payCratePayTokenDto.getOrderId(); if (StringUtils.isEmpty(orderId)) { return setResultError("订单号码不能为空!"); } Long payAmount = payCratePayTokenDto.getPayAmount(); if (payAmount == null) { return setResultError("金额不能为空!"); } Long userId = payCratePayTokenDto.getUserId(); if (userId == null) { return setResultError("userId不能为空!"); } // 2.将输入插入数据库中 PaymentTransactionEntity paymentTransactionEntity = new PaymentTransactionEntity(); paymentTransactionEntity.setOrderId(orderId); paymentTransactionEntity.setPayAmount(payAmount); paymentTransactionEntity.setUserId(userId); // 使用雪花算法 生成全局id paymentTransactionEntity.setPaymentId(SnowflakeIdUtils.nextId() + ""); int result = paymentTransactionMapper.insertPaymentTransaction(paymentTransactionEntity); if (!toDaoResult(result)) { return setResultError("系统错误!"); } Long payId = paymentTransactionEntity.getId(); if (payId == null) { return setResultError("系统错误!"); } // 3.生成对应支付令牌 String keyPrefix = "pay_"; String token = generateToken.createToken(keyPrefix, payId + ""); JSONObject dataResult = new JSONObject(); dataResult.put("token", token); return setResultSuccess(dataResult); } }
策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。策略模式通常把一个系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类。
这个模式涉及到三个角色:
● 环境(Context)角色:持有一个Strategy的引用。
● 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
● 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
meite-shop-pay-plugin
PayStrategy
public interface PayStrategy {
/**
*
* @param paymentChannel
* 渠道<br>
* @param payToken
* 支付令牌<br>
* @return
*/
String toPayHtml(PaymentChannelEntity paymentChannel, PayMentTransacDTO payMentTransacDTO);
}
UnionPayStrategy
@Slf4j @Component public class UnionPayStrategy implements PayStrategy { @Override public String toPayHtml(PaymentChannelEntity paymentChannel, PayMentTransacDTO payMentTransacDTO) { log.info(">>>>>>>>银联支付组装参数开始<<<<<<<<<<<<"); Map<String, String> requestData = new HashMap<String, String>(); /*** 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 ***/ requestData.put("version", UnionPayBase.version); // 版本号,全渠道默认值 requestData.put("encoding", UnionPayBase.encoding); // 字符集编码,可以使用UTF-8,GBK两种方式 requestData.put("signMethod", SDKConfig.getConfig().getSignMethod()); // 签名方法 requestData.put("txnType", "01"); // 交易类型 ,01:消费 requestData.put("txnSubType", "01"); // 交易子类型, 01:自助消费 requestData.put("bizType", "000201"); // 业务类型,B2C网关支付,手机wap支付 requestData.put("channelType", "07"); // 渠道类型,这个字段区分B2C网关支付和手机wap支付;07:PC,平板 // 08:手机 /*** 商户接入参数 ***/ String merchantId = paymentChannel.getMerchantId(); requestData.put("merId", merchantId); // 商户号码,请改成自己申请的正式商户号或者open上注册得来的777测试商户号 requestData.put("accessType", "0"); // 接入类型,0:直连商户 String paymentId = payMentTransacDTO.getPaymentId(); requestData.put("orderId", paymentId); // 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则 requestData.put("txnTime", format(payMentTransacDTO.getCreatedTime())); // 订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效 requestData.put("currencyCode", "156"); // 交易币种(境内商户一般是156 人民币) Long payAmount = payMentTransacDTO.getPayAmount(); requestData.put("txnAmt", payAmount + ""); // 交易金额,单位分,不要带小数点 // requestData.put("reqReserved", "透传字段"); // //请求方保留域,如需使用请启用即可;透传字段(可以实现商户自定义参数的追踪)本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节。出现&={}[]符号时可能导致查询接口应答报文解析失败,建议尽量只传字母数字并使用|分割,或者可以最外层做一次base64编码(base64编码之后出现的等号不会导致解析失败可以不用管)。 requestData.put("riskRateInfo", "{commodityName=测试商品名称}"); // 前台通知地址 (需设置为外网能访问 http https均可),支付成功后的页面 点击“返回商户”按钮的时候将异步通知报文post到该地址 // 如果想要实现过几秒中自动跳转回商户页面权限,需联系银联业务申请开通自动返回商户权限 // 异步通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知\ String syncUrl = paymentChannel.getSyncUrl(); requestData.put("frontUrl", syncUrl); // 后台通知地址(需设置为【外网】能访问 http // https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,失败的交易银联不会发送后台通知 // 后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知 // 注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 3.收单后台通知后需要10秒内返回http200或302状态码 // 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200,那么银联会间隔一段时间再次发送。总共发送5次,每次的间隔时间为0,1,2,4分钟。 // 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d // 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败 String asynUrl = paymentChannel.getAsynUrl(); requestData.put("backUrl", asynUrl); // 订单超时时间。 // 超过此时间后,除网银交易外,其他交易银联系统会拒绝受理,提示超时。 // 跳转银行网银交易如果超时后交易成功,会自动退款,大约5个工作日金额返还到持卡人账户。 // 此时间建议取支付时的北京时间加15分钟。 // 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。 requestData.put("payTimeout", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date().getTime() + 15 * 60 * 1000)); // // // 报文中特殊用法请查看 PCwap网关跳转支付特殊用法.txt // // /** 请求参数设置完毕,以下对请求参数进行签名并生成html表单,将表单写入浏览器跳转打开银联页面 **/ Map<String, String> submitFromData = AcpService.sign(requestData, UnionPayBase.encoding); // 报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。 String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl(); // 获取请求银联的前台地址:对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrl String html = AcpService.createAutoFormHtml(requestFrontUrl, submitFromData, UnionPayBase.encoding); // 生成自动跳转的Html表单 LogUtil.writeLog("打印请求HTML,此为请求报文,为联调排查问题的依据:" + html); // 将生成的html写到浏览器中完成自动跳转打开银联支付页面;这里调用signData之后,将html写到浏览器跳转到银联页面之前均不能对html中的表单项的名称和值进行修改,如果修改会导致验签不通过 return html; } private String format(Date timeDate) { String date = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(timeDate); return date; } }
PayContextService
public interface PayContextService {
@GetMapping("/toPayHtml")
public BaseResponse<JSONObject> toPayHtml(String channelId, String payToken);
}
PayContextServiceImpl
@RestController public class PayContextServiceImpl extends BaseApiService<JSONObject> implements PayContextService { @Autowired private PaymentChannelMapper paymentChannelMapper; @Autowired private PayMentTransacInfoService payMentTransacInfoService; public BaseResponse<JSONObject> toPayHtml(String channelId, String payToken) { // 1.使用渠道id查询渠道信息 PaymentChannelEntity pymentChannel = paymentChannelMapper.selectBychannelId(channelId); if (pymentChannel == null) { return setResultError("没有查询到该渠道信息"); } // 2.使用payToken查询待支付信息 BaseResponse<PayMentTransacDTO> tokenByPayMentTransac = payMentTransacInfoService .tokenByPayMentTransac(payToken); if (!isSuccess(tokenByPayMentTransac)) { return setResultSuccess(tokenByPayMentTransac.getMsg()); } // 3.使用Java反射机制初始化子类 String classAddres = pymentChannel.getClassAddres(); PayStrategy payStrategy = StrategyFactory.getPayStrategy(classAddres); if (payStrategy == null) { return setResultError("支付系统网关错误!"); } PayMentTransacDTO payMentTransacDTO = tokenByPayMentTransac.getData(); // 4.直接执行子类实现方法 String payHtml = payStrategy.toPayHtml(pymentChannel, payMentTransacDTO); JSONObject data = new JSONObject(); data.put("payHtml", payHtml); return setResultSuccess(data); } }
StrategyFactory
public class StrategyFactory { private static Map<String, PayStrategy> strategyBean = new ConcurrentHashMap<String, PayStrategy>(); /** * 使用Java反射机制初始化类 * * @param classAddres * @return */ public static PayStrategy getPayStrategy(String classAddres) { try { PayStrategy beanPayStrategy = strategyBean.get(classAddres); if (beanPayStrategy != null) { return beanPayStrategy; } Class<?> forName = Class.forName(classAddres); PayStrategy payStrategy = (PayStrategy) forName.newInstance(); strategyBean.put(classAddres, payStrategy); return payStrategy; } catch (Exception e) { return null; } } }
https://docs.open.alipay.com/270/106291/ 支付宝电商文档
https://openhome.alipay.com/platform/appDaily.htm 支付宝沙箱环境
抽象模板(Abstract Template)角色有如下责任:
定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
具体模板(Concrete Template)角色又如下责任:
实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
public abstract class AbstractPayCallbackTemplate { /** * 异步回调验证签名参数 * * @param req * @param resp * @return */ public abstract Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp); /** * 异步回调执行业务逻辑 * * @return */ public abstract String asyncService(Map<String, String> verifySignature); /** * 失败结果 * * @return */ public abstract String failResult(); /** * 失败结果 * * @return */ public abstract String successResult(); public String asyncCallBack(HttpServletRequest req, HttpServletResponse resp) { // 1.获取第三方支付回调参数信息 Map<String, String> verifySignature = verifySignature(req, resp); if (verifySignature.get(PayConstant.RESULT_NAME).equals(PayConstant.RESULT_PAYCODE_201)) { return failResult(); } // 2.将支付参数信息根据支付id插入到数据库中 payLog(verifySignature); // 3.处理异步回调相关日志信息 return asyncService(verifySignature); } /** * 将log写入到日志中 * * @param paymentParameter */ private void payLog(Map<String, String> paymentParameter) { String paymentId = paymentParameter.get("paymentId"); log.info(">>>>>paymentId:{},paymentParameter:{}", paymentId, paymentParameter); } }
@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("验证签名结果[成功]."); reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_200); } reqParam.put("paymentId", reqParam.get("orderId")); return reqParam; } @Override public String asyncService(Map<String, String> verifySignature) { String orderId = verifySignature.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取 if (StringUtils.isEmpty(orderId)) { return failResult(); } // 1.根据orderId查询该支付信息 PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId); if (paymentTransaction == null) { return failResult(); } // 2.判断之前是否已经支付过,已经支付过则返回ok Integer paymentStatus = paymentTransaction.getPaymentStatus(); if (paymentStatus.equals(PayConstant.PAY_STATUS_SUCCESS)) { return successResult(); } String respCode = verifySignature.get("respCode"); // 3.判断银联状态码不是00或者A6状态则状态码修改为已支付失败 if (!(respCode.equals("00") || respCode.equals("A6"))) { return failResult(); } // 4.如果银联状态码返回是00和A6的话 paymentTransactionMapper.updatePaymentStatus(1, orderId); return successResult(); } @Override public String failResult() { return PayConstant.YINLIAN_RESULT_FAIL; } @Override public String successResult() { // TODO Auto-generated method stub return PayConstant.PAY_STATUS_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; } }
public class TemplateFactory {
/**
* 获取具体模版方法类
*
* @param payCallbackName
* bean实例名称
* @return
*/
public static AbstractPayCallbackTemplate getPayCallbackTemplate(String payCallbackName) {
return (AbstractPayCallbackTemplate) SpringContextUtil.getBean(payCallbackName);
}
}
@RestController public class PayAsynCallbackService { /** * 获取银联回调模版 */ private static final String UNIONPAYCALLBACKT_EMPLATENAME = "unionPayCallbackTemplate"; /** * 银联异步回调通知 * * @return */ @RequestMapping("/unionPayAsynCallback") public String unionPayCallback(HttpServletRequest req, HttpServletResponse resp) { AbstractPayCallbackTemplate payCallbackTemplate = TemplateFactory .getPayCallbackTemplate(UNIONPAYCALLBACKT_EMPLATENAME); return payCallbackTemplate.asyncCallBack(req, resp); } }
@Component public class SpringContextUtil implements ApplicationContextAware { /** * 上下文对象实例 */ private static ApplicationContext applicationContext; @Autowired public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 获取applicationContext * * @return */ public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 通过name获取 Bean. * * @param name * @return */ public static Object getBean(String name) { return getApplicationContext().getBean(name); } /** * 通过class获取Bean. * * @param clazz * @param <T> * @return */ public static <T> T getBean(Class<T> clazz) { return getApplicationContext().getBean(clazz); } /** * 通过name,以及Clazz返回指定的Bean * * @param name * @param clazz * @param <T> * @return */ public static <T> T getBean(String name, Class<T> clazz) { return getApplicationContext().getBean(name, clazz); } }
聚合支付平台优缺点:
优点:对接接口简单、后期维护成本低。
缺点:跑路或者资金链锻炼,安全系数低。
适合于中小型公司 月流水几万左右。
传统在线支付平台
回调设计模式:模版方法 外观设计模式
策略+ 模版+单例
对接的第三方支付接口整体流程都是相同的。
思考:
银联支付接口对接实现步骤:
1.银联开放平台申请测试账号
https://open.unionpay.com/tjweb/acproduct/list?apiservId=448
2.下载对应的商户的证书 放入到d:/certs/
3. 启动银联支付demo 的时候修改acp_test_sign.p12 证书名称
银联支付请求分析:/form_6_2_FrontConsume 跳转银联支付接口。
银联支付以分为单位 支付宝接口元 为单位
表的 设计中如何存放金额呢? 用什么类型合适呢 long 存放分单位
1.24元等于多少分 24/100 0.24
前台通知:
后台通知:
验证签名作用:防止数据被篡改
银联支付提交原理:
1.封装支付请求参数
2.将参数验证签名,以html 表单形式提交给银联支付接口。
以html表单形式提交有什么好处、方便提交参数。
思考问题
在第三方支付平台(银联、支付宝、微信支付)支付结果是如何通知给商户端?
支付系统分布式事务问题存在原因:
用户在银联系统中,支付完成之后,如何把结果通知给商户端。
跨系统中如何解决分布式事务
采用最终一致性问题 双方可以短暂一致性,但是最终一定实现一致性问题,柔性事务
采用通知补偿性
同步回调(前台通知):第三方支付系统以浏览器重定向形式将支付结果给商户端
作用:提示给用户在银联支付系统中,已经支付成功。
异步回调(后台通知):第三方支付系统使用类似于HttpClient技术调用商户接口进行通知。
作用:将银联支付安全的形式发送给商户端,进行修改对应的订单状态。
注意:异步通知修改订单状态
本地开发中使用内网穿透技术 natapp
http://localhost:8080/ACPSample_B2C/frontRcvResponse
验证签名原理:
MD5 加密加盐 不可逆
商户端 银联支付接口
10 MD5 ==aaabb 10 MD5=aaabb
验证签名防止数据被篡改
加密是防止抓包获取明文的数据 可逆。
互联网安全架构平台设计,具体底层如何实现。
适配器 模版方法 策略。
支付流程:
1.订单服务调用支付服务 生成支付令牌(userID、订单号码、支付金额)
2.订单服务获取支付令牌,在使用订单web传递Token形式跳转到支付Web。
预提交待支付记录 保证参数的安全问题。
设计模式:单例 工厂 代理 模版 外观、策略
策略+工厂+反射机制 实现解决if else 多重判断问题。
聚合支付平台对接多个不同的支付接口,但是支付接口大体实现思路都是相同的,唯一实现接口不同。
重构一下聚合支付平台提交参数 根据不同渠道(支付接口)返回不同的from表单提交参数。
分析设计模式
传统多个 if判断 维护信、不易扩展。
使用策略模式解决 多重if 判断问题。
在渠道表中CLASS_ADDRES 字段 具体策略实现的子类 最终使用java反射机制执行。
大体实现思路:使用渠道id查询数据获取CLASS_ADDRES,在使用java的反射机制执行toPayHtml。
策略模式
回调代码中,如何实现基于设计模式的重构?
支付回调(大体流程肯定是相同的),实现不同具体对接回调的代码不同。
相同点:使用异步通知,返回对应的结果。
模版方法设计模式(定义抽象类 (相同的代码)定义大体的骨架),不同的方法使用具体子类进行实现
相同点:
1.将报文数据存放到es
2.验证报文参数
3.将日志根据支付id存放到数据库中
4.执行的异步回调业务逻辑
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。