0%

通用型支付系统

通用型支付系统的实现

一、系统概述

下单–》支付

image-20201119114332792

支付结果:

image-20201119114412609

二、数据库设计

  • 表关系

    image-20210522175938750

    image-20210522175946905

  • 唯一索引

  • 单索引及组合索引

    加快查询速度

    订单号,user_id来查

  • 时间戳

三、支付系统

消息队列MQ应用:业务解耦,实现支付高性能

1.支付知识普及

1.1 支付场景

微信支付场景

image-20201124185915599

付款码支付:主要用于商户收银员用扫码设备扫描用户的条码/二维码支付。超市中很常见

详细介绍网址: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/

支付宝支付场景

支付宝支付和微信支付大致相似,平时用的最多的也就是当面付

image-20201129112417755

官方文档: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

时序图:

电脑网站支付.png

1.3 同步/异步支付

https://blog.csdn.net/shiyong1949/article/details/80854656

异步与同步的区别简而言之就是一个需要等待,一个不需要等待。同步固然是好,但是涉及到支付金钱等方面还是要以安全为主。所以支付以异步为准。

2.通用型支付系统开发

image-20201124194214540

参考支付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小时,过期后扫码不能再发起支付。

生成二维码所需参数列表

image-20201125160437833

商户提供的支付回调URL(回调地址设置)需要实现以下功能:接收用户扫码后微信支付系统发送的数据,根据接收的数据生成支付订单,调用【统一下单API】提交支付交易。

输入参数:

image-20201125160553821

输出参数:

image-20201125160658048

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

时序图:

电脑网站支付.png

从图中可以大概知道思路,

首先发起支付:商户(商户应用私钥签名)–》支付宝(商户应用公钥验签)

异步通知:支付宝(支付宝私钥签名)–》商户(支付宝公钥验签)

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) {
/**
* 1.签名校验
*/
PayResponse payResponse = bestPayService.asyncNotify(notifyDate);
log.info("payResponse={}",payResponse);
/**
* 2.金额校验
*/
PayInfo payInfo = payInfoMapper.selectByOrderNo(Long.parseLong(payResponse.getOrderId()));
if (payInfo == null) {
//告警
throw new RuntimeException("通过orderNo查询到的结果是null");
}


/**
* 3.修改订单支付状态
*/
//如果订单支付状态不是"已支付"
if (!payInfo.getPlatformStatus().equals(OrderStatusEnum.SUCCESS.name())) {
//Double类型比较大小,精度。1.00 1.0
if (payInfo.getPayAmount().compareTo(BigDecimal.valueOf(payResponse.getOrderAmount())) != 0) {
//告警
throw new RuntimeException("异步通知中的金额和数据库里的不一致,orderNo=" + payResponse.getOrderId());
}

//3. 修改订单支付状态
payInfo.setPlatformStatus(OrderStatusEnum.SUCCESS.name());
payInfo.setPlatformNumber(payResponse.getOutTradeNo());

payInfoMapper.updateByPrimaryKeySelective(payInfo);
}

//TODO pay发送MQ消息,mall接受MQ消息




/**
* 4.告诉微信/支付宝不要再通知了
*/

if (payResponse.getPayPlatformEnum() == BestPayPlatformEnum.WX) {
//4. 告诉微信不要再通知了
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 支付与数据库

支付数据肯定是要存储到数据库中的

image-20201203101112529

数据:

image-20201203101112529

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