通用型支付系统的实现
一、系统概述
下单–》支付
支付结果:
二、数据库设计
表关系
唯一索引
单索引及组合索引
加快查询速度
订单号,user_id来查
时间戳
三、支付系统
消息队列MQ应用:业务解耦,实现支付高性能
1.支付知识普及
1.1 支付场景
微信支付场景
付款码支付:主要用于商户收银员用扫码设备扫描用户的条码/二维码支付。超市中很常见
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_1
JSAPI支付:商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
Native支付:用户扫描商户展示在各种场景的二维码进行支付。
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
APP支付:适用于商户在移动端APP中集成微信支付功能。
商户APP调用微信提供的SDK调用微信支付模块,商户APP会跳转到微信中完成支付,支付完后跳回到商户APP内,最后展示支付结果。目前微信支付支持手机系统有:IOS(苹果)、Android(安卓)和WP(Windows Phone)。
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_1
H5支付:是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。
主要用于触屏版的手机浏览器请求微信支付的场景。可以方便的从外部浏览器唤起微信支付。
提醒:H5支付不建议在APP端使用,如需要在APP中使用微信支付,请接APP支付
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_1
小程序支付:如果开发者已做过JSAPI或JSSDK调起微信支付,接入小程序支付非常相似,以下是三种接入方式的对比:
详细介绍网址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1
刷脸支付:https://pay.weixin.qq.com/wiki/doc/wxfacepay/
支付宝支付场景
支付宝支付和微信支付大致相似,平时用的最多的也就是当面付
官方文档:https://opendocs.alipay.com/open/194/105072
1.2 名词解释
微信:
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=2_2
支付宝:
配置参数 |
示例值解释 |
获取方式/示例值 |
URL |
支付宝网关(固定) |
https://openapi.alipay.com/gateway.do |
APPID |
APPID 即创建应用后生成 |
获取见上方 创建应用 |
APP_PRIVATE_KEY |
开发者私钥,由开发者自己生成 |
获取见 配置密钥 |
FORMAT |
参数返回格式,只支持 json |
json(固定) |
CHARSET |
编码集,支持 GBK/UTF-8 |
开发者根据实际工程编码配置 |
ALIPAY_PUBLIC_KEY |
支付宝公钥,由支付宝生成 |
获取详见 配置密钥 |
SIGN_TYPE |
商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2 |
RSA2 |
时序图:
1.3 同步/异步支付
https://blog.csdn.net/shiyong1949/article/details/80854656
异步与同步的区别简而言之就是一个需要等待,一个不需要等待。同步固然是好,但是涉及到支付金钱等方面还是要以安全为主。所以支付以异步为准。
2.通用型支付系统开发
参考支付demo
https://github.com/Pay-Group/best-pay-sdk
https://github.com/Pay-Group/best-pay-demo
2.1 SpringBoot项目初始化
maven:3.6.3
jdk:1.8
springboot2.1.7
2.2 对接微信Native支付
微信支付我实现的是Native支付,Native支付有两种模式。模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。
商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
微信Native支付:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
模式二:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
注意:code_url有效期为2小时,过期后扫码不能再发起支付。
生成二维码所需参数列表
商户提供的支付回调URL(回调地址设置)需要实现以下功能:接收用户扫码后微信支付系统发送的数据,根据接收的数据生成支付订单,调用【统一下单API】提交支付交易。
输入参数:
输出参数:
2.3 对接支付宝支付
该项目支付宝实现的是电脑网站支付
支付宝电脑网站支付:https://opendocs.alipay.com/open/270/105898
支付宝沙箱环境:https://opendocs.alipay.com/open/200/105311
接入之前需要设置这些参数,
配置参数 |
示例值解释 |
获取方式/示例值 |
URL |
支付宝网关(固定) |
https://openapi.alipay.com/gateway.do |
APPID |
APPID 即创建应用后生成 |
获取见上方 创建应用 |
APP_PRIVATE_KEY |
开发者私钥,由开发者自己生成 |
获取见 配置密钥 |
FORMAT |
参数返回格式,只支持 json |
json(固定) |
CHARSET |
编码集,支持 GBK/UTF-8 |
开发者根据实际工程编码配置 |
ALIPAY_PUBLIC_KEY |
支付宝公钥,由支付宝生成 |
获取详见 配置密钥 |
SIGN_TYPE |
商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2 |
RSA2 |
Java代码配置如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号 appId: xxxxxxxx # 商户私钥,您的PKCS8格式RSA2私钥 privateKey: xxxxxxx # 支付宝公钥,查看地址:https://openhome.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 publicKey: xxxxxxx # 服务器异步通知页面路径需http://格式的完整路径,不能加?id=123这类自定义参数 notifyUrl: http://xutakf.natappfree.cc/notify # 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数 returnUrl: http://xutakf.natappfree.cc/pay/success # 签名方式 signType: RSA2 # 字符编码格式 charset: utf-8 # 支付宝网关 gatewayUrl: https://openapi.alipaydev.com/gateway.do
|
时序图:
从图中可以大概知道思路,
首先发起支付:商户(商户应用私钥签名)–》支付宝(商户应用公钥验签)
异步通知:支付宝(支付宝私钥签名)–》商户(支付宝公钥验签)
2.4 支付功能创建
控制层:
创建订单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Autowired private PayService payService;
@Autowired private WxPayConfig wxPayConfig;
@GetMapping("/create") public ModelAndView create(@RequestParam("orderId")String orderId, @RequestParam("amount")BigDecimal amount,@RequestParam("payType") BestPayTypeEnum bestPayTypeEnum){
PayResponse response = payService.create(orderId,amount,bestPayTypeEnum); Map<String,String> map = new HashMap<String, String>();
if (bestPayTypeEnum == BestPayTypeEnum.WXPAY_NATIVE){ map.put("codeUrl",response.getCodeUrl()); map.put("orderId",orderId); map.put("returnUrl", wxPayConfig.getReturnUrl()); return new ModelAndView("createForWxNative",map);
}else if (bestPayTypeEnum == BestPayTypeEnum.ALIPAY_PC){ map.put("body",response.getBody()); return new ModelAndView("createForAliPayPc",map); }
throw new RuntimeException("不支持的支付类型");
}
|
业务层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override public PayResponse create(String orderId, BigDecimal amount, BestPayTypeEnum bestPayTypeEnum) {
PayInfo payInfo = new PayInfo(Long.parseLong(orderId), PayPlatformEnum.getByBestPayTypeEnum(bestPayTypeEnum).getCode(), OrderStatusEnum.NOTPAY.name(), amount); payInfoMapper.insertSelective(payInfo);
PayRequest payRequest = new PayRequest(); payRequest.setOrderId(orderId); payRequest.setOrderAmount(amount.doubleValue()); payRequest.setOrderName("测试支付功能"); payRequest.setPayTypeEnum(bestPayTypeEnum);
PayResponse payResponse = bestPayService.pay(payRequest); log.info("response={}",payResponse);
return payResponse; }
|
2.5 支付结果异步通知
异步结果校验主要有四步:
1.签名校验
2.金额校验
3.修改订单支付状态
4.告诉微信/支付宝不要再通知了
控制层:
1 2 3 4 5 6
| @PostMapping("/notify") @ResponseBody public String asyncNotify(@RequestBody String notifyData){ log.info("notifyData{}",notifyData); return payService.asyncNotify(notifyData); }
|
业务层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @Override public String asyncNotify(String notifyDate) {
PayResponse payResponse = bestPayService.asyncNotify(notifyDate); log.info("payResponse={}",payResponse);
PayInfo payInfo = payInfoMapper.selectByOrderNo(Long.parseLong(payResponse.getOrderId())); if (payInfo == null) { throw new RuntimeException("通过orderNo查询到的结果是null"); }
if (!payInfo.getPlatformStatus().equals(OrderStatusEnum.SUCCESS.name())) { if (payInfo.getPayAmount().compareTo(BigDecimal.valueOf(payResponse.getOrderAmount())) != 0) { throw new RuntimeException("异步通知中的金额和数据库里的不一致,orderNo=" + payResponse.getOrderId()); }
payInfo.setPlatformStatus(OrderStatusEnum.SUCCESS.name()); payInfo.setPlatformNumber(payResponse.getOutTradeNo());
payInfoMapper.updateByPrimaryKeySelective(payInfo); }
if (payResponse.getPayPlatformEnum() == BestPayPlatformEnum.WX) { return "<xml>\n" + " <return_code><![CDATA[SUCCESS]]></return_code>\n" + " <return_msg><![CDATA[OK]]></return_msg>\n" + "</xml>"; }else if (payResponse.getPayPlatformEnum() == BestPayPlatformEnum.ALIPAY) { return "success"; }
throw new RuntimeException("异步通知中错误的支付平台"); }
|
2.6 支付与数据库
支付数据肯定是要存储到数据库中的
数据:
2.7 规范配置
通过yml文件写配置信息,然后在写一个config类将配置整合,这样可以解耦合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component @ConfigurationProperties(prefix = "alipay") @Data public class AlipayAccountConfig {
private String appId;
private String privateKey;
private String publicKey;
private String notifyUrl;
private String returnUrl;
private String serverUrl; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Component @ConfigurationProperties(prefix = "wxpay") @Data public class WxAccountConfig {
private String appId;
private String mchId;
private String mchKey;
private String notifyUrl;
private String returnUrl; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Component public class BestPayConfig {
@Autowired private WxAccountConfig wxAccountConfig;
@Autowired private AlipayAccountConfig alipayAccountConfig;
@Bean public BestPayService bestPayServiceByWX(WxPayConfig wxPayConfig) { AliPayConfig aliPayConfig = new AliPayConfig(); aliPayConfig.setAppId(alipayAccountConfig.getAppId()); aliPayConfig.setPrivateKey(alipayAccountConfig.getPrivateKey()); aliPayConfig.setAliPayPublicKey(alipayAccountConfig.getPublicKey()); aliPayConfig.setNotifyUrl(alipayAccountConfig.getNotifyUrl()); aliPayConfig.setReturnUrl(alipayAccountConfig.getReturnUrl());
BestPayServiceImpl bestPayService = new BestPayServiceImpl(); bestPayService.setWxPayConfig(wxPayConfig); bestPayService.setAliPayConfig(aliPayConfig); return bestPayService;
}
@Bean public AliPayConfig aliPayConfig(){ AliPayConfig aliPayConfig = new AliPayConfig();
aliPayConfig.setAppId(alipayAccountConfig.getAppId()); aliPayConfig.setPrivateKey(alipayAccountConfig.getPrivateKey()); aliPayConfig.setAliPayPublicKey(alipayAccountConfig.getPublicKey()); aliPayConfig.setNotifyUrl(alipayAccountConfig.getNotifyUrl()); aliPayConfig.setReturnUrl(alipayAccountConfig.getReturnUrl());
return aliPayConfig; }
@Bean public WxPayConfig wxPayConfig() { WxPayConfig wxPayConfig = new WxPayConfig();
wxPayConfig.setAppId(wxAccountConfig.getAppId()); wxPayConfig.setMchId(wxAccountConfig.getMchId()); wxPayConfig.setMchKey(wxAccountConfig.getMchKey());
wxPayConfig.setNotifyUrl(wxAccountConfig.getNotifyUrl()); wxPayConfig.setReturnUrl(wxAccountConfig.getReturnUrl());
return wxPayConfig; } }
|
3.内网穿透
可以通过内网穿透的方式调试支付功能
https://natapp.cn/article/natapp_newbie