微信支付

1 开发准备

1.1 开发文档

微信支付接口调用的整体思路:

按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。

在线微信支付开发文档:

https://pay.weixin.qq.com/wiki/doc/api/index.html

这里用的是微信公众平台,微信登录是微信开发平台,并且不能同一个邮箱,这个微信支付必须是服务号且认证过的。所以鑫哥又没办法自己申请了,还是用的我们的大尚硅谷的,强势给尚硅谷打一波广告…..希望大家去B站关注一波吧…..

1
2
3
4
1. appid:微信公众账号或开放平台APP的唯一标识
2. mch_id:商户号 (配置文件中的partner)
3. partnerkey:商户密钥
4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性

1.2 微信支付模式

这是官方的模式二,模式一是用户自己选择支付金额,二维码永久生效

这个模式二是系统生成价格,然后二维码只能使用一次

1558448510488

业务流程说明:

1
2
3
4
5
6
7
8
9
10
11
12
1.商户后台系统根据用户选购的商品生成订单。
2.用户确认支付后调用微信支付【统一下单API】生成预支付交易;
3.微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
4.商户后台系统根据返回的code_url生成二维码。
5.用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
6.微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
7.用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
8.微信支付系统根据用户授权完成支付交易。
9.微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
10.微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
11.未收到支付通知的情况,商户后台系统调用【查询订单API】。
12.商户确认订单已支付后给用户发货。

1.3 微信支付SDK

微信支付提供了SDK, 大家下载后打开源码,install到本地仓库。

(鑫哥是没发现有什么用…..倒不如直接看我文档手摸手一步一步带你搭建,最后源码也会放我gitee上)

1537902584152

使用微信支付SDK,在maven工程中引入依赖

1
2
3
4
5
6
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>

我们主要会用到微信支付SDK的以下功能:

获取随机字符串

1
WXPayUtil.generateNonceStr()

MAP转换为XML字符串(自动添加签名)

1
WXPayUtil.generateSignedXml(param, partnerkey)

XML字符串转换为MAP

1
WXPayUtil.xmlToMap(result)

微信支付需要引入的依赖

1
2
3
4
5
6
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>

1.4 HttpClient工具类

HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。

HttpClient通俗的讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用HttpClient.

关于HttpClient(原生)具体的使用不属于我们本章的学习内容,我们这里这里为了简化HttpClient的使用,提供了工具类HttpClient(对原生HttpClient进行了封装)

HttpClient工具类代码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;

public boolean isHttps() {
return isHttps;
}

public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}

public String getXmlParam() {
return xmlParam;
}

public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}

public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}

public HttpClient(String url) {
this.url = url;
}

public void setParameter(Map<String, String> map) {
param = map;
}

public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}

public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}

public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}

public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}

/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet()) {
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
}
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}

private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
@Override
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
}
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}

public int getStatusCode() {
return statusCode;
}

public String getContent() throws ParseException, IOException {
return content;
}
}

2 微信支付二维码生成

2.1 代码实现

index.html 点击跳转到创建订单的 controller 这里订单Id测试的时候必须换!!!! 重复的微信服务器会返回错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}"></title>
</head>
<body>
<div >
<!-- 这里的total_fee单位是分 所以=1就是1分钱-->
<font color="black"><strong>微信支付测试</strong></font><br/><br/>
<a href="/createOrder?orderNo=000001&total_fee=1">创建订单</a>

</div>
</body>

后端controller(最后复制了一个完整的类,可以直接跳过去粘贴到项目中)

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
@RequestMapping("/createOrder")
public String createNatvie(String orderNo, String total_fee ,Model modle) {
try {
//2 使用map设置生成二维码需要参数
Map m = new HashMap();
m.put("appid","wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("nonce_str", WXPayUtil.generateNonceStr());
m.put("body", "测试一波在线支付"); //课程标题
m.put("out_trade_no", orderNo); //订单号
m.put("total_fee", total_fee);
m.put("spbill_create_ip", "127.0.0.1");
m.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");
m.put("trade_type", "NATIVE");
//3 发送httpclient请求,传递参数xml格式,微信支付提供的固定的地址
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
//设置xml格式的参数
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
//执行post请求发送
client.post();
//4 得到发送请求返回结果
//返回内容,是使用xml格式返回
String xml = client.getContent();
//把xml格式转换map集合,把map集合返回
Map<String,String> resultMap = WXPayUtil.xmlToMap(xml);
System.out.println(resultMap);
//最终返回数据 的封装
Map map = new HashMap();
map.put("out_trade_no", orderNo);
map.put("result_code", resultMap.get("result_code")); //返回二维码操作状态码
map.put("code_url", resultMap.get("code_url")); //二维码地址
modle.addAttribute("map",map);
return "pay";
}catch(Exception e) {
e.printStackTrace();
modle.addAttribute("map","adwadwada");
return "pay";
}
}

创建前端生成二维码的html

有两个js文件 其中一个是生成二维码的qrious.js

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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>二维码入门Demo</title>
<script src="jquery.min.js"></script>
<script src="qrious.js"></script>
</head>
<body>
<div>二维码付款</div>
<img id="myqrious">

<script th:inline="javascript">

var code_url = [[${map.code_url}]]
console.log(code_url)
var qrious = new QRious({
element:document.getElementById("myqrious"),// 指定的是图片所在的DOM对象
size:250,//指定图片的像素大小
level:'H',//指定二维码的容错级别(H:可以恢复30%的数据)
value:code_url //指定二维码图片代表的真正的值
});
function orderStatus() {
$.post("returnUrl",{},function (data) {
if(data==1){
window.location.href="/"
}
});
};

setInterval("orderStatus()",3000)
</script>
<dev></dev>
</body>
</html>

2.2 测试

image-20200826215703006

image-20200826215725213

image-20200826215417398

image-20200826215353029

1
2
3
4
5
6
7
//contoller中的返回map 如果订单id重复就会返回err_code

//订单重复
{nonce_str=tZw3Eyx0I2vHEx5r, appid=wx74862e0dfcf69954, sign=ABDF977D369E83174B5170063D31F741, err_code=ORDERPAID, return_msg=OK, result_code=FAIL, err_code_des=该订单已支付, mch_id=1558950191, return_code=SUCCESS}

//不重复
{nonce_str=gUMCr9HUMjsliHt1, code_url=weixin://wxpay/bizpayurl?pr=3BA0LnU, appid=wx74862e0dfcf69954, sign=10144DA6CE17E2D8715BDAAFEB9BCB69, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, mch_id=1558950191, return_code=SUCCESS, prepay_id=wx26211128920344780912e4758977c40000}

目前项目是支付是完成了,微信是扣钱了,但是数据库状态是什么,成功了吗,而且页面还在这二维码页面,不合适了。

因为这个支付有点绕,所以分开讲,下面就开始解决这些问题。

3 订单状态及回调

查询订单支付状态去微信服务器

因为支付了用户自己是肯定知道了,但是项目里面不知道(微信支付成功会异步回调给项目,但是万一项目宕机了呢,微信默认是会重试三次,如果还没收到需要自己去查询),所以需要自己去微信服务器查询

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
//查询订单支付状态
@RequestMapping("/queryPayStatus")
@ResponseBody
public Map<String, String> queryPayStatus(String orderNo) {
try {
//1、封装参数
Map m = new HashMap<>();
m.put("appid", "wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("out_trade_no", orderNo);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//2 发送httpclient
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
client.post();
//3 得到请求返回内容
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
//6、转成Map再返回
return resultMap;
//伪代码放这里 自己封装
/* if(map == null) {
return R.error().message("支付出错了");
}
//如果返回map里面不为空,通过map获取订单状态
if(map.get("trade_state").equals("SUCCESS")) {//支付成功
//添加记录到支付表,更新订单表订单状态
payLogService.updateOrdersStatus(map);
return R.ok().message("支付成功");
}
return R.ok().code(25000).message("支付中");*/
}catch(Exception e) {
return null;
}
}

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"transaction_id":"4200000723202008267389085785",
"nonce_str":"Nf3taNRG42MDSIhN",
"trade_state":"SUCCESS",
"bank_type":"OTHERS",
"openid":"oHwsHuIJfVKb5N9Yy3fyuzQxa3GE",
"sign":"F20256D926418E00DA9D8D1548789276",
"return_msg":"OK",
"fee_type":"CNY",
"mch_id":"1558950191",
"cash_fee":"1",
"out_trade_no":"00001231301",
"cash_fee_type":"CNY",
"appid":"wx74862e0dfcf69954",
"total_fee":"1",
"trade_state_desc":"支付成功",
"trade_type":"NATIVE",
"result_code":"SUCCESS",
"attach":"",
"time_end":"20200826211143",
"is_subscribe":"N",
"return_code":"SUCCESS"
}

同步回调

用户扫码之后要修改数据库状态吧,也得给用户跳转到别的页面吧。

前端代码还是之前那个 pay.html,不过用到了后面的监听器了,每隔三秒监听业务状态,发生变化时,直接同步回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<div>二维码付款</div>
<img id="myqrious">
<script th:inline="javascript">

var code_url = [[${map.code_url}]]
console.log(code_url)
var qrious = new QRious({
element:document.getElementById("myqrious"),// 指定的是图片所在的DOM对象
size:250,//指定图片的像素大小
level:'H',//指定二维码的容错级别(H:可以恢复30%的数据)
value:code_url //指定二维码图片代表的真正的值
});
function orderStatus() {
$.post("returnUrl",{},function (data) {
if(data==1){
window.location.href="/"
}
});
};

setInterval("orderStatus()",3000)
</script>
</body>
1
2
3
4
这里用给i复制代替业务逻辑,i是全局变量,并发直接打到,代码有很严重的问题,必须根据业务自己修改!!!
鑫哥这里只是用这个来代替数据库查询耗时操作
(PS:如果用过支付宝支付的话 支付宝支付有returnUrl直接传递到支付宝服务器,你只需要自己配置好地址就能自动支付完成回调了,
微信需要自己做一个监听器,就这里有点小小的区别)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 添加支付记录和更新订单状态
* 前端扫码支付查询这里状态看是否修改完数据库状态
* 作用类似于 微信支付的同步回调
*/
int i=0;
@RequestMapping("/returnUrl")
@ResponseBody
public int updateOrdersStatus() {
i++;
System.err.println("#############"+i);
try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace();}
if(i==10){
i=0;
return 1;
}
return 0;

}

异步回调

这个就是直接支付发送到微信服务器,扣款之后,微信服务器给你发送成功与否的消息,想想既让是微信回调,这波肯定得连接外网了,一波localhost肯定给你安排不了了,其中创建订单第一步就会让你发送一个异步回调的地址,如果不写异步回调地址,就只能自己主动去查询了,就是之前那个查询订单支付状态去微信服务器,实际开发中肯定项目部署上去,回调域名地址,但我这开发过程中,用个内网穿透也方便调试什么的,美滋滋。鑫哥为了担心有小老弟连这个都没用过,也懒得单独整篇博客了,小题大做,所以直接在这里教学一波。

发布在文章的末尾了 可以自己去看看。

异步回调地址如图(不影响视觉我直接截图了,就是前面的创建订单)

image-20200826224857210

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
/***
* 支付完成异步回调
* @param request
* @return
*/
@RequestMapping(value = "/notify/url")
@ResponseBody
public String notifyUrl(HttpServletRequest request){
System.out.println("----------异步回调--------");
InputStream inStream;
try {
//读取支付回调数据
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
// 将支付回调数据转换成xml字符串
String result = new String(outSteam.toByteArray(), "utf-8");
//将xml字符串转换成Map结构
Map<String, String> map = WXPayUtil.xmlToMap(result);
System.out.println("=="+map);
//响应数据设置
Map respMap = new HashMap();
respMap.put("return_code","SUCCESS");
respMap.put("return_msg","OK");
return WXPayUtil.mapToXml(respMap);
} catch (Exception e) {
e.printStackTrace();
//记录错误日志
}
return null;
}

控制台输出

1
2
3
4
----------异步回调--------

=={transaction_id=4200000710202008262414000181, nonce_str=5459dab210484b818d8fb3a5819aeec1, bank_type=OTHERS, openid=oHwsHuIJfVKb5N9Yy3fyuzQxa3GE, sign=BB4ADF1E575D353CAF87F95C21C0184C, fee_type=CNY, mch_id=1558950191, cash_fee=1, out_trade_no=12312312, appid=wx74862e0dfcf69954, total_fee=1, trade_type=NATIVE, result_code=SUCCESS, time_end=20200826225114, is_subscribe=N, return_code=SUCCESS}

这个接入微信支付可能真的有点小饶,关键上面那张图片兄弟们得理解了。

4.内网穿透

①目的

让本地运行的项目可以通过外网访问。

②工作机制

image

③NATAPP内网穿透服务使用

image-20200826224420143

image-20200826224618511

1
2
3
4
5
6
7
8
9
10
#authtoken每个人不一样,这个文件仅供参考
#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=79a1980a333f8a5f #对应一条隧道的authtoken
clienttoken= #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy= #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空

启动natapp.exe,如果上面操作成功,会看到下面效果:

image

④测试效果

[1]启动本地应用

本地应用监听的端口号需要和隧道一致,比如都是8080

[2]通过隧道暴露到外网的域名访问本地应用

例如:http://aatczu.natappfree.cc/apple/to/thymeleaf/page