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

·  阅读 2504

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

实现应用微信支付,你需要有微信商户平台: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了。如果有小伙伴需要源码的可以私信,感谢!

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

分类:
后端
收藏成功!
已添加到「」, 点击更改