SpringBoot:实现PC端微信扫码支付-超详细实战!

开篇前絮叨两句,也算是我个人的一个记录吧,整体实现了微信扫码支付,但还有很多细节和提升的点,为了简介明了,只将整体过程给大家串一下,如果有大佬看到也要多多指点,不懂的也可以私信我。如果想了解微信登陆的,在分栏微信相关也有文章,大家可以看一下

实现应用微信支付,你需要有微信商户平台:pay.weixin.qq.com。申请公众号(服务号)认证费300,才能开通微信支付。在微信支付中需要有公众号id和密钥还有商户id和密钥,如果你没有上线应用把整体流程明白就可以,当然,朋友公司之类有的话更好。

先将一些用到的链接地址放在这里,方便大家查看

微信支付申请流程pay.weixin.qq.com/guide/qrcod…

常用支付方式文档pay.weixin.qq.com/wiki/doc/ap…

案例演示pay.weixin.qq.com/guide/webba…

扫码支付文档pay.weixin.qq.com/wiki/doc/ap…

微信支付时序图pay.weixin.qq.com/wiki/doc/ap…

统一下单文档pay.weixin.qq.com/wiki/doc/ap…

签名算法规范pay.weixin.qq.com/wiki/doc/ap…

签名校验工具pay.weixin.qq.com/wiki/doc/ap…

NatAPP内网穿透natapp.cn/

上代码之前需要给大家讲解一些必要知识,不然直接来代码你还是一头雾水,完成了功能但不明白这个过程也是白费

Step1:微信网站扫码支付介绍

Stpe1.1:名词理解

appid:公众号唯一表示

appsecret:公众号密钥

mch_id:商户号,申请微信支付的时候分配的

key:支付交易过程中生成签名的密钥, 设置路径: 微信商户平台(pay.wexin.qq.com)–>账户中心–>账户设置–>API安全–>密钥设置

Step1.2:微信支付交互方式

  • POST方式提交
  • XML格式的协议
  • 签名算法MD5
  • 交互业务规则 先判断协议字段返回,再判断业务返回,最后判断交易状态
    ​ 接口交易单位 分
    ​ 交易类型:JSAPI– 公众号支付、NATIVE–原生扫码支付、APP-app支付
  • 商户订单号规则:商户支付的订单号由商户自定义生成,仅支持使用字母、数字、中划线-、下划线_、竖线|、星号*这些英文半角字符的组合,请勿使用汉字或全角等特殊字符,微信支付要求商户订单号保持唯一性

Step1.3:时序图讲解(重点!)

顶部有微信官方的时序图链接。这个图一定要明白,因为下面我上代码会告诉这是时序中的第几步,看的图就容易明白代码了

时序图说白了就是你一个操作的流程,这个过程中会经过哪个对象的方法,返回什么操作等的过程

顺序的时序图:就是交互流程图(把大象装进冰箱分几步)

对象(Object)、生命线(Lifeline)、激活(Activation)、消息(Message)

对象:时序图中的对象在交互中扮演的角色就是对象,使用矩形将对象名称包含起来, 名称下有下划线

生命线:生命线是一条垂直的虚线, 这条虚线表示对象的存在, 在时序图中, 每个对象都有生命线

激活:代表时序图中对象执行一项操作的时期, 表示该对象被占用以完成某个任务,当对象处于激活时期, 生命线可以拓宽为矩形

消息:对象之间的交互是通过相互发消息来实现的,箭头上面标出消息名,一个对象可以请求(要求)另一个对象做某件事件。消息从源对象指向目标对象,消息一旦发送便将控制从源对象转移到目标对象,息的阅读顺序是严格自上而下的

消息交互中的实线:请求消息

消息交互中的虚线:响应返回消息

自己调用自己的方法:反身消息

用我的白话给大家讲一下:

1.用户下单,进入购买页面,点击购买进入后台

2.后台收到请求,生成订单。大家肯定都用淘宝买过东西对吧,你购买东西但发现钱不够,这个订单在一段时间内都存在等待你支付,但这个订单在数据库中已经申城了,之后你支付后订单才会修改状态

3.后台调微信统一下单,不光你后台生成订单,微信也生成预订单号

4.微信返回code_url支付交易链接,通过这个值生成二维码图片

5.将这个二维码返回给前台用户,用户进行扫一扫支付。这里是直接和微信交互的

6.微信支付系统验证有效性,验证后返回用户是否确认支付

7.用户确认,输密码。返回给微信支付系统授权

8.验证授权,完成支付交易

9.返回支付结果,发送短信和微信消息提示。这里是并行处理,一个通知用户,一个通知后台

10.异步通知后台支付结果,会携带一些参数,订单号等。收到结果后告知接收情况

11.如果后台宕机,微信会定时发送通知,后台可以做定时任务,用户支付了但订单状态未修改,定时调微信的接口,调API有没有成功做操作

下面的一些流程都是看业务情况
在这里插入图片描述

Step2:统一下单

商户系统先调用该接口在微信支付服务后台生成预支付交易订单,返回正确的预支付交易会标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。这里是时序图第二步

顶部链接文档pay.weixin.qq.com/wiki/doc/ap…

这里上个统一下单的流程图

1.告诉微信支付你要下单

2.微信支付系统数据库生成一条订单,但未支付。你的后台也是生成一条未支付订单

3.用户支付后微信支付将订单更新为已支付

4.微信调后台告诉我们已经支付了

5.后台再返回确认信息等

时序图和统一下单的流程基本都在这了,一定要明白,一定要清楚!
在这里插入图片描述

Step2.1:统一下单请求

向微信支付系统发送http请求,我们需要组成一个xml格式的数据消息,里面包括一些必须的参数
在这里插入图片描述

官方例子
在这里插入图片描述

大部分人在做微信支付都是错在这里,签名方式不对,或者传输的一些信息不符合规范等,这里只是先给大家讲解一下,下面实战都会说清楚的。

Stpe2.2:统一下单返回消息

返回一些我们需要的参数,也就是code_url,如果你发送的xml不正确会返回错误提示
在这里插入图片描述

在这里插入图片描述

Step3:战前准备

Step3.1:数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码视频表 也可以认为是商品表 里面的一些字段是按照我项目需求来的,有一些你感觉用不到的可以不加,如果你有自己的数据库设计更好
CREATE TABLE `video` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`title` varchar(255) DEFAULT NULL COMMENT '视频标题',
`summary` varchar(255) DEFAULT NULL COMMENT '详情',
`cover_img` varchar(255) DEFAULT NULL COMMENT '封面图',
`price` int(11) DEFAULT NULL COMMENT '价格-最小单位分',
`c_id` int(10) DEFAULT NULL COMMENT '分类id',
`point` double(11,2) DEFAULT NULL COMMENT '评分-最长11保留两位小数',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`view_num` int(10) DEFAULT NULL COMMENT '观看数',
`online` int(11) DEFAULT '1' COMMENT '0表示未上线,1表示上线',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码订单表	用户表就不给大家上了 无非就是跟了个用户主键 自己创一个就可以了
del字段采用逻辑删除 避免订单出现问题 不删除信息
CREATE TABLE `video_order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单流水号唯一标识',
`state` int(11) DEFAULT NULL COMMENT '订单状态(1支付-0未支付)',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
`total_fee` int(11) DEFAULT NULL COMMENT '订单金额',
`video_id` int(11) DEFAULT NULL COMMENT '视频主键id',
`video_title` varchar(128) DEFAULT NULL COMMENT '标题字段冗余',
`video_img` varchar(255) DEFAULT NULL COMMENT '图片字段冗余',
`user_id` int(11) DEFAULT NULL COMMENT '用户主键id',
`ip` varchar(64) DEFAULT NULL COMMENT '用户ip地址',
`openid` varchar(64) DEFAULT NULL COMMENT '用户标示',
`notify_time` datetime DEFAULT NULL COMMENT '支付回调时间',
`nickname` varchar(32) DEFAULT NULL COMMENT '微信昵称',
`head_img` varchar(128) DEFAULT NULL COMMENT '微信头像',
`del` int(11) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
PRIMARY KEY (`id`),
UNIQUE KEY `out_trade_no` (`out_trade_no`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8;

Step3.2:实体类

自行加上get set方法

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
swift复制代码/**
* 视频实体
*/
public class Video implements Serializable {

private Integer id;

/**
* 视频标题
*/
private String title;

/**
* 描述
*/
private String summary;

/**
* 封面图路径
*/
@JsonProperty("cover_img")
private String coverImg;

/**
* 价格
*/
private Integer price;

/**
* 视频分类
*/
@JsonProperty("c_id")
private Integer cId;

/**
* 评分
*/
private Double point;

/**
* 视频创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
@JsonProperty("create_time")
private Date createTime;

/**
* 观看数
*/
@JsonProperty("view_num")
private Integer viewNum;

/**
* 0表示未上线,1表示上线
*/
private Integer online;
@JsonProperty("chapter_list")
private List<Chapter> chapterList;
}
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
scss复制代码/**
* 视频订单实体
*/
public class VideoOrder implements Serializable {

private Integer id;

/**
* 订单流水号
*/
@JsonProperty("out_trade_no")
private String outTradeNo;

/**
* 订单状态
*/
private Integer state;

/**
* 订单创建时间
*/
@JsonProperty("cover_img")
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
private Date createTime;

/**
* 订单金额
*/
@JsonProperty("total_fee")
private Integer totalFee;

/**
* 视频id
*/
@JsonProperty("video_id")
private Integer videoId;

/**
* 视频荣誉字段-标题
*/
@JsonProperty("video_title")
private String videoTitle;

/**
* 视频冗余字段-图片
*/
@JsonProperty("video_img")
private String videoImg;

/**
* 用户id
*/
@JsonProperty("user_id")
private Integer userId;

/**
* 用户ip地址
*/
private String ip;

@JsonProperty("open_id")
private String openId;

/**
* 支付回调时间2
*/
@JsonProperty("notify_time")
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
private Date notifyTime;

/**
* 冗余字段:微信昵称
*/
@JsonProperty("nick_name")
private String nickName;

/**
* 冗余字段:微信头像
*/
@JsonProperty("head_img")
private String headImg;

/**
* 0表示未删除,1表示已经删除
*/
private Integer del;
}
1
2
3
4
5
scala复制代码/**
* 订单数据传输对象
*/
public class VideoOrderDto extends VideoOrder {
}

Step3.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
ini复制代码server.port=8089

#==============================数据库相关配置==========================================================
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/educationapp?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#使用阿里巴巴druid数据源,默认使用自带的(com.zaxxer,hikari.HikariDataSource)如果使用默认的这里就不用写
spring.datasource.type =com.alibaba.druid.pool.DruidDataSource


#=============================MyBatis相关配置
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#mapper配置扫描
mybatis.mapper-locations=classpath:mapper/*.xml
#配置xml的结果别名 resultType:去掉前缀
mybatis.type-aliases-package=net.jhclass.online_jhclass.model.pojo


#======================================微信相关
#公众号
wxpay.appid=*********
wxpay.appsecret=***********


#微信商户平台商户号
wxpay.mer_id=********
#密钥
wxpay.key=**************
#回调地址
wxpay.callback=http://sj6e6c.natappfree.cc/api/v2/wechat/order/callback

Step3.4:微信配置类

自行加上get、set方法

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
kotlin复制代码/**
* 微信配置类
*/
@Configuration
@PropertySource(value = "classpath:application.properties")
public class WeChatConfig {

/**
* 公众号appid
*/
@Value("${wxpay.appid}")
private String appId;

/**
* 公众号密钥
*/
@Value("${wxpay.appsecret}")
private String appsecret;
/**
* 商户号ID
*/
@Value("${wxpay.mer_id}")
private String mchId;

/**
* 支付key
*/
@Value("${wxpay.key}")
private String key;

/**
* 微信支付回调URL
*/
@Value("${wxpay.callback}")
private String payCallbackUrl;

/**
* 微信统一下单url地址
*/
private final static String UNIFIED_ORDER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder";

Step3.5:封装http、get、post方法

相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<!--HttpClient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
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
scss复制代码/**
* 封装http get post方法
*/
public class HttpUtils {

private static final Gson gson = new Gson();

/**
* get方法
* @param url
* @return
*/
public static Map<String,Object> doGet(String url){

Map<String,Object> map = new HashMap<>();

CloseableHttpClient httpClient = HttpClients.createDefault();


RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000)//连接超时
.setConnectionRequestTimeout(5000)//请求连接超时
.setSocketTimeout(5000)
.setRedirectsEnabled(true)//允许重定向
.build();

HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);

try {
HttpResponse httpResponse = httpClient.execute(httpGet);
if(httpResponse.getStatusLine().getStatusCode() == 200){

String jsonResult = EntityUtils.toString(httpResponse.getEntity());
//转换key value形式
map = gson.fromJson(jsonResult,map.getClass());
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭请求
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}

return map;
}


public static String doPost(String url,String data,int timeout){

CloseableHttpClient httpClient = HttpClients.createDefault();

//超时设置
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(timeout)//连接超时
.setConnectionRequestTimeout(timeout)//请求连接超时
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)//允许重定向
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
//增加头信息
httpPost.addHeader("Content-Type","text/html;chartset=UTF-8");
if(data != null && data instanceof String){//使用字符窜传参
StringEntity stringEntity = new StringEntity(data,"UTF-8");
httpPost.setEntity(stringEntity);
}

try {

CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String result = EntityUtils.toString(httpEntity);
return result;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}

Step3.6:微信支付工具类 xml转map mao转xml 生成签名

微信官方也有java相关的工具类,基本给大家的无差别,这里我就直接给大家上代码用

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
140
141
142
143
ini复制代码public class WXPayUtil {

/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}

}

/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}


/**
* 生成微信支付sign
* @return
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();

//生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
while (it.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}

sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}


/**
* 校验签名
* @param params
* @param key
* @return
*/
public static boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);

String weixinPaySign = params.get("sign").toUpperCase();

return weixinPaySign.equals(sign);
}


/**
* Map转SortedMap 获取有序map
* @param map
* @return
*/
public static SortedMap<String,String> getSortedMap(Map<String,String> map){
SortedMap<String,String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()){
String key = (String)it.next();
String value = map.get(key);
//定义临时变量
String temp="";
if(null!=value){
temp = value.trim();
}
sortedMap.put(key,temp);
}


return sortedMap;
}


}

Step3.7:返回工具类

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
typescript复制代码/**
* 数据传输对象(后端输出对象)
* @param <T>
* Created by hanlu on 2017/5/7.
*/
public class Dto<T>{
private String success; //判断系统是否出错做出相应的true或者false的返回,与业务无关,出现的各种异常
private String errorCode;//该错误码为自定义,一般0表示无错
private String msg;//对应的提示信息
private T data;//具体返回数据内容(pojo、自定义VO、其他)
private int count; // 数据的数量

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}

public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}

}
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
scss复制代码
/**
* 用于返回Dto的工具类
* Created by XX on 17-5-8.
*/
public class DtoUtil {

public static String success="true";

public static String fail="false";

public static String errorCode="0";
/***
* 统一返回成功的DTO
*/
public static Dto returnSuccess(){
Dto dto=new Dto();
dto.setSuccess(success);
return dto;
}
/***
* 统一返回成功的DTO 带数据
*/
public static Dto returnSuccess(String message,Object data){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setMsg(message);
dto.setErrorCode(errorCode);
dto.setData(data);
return dto;
}
/***
* 统一返回成功的DTO 不带数据
*/
public static Dto returnSuccess(String message){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setMsg(message);
dto.setErrorCode(errorCode);
return dto;
}
/***
* 统一返回成功的DTO 带数据 没有消息
*/
public static Dto returnDataSuccess(Object data){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setErrorCode(errorCode);
dto.setData(data);
return dto;
}

/**
* 请求失败,返回错误语句及错误码
* @param message
* @param errorCode
* @return
*/
public static Dto returnFail(String message,String errorCode){
Dto dto=new Dto();
dto.setSuccess(fail);
dto.setMsg(message);
dto.setErrorCode(errorCode);
return dto;
}

/**
* 返回数据 并返回数据数量
* @param data
* @param count
* @return
*/
public static Dto returnPage(Object data,int count){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setErrorCode(errorCode);
dto.setData(data);
dto.setCount(count);

return dto;
}
}

Step4:生成订单

一些其它像查询用户信息和查询视频信息的操作就不给大家上了,就是简单查询,这里主要做订单的,避免大家看不懂下面代码的一些方法

Step4.1:Service层

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
java复制代码public interface VideoOrderService {

/**
* 下单操作 你会问 不应该是int返回么 为什么String? 你dao层写int service就写String,因为需要拿微信返回的code_url 所以这里写String
* @return
*/
String saveVideoOrder(VideoOrderDto videoOrderDto) throws Exception;

/**
* 查询用户订单列表
* @param userId 用户id
* @return
*/
List<VideoOrder> listOrderByUserId(Integer userId);

/**
* 根据订单流水号查找订单对象
* @param outTradeNo
* @return
*/
VideoOrder findByOutTradeNo(String outTradeNo);

/**
* 根据流水号更新订单状态
* @param videoOrder
* @return
*/
int updateVideoOrderByOutTradeNo(VideoOrder videoOrder);
}

Step4.2:mapper文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="net.jhclass.online_jhclass.mapper.VideoOrderMapper">

<!--检查用户订单状态-->
<select id="findByUserIdAndVideoIdAndState" resultType="VideoOrder">
select * from `video_order` where user_id=#{user_id} and video_id=#{video_id} and state=#{state}
</select>

<!--下单-->
<insert id="saveVideoOrder" useGeneratedKeys="true" keyColumn="id" keyProperty="id" parameterType="VideoOrder">
insert into `video_order` (out_trade_no,state,create_time,total_fee,video_id,video_title,video_img,user_id,ip,openid,notify_time,nickname,head_img,del)
values (#{outTradeNo},#{state},#{createTime},#{totalFee},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{openId},#{notifyTime},#{nickName},#{headImg},#{del})
</insert>

<!--查询订单列表-->
<select id="listOrderByUserId" resultType="VideoOrder">
select * from `video_order` where user_id=#{user_id}
</select>

<!--根据订单id 查询订单信息-->
<select id="findById" resultType="VideoOrder">
select * from `video_order` where id=#{order_id} and del=0
</select>

<!--根据订单流水号 查询订单信息-->
<select id="findByOutTradeNo" resultType="VideoOrder">
select * from `video_order` where out_trade_no=#{out_trade_no} and del=0
</select>

<!--逻辑删除订单-->
<update id="del">
update video_order set del=0 where id=#{id} and user_id=#{userId}
</update>

<!--
微信回调更新订单状态
根据订单流水号更新
-->
<update id="updateVideoOrderByOutTradeNo" parameterType="VideoOrder">
update video_order set state=#{state},notify_time=#{notifyTime},openid=#{openId}
where
out_trade_no=#{outTradeNo} and state=0 and del=0
</update>
</mapper>

Step5:控制层

一切准备工作完成后重点要来了!

控制层先这么 稍后再更新

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
less复制代码/**
* 视频订单控制层
*/
@RestController
@RequestMapping("api/v1/pri/order")

public class VideoOrderController {

@Autowired
private VideoOrderService videoOrderService;

/**
* 下单接口
* @param videoId 视频id
* @param request 用户信息
* @return
* @throws Exception
*/
@GetMapping("saveOrder")
public void saveOrder(@RequestParam(value = "video_id",required = true) int videoId,
HttpServletRequest request,
HttpServletResponse response) throws Exception {

//记录用户下单ip
//如果使用reques去拿ip不严谨,容易出现拿不到的情况,会过滤一些http头信息

//获取ip 模拟一个假的ip
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.43";

//获取用户id 这里是我的项目里加了jwt登陆 你可以直接写一次参数传
Integer userId = (Integer)request.getAttribute("user_id");

VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setVideoId(videoId);
videoOrderDto.setUserId(userId);
videoOrderDto.setIp(ip);

videoOrderService.saveVideoOrder(videoOrderDto);

return DtoUtil.returnSuccess("下单成功");
}

}

Step6:ServiceImpl实现

这里的操作是先保证下单成功,数据库能生成数据

注释的地方都是需要更新的

完成这一步可以先启动一下,调一下接口 看看数据能否添加成功

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
scss复制代码@Service
public class VideoOrderServiceImpl implements VideoOrderService {

@Autowired
private VideoOrderMapper videoOrderMapper;

@Autowired
private UserMapper userMapper;

@Autowired
private VideoMapper videoMapper;

@Autowired
private WeChatConfig weChatConfig;

/**
* 下单操作
* 未来版本 优惠卷功能、微信支付、风控用户检查、生成订单基础信息、生成支付信息
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)//默认隔离级别
public String saveVideoOrder(VideoOrderDto videoOrderDto) throws Exception {

int videoId = videoOrderDto.getVideoId();
int userId = videoOrderDto.getUserId();
String ip = videoOrderDto.getIp();

//判断是否已经购买 订单状态码1
VideoOrder videoOrder = videoOrderMapper.findByUserIdAndVideoIdAndState(userId,videoId,1);

if(videoOrder!=null){
//已经支付过了,订单存在
return null;
}

//查询视频信息
Video video = videoMapper.findById(videoId);
//查询用户信息
User user = userMapper.findByUserId(userId);

//生成订单
//构造订单实体 根据用户购买哪个视频做处理
VideoOrder newvideoOrder = new VideoOrder();
newvideoOrder.setCreateTime(new Date());//订单创建时间
newvideoOrder.setOutTradeNo(CommonUtils.generateUUID());//唯一流水号
newvideoOrder.setTotalFee(video.getPrice());//价格

newvideoOrder.setState(0);//支付状态
newvideoOrder.setUserId(userId);//用户id
newvideoOrder.setVideoId(video.getId());//视频id
newvideoOrder.setHeadImg(user.getHeadImg());//微信头像
newvideoOrder.setNickName(user.getUsername());//微信昵称
newvideoOrder.setVideoImg(video.getCoverImg());//冗余字段
newvideoOrder.setVideoTitle(video.getTitle());//冗余字段

newvideoOrder.setDel(0);
newvideoOrder.setIp(ip);

//保存订单
int num = videoOrderMapper.saveVideoOrder(newvideoOrder);


//生成签名
String codeUrl = unifiedOrder(newvideoOrder);

//统一下单

//获取code_url

//生成二维码

return codeUrl;
}

}

Step6.1:签名开发

orderserviceimpl下再写一个统一下单方法 生成签名

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
csharp复制代码/**
* 统一下单
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {

//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());//公众号AppId
params.put("mch_id",weChatConfig.getMchId());//商户ID
params.put("nonce_str", CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());//商品描述
params.put("out_trade_no",videoOrder.getOutTradeNo());//订单流水号
params.put("total_fee",videoOrder.getTotalFee().toString());//商品金额
params.put("spbill_create_ip",videoOrder.getIp());//终端IP
params.put("notify_url",weChatConfig.getPayCallbackUrl());//通知地址
params.put("trade_type","NATIVE");//交易类型 扫码支付

//sign签名 调用工具类
String sign = WXPayUtil.createSign(params,weChatConfig.getKey());
params.put("sign",sign);

//生成签名后转map 进行校验 map>xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
System.out.println(sign);

//统一下单
//发送post请求
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(orderStr == null){
return null;
}
//接收返回结果 将微信返回的结果xml转map
Map<String,String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
if(unifiedOrderMap != null){
return unifiedOrderMap.get("code_url");

}
return null;
}

打断点调试一下

这里很重要,如果你签名生成的不对,下面是无法进行的
在这里插入图片描述

得到payXml值之后复制一下 去微信支付文档签名校验一下,如果能通过,那么恭喜你,重要的第一步完成了。链接在顶部!
在这里插入图片描述

Step6.2:发送请求

签名校验通过后给微信发送请求。这里都是时序图的第二步
在这里插入图片描述

orderStr就是微信返回给我们的信息,如果提示SUCCESS表示成功下单

Step6.3:拿取code_url

主要是说明一下,时序图第三步,微信生成订单后返回这样的值,里面包含code_url,就是二维码生成链接,我们需要这个值来生成二维码

Step7:更新控制层生成二维码

Step7.1:添加google二维码依赖

1
2
3
4
5
6
7
8
9
10
11
xml复制代码		<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>2.0</version>
</dependency>

Step7.2:更新控制层

现在就明白service为什么使用String的返回了吧 就是需要拿到code_url这个值

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
ini复制代码@GetMapping("saveOrder")
public void saveOrder(@RequestParam(value = "video_id",required = true) int videoId,
HttpServletRequest request,
HttpServletResponse response) throws Exception {

//记录用户下单ip
//如果使用reques去拿ip不严谨,容易出现拿不到的情况,会过滤一些http头信息

//获取ip
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.43";

//获取用户id
Integer userId = (Integer)request.getAttribute("user_id");

VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setVideoId(videoId);
videoOrderDto.setUserId(userId);
videoOrderDto.setIp(ip);

//统一下单拿支付交易链接codeUrl
String codeUrl = videoOrderService.saveVideoOrder(videoOrderDto);
if(codeUrl == null){
throw new NullPointerException();
}

try {
//生成二维码配置
Map<EncodeHintType,Object> hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//设置编码类型
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
//构造图片对象
BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
//输出流
OutputStream out = response.getOutputStream();

MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
}catch (Exception e){
e.printStackTrace();
}


}

Step7.3:生成二维码

重新启动项目,使用postman测试,老版的不显示二维码,生成的是乱码,需要去浏览器访问,新版的可以显示二维码
在这里插入图片描述

出现下面的二维码你就可以打开手机扫码了!
在这里插入图片描述

之后的步骤大家都明白吧,就是用户和微信交互了,确认支付输密码之类的。直接到时序图的第八步

Step8:内网穿透接收消息

微信完成预支付信息后,给用户发消息的同时,还给我们后台发消息,告诉我们支付成功了,我们拿到这个信息后修改订单状态就完事了,但问题是我们是本地开发, 怎样接收发来的信息呢?

使用工具NatApp,顶部有链接,使用方法非常简单,使用免费隧道,但每次启动都是随机隧道,所以每次需要改配置文件
在这里插入图片描述

前面的域名: rrgdbr.natappfree.cc 就代理了本地 像我这样就能正常访问本地项目

rrgdbr.natappfree.cc/api/v1/pri/…

在这里插入图片描述

注意配置文件也要修改,可能有些懵,我这个值是在什么时候告诉微信支付系统的呢?就是在我们生成签名第一次给微信发统一下单微信那边就记录了

Step9:接收微信确认消息

路径一定要对啊,别你写回调地址和你控制层接收消息的路径不一致,不然怎么你也收不到消息,第一次我就脑瘫了,路径写错了,打断点试了半天也没进到控制层,浪费了好几毛钱。。。

下面的代码简单说几句,都有注释,流程就是收到请求后验证一下签名,有没有错误信息什么的,之后更新订单状态,完事再告诉微信,我这里OK了!就行了。如果不告诉微信它会一直给你发消息,直到你告诉他。

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
scss复制代码@Controller
@RequestMapping("api/v2/wechat")
public class WechatController {

@Autowired
private WeChatConfig weChatConfig;

@Autowired
private VideoOrderService videoOrderService;

/**
* 微信支付回调
*/
@RequestMapping ("/order/callback")
public void orderCallback(HttpServletResponse response, HttpServletRequest request) throws Exception{

//获取流信息
InputStream inputStream = request.getInputStream();

//转换 比inputStream更快 包装设计模式 性能更高
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
//进行缓冲
StringBuffer sb = new StringBuffer();
String line;
while ((line = in.readLine())!=null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());

//map转sortedMap
SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);

//判断签名是否正确
if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
System.out.println("OK");
//判断业务状态是否正确
if("SUCCESS".equals(sortedMap.get("result_code"))){

String outTradeNo = sortedMap.get("out_trade_no");

//使用队列方式提高性能
VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);

//更新订单状态
if(dbVideoOrder !=null && dbVideoOrder.getState()==0){//判断逻辑看业务场景
VideoOrder videoOrder = new VideoOrder();
videoOrder.setOpenId(sortedMap.get("openid"));
videoOrder.setOutTradeNo(outTradeNo);
videoOrder.setNotifyTime(new Date());
videoOrder.setState(1);
int rows = videoOrderService.updateVideoOrderByOutTradeNo(videoOrder);
System.out.println("受影响行数:"+rows);
//判断影响行数 row==1 或者row==0 响应微信成功或者失败
if(rows==1){//通知微信订单处理成功
response.setContentType("text/xml");
response.getWriter().println("success");
return;
}
}
}
}
//都处理失败
response.setContentType("text/xml");
response.getWriter().println("fail");
}
}

序言:整体微信支付就是这样,但还有细节的地方,验证订单、或者某笔交易出现问题都没有做。还有小伙伴会问。那我前台支付成功后前台是怎么给用户显示支付OK了,你这里方法也没告诉前台的啊。其实这个操作是前台来发请求的,循环的向后台发请求,查询用户这笔订单状态,变成1就不发请求了,然后给用户显示支付OK了。如果有小伙伴需要源码的可以私信,感谢!

WechatIMG244.jpeg
感觉不错的大佬点个赞呗~ 手敲截图演示不易

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%