开篇前絮叨两句,也算是我个人的一个记录吧,整体实现了微信扫码支付,但还有很多细节和提升的点,为了简介明了,只将整体过程给大家串一下,如果有大佬看到也要多多指点,不懂的也可以私信我。如果想了解微信登陆的,在分栏微信相关也有文章,大家可以看一下
实现应用微信支付,你需要有微信商户平台: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:数据库设计
视频表 也可以认为是商品表 里面的一些字段是按照我项目需求来的,有一些你感觉用不到的可以不加,如果你有自己的数据库设计更好
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;
复制代码
订单表 用户表就不给大家上了 无非就是跟了个用户主键 自己创一个就可以了
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方法
/**
* 视频实体
*/
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;
}
复制代码
/**
* 视频订单实体
*/
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;
}
复制代码
/**
* 订单数据传输对象
*/
public class VideoOrderDto extends VideoOrder {
}
复制代码
Step3.3:配置文件
******代表用自己的微信配置
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方法
/**
* 微信配置类
*/
@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方法
相关依赖
<!--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>
复制代码
/**
* 封装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相关的工具类,基本给大家的无差别,这里我就直接给大家上代码用
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:返回工具类
/**
* 数据传输对象(后端输出对象)
* @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;
}
}
复制代码
/**
* 用于返回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层
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文件
<?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:控制层
一切准备工作完成后重点要来了!
控制层先这么 稍后再更新
/**
* 视频订单控制层
*/
@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实现
这里的操作是先保证下单成功,数据库能生成数据
注释的地方都是需要更新的
完成这一步可以先启动一下,调一下接口 看看数据能否添加成功
@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下再写一个统一下单方法 生成签名
/**
* 统一下单
* @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二维码依赖
<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这个值
@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了!就行了。如果不告诉微信它会一直给你发消息,直到你告诉他。
@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了。如果有小伙伴需要源码的可以私信,感谢!
感觉不错的大佬点个赞呗~ 手敲截图演示不易