SpringBoot实战接入微信扫一扫支付功能

前边讲过了 微信的扫一扫登陆功能实战

今天继续实战一下微信的扫一扫支付功能实战

一、准备

我们想接入微信的扫一扫支付功能,那首先需要开通微信的商户平台,然后申请开通支付

本篇是侧重讲我们在代码里怎么接入微信的支付功能,所以具体怎么申请开通微信支付就不细讲了。

申请微信商家账号和开通支付功能,我们主要是为了拿到两个属性:一个是微信支付商户号,另一个是微信支付API秘钥。

如果是你们公司需要你开发代码接入微信支付功能,那这两个信息你们公司肯定会提前申请好提供给你的,要不然就没办法开发联调测试了,所以公司肯定会提前给你的,个人没必要太关注,当然你自己感兴趣办一个营业执照做个网站想接入的话,也可以研究一下。我这边就是自己的营业执照申请的,按照提示一步一步做,也不难。

申请好了进入一下页面拿到上边说的两个

SpringBoot实战接入微信扫一扫支付功能_第1张图片

SpringBoot实战接入微信扫一扫支付功能_第2张图片

二、了解整体过程

我们可以先看下微信官方给的接入文档

https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=2_2

打开文档我们看到,微信支付的文档还是比较多的,看完文档后觉得里边最重要的是一张官方介绍的业务流程时序图和API列表里边的几个重要的接口文档

下面我们先看下微信官方提供的时序图

SpringBoot实战接入微信扫一扫支付功能_第3张图片

仔细看完这张时序图(其中图上的红色部分是我们需要做的)

我们就能从整体上了解整个过程:

a、用户去我们的网站选择了一个商品点击购买,然后访问我们后台的接口,这个接口需要处理的逻辑是首先在我们系统里生成一条订单,然后调用微信的统一下单接口,微信的统一下单接口需要根据文档上指定的一些参数进行生成签名和加密传输,具体的签名生成规则可以参考下边代码的 WXPayUtil 类里的 createSign 方法或者可以参考官方的这个链接:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3

我们按照规则生成签名和加密串后,就可以调用微信的统一下单接口了,微信后台系统会给我们返回一个code_url链接,我们后台的代码拿到这个code_url链接后将它转成二维码图片,返给前端展示,然后用户使用微信扫一扫进行支付;

b、用户支付后,微信会回调我们的接口,告诉我们支付的结果,所以我们需要再写一个微信支付回调的接口

这个接口的大概逻辑应该是:拿到支付成功的状态去更新我们系统里订单表里的状态为已支付,然后给微信返回我们已经收到通知的信息;如果我们的接口没给微信返回已经收到通知的信息,微信那边会有一定的策略,它会每隔一段时间去调一次我们的接口试一下,直到成功或者直到达到一定的次数或者达到一定的时间,才会停止调用

还有就是我们这边超过一定的时间没收到微信的回调的话,我们也可以主动调微信的接口去查询相应的支付状态

下图是微信官方给出的回调时的步骤和注意事项

SpringBoot实战接入微信扫一扫支付功能_第4张图片

三、开始实战开发

了解完文档后,我就可以开始开发了

我们整体上需要两个接口:用户点击下单我们调微信系统生成二维码的接口微信支付的回调接口

1、下单生成二维码的接口

首先把微信支付商户号和微信支付API秘钥配置到配置文件里

SpringBoot实战接入微信扫一扫支付功能_第5张图片

然后开始开发,我们以用户购买我们网站的学习视频为例进行讲解

package com.cj.wx_pay.controller;

import com.cj.wx_pay.domain.JsonData;
import com.cj.wx_pay.domain.Video;
import com.cj.wx_pay.domain.VideoOrder;
import com.cj.wx_pay.dto.VideoOrderDto;
import com.cj.wx_pay.service.VideoOrderService;
import com.cj.wx_pay.service.VideoService;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 订单接口
 */
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {

    @Autowired
    private VideoOrderService videoOrderService;

    @Autowired
    private VideoService videoService;

    //random参数是一个随机的且唯一的字符串,作用是供前端轮询查询订单状态使用的,暂时可以先忽略测 
    //试的时候随便传入一个字符串就可以
    @GetMapping("add")
    public void saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
                          @RequestParam(value = "random",required = true)String random,
                              HttpServletRequest request,
                              HttpServletResponse response) throws Exception {
        //String ip = IpUtils.getIpAddr(request);
        //int userId = request.getAttribute("user_id");
        int userId = 1;    //临时写死的配置,实际应该从用户带的token里进行解析然后读取
        String ip = "120.25.1.43";
        VideoOrderDto videoOrderDto = new VideoOrderDto();
        videoOrderDto.setUserId(userId);
        videoOrderDto.setVideoId(videoId);
        videoOrderDto.setIp(ip);
        videoOrderDto.setRandom(random);
        String codeUrl = videoOrderService.save(videoOrderDto);
        if(codeUrl == null) {
            throw new  NullPointerException();
        }
        try{
            Cookie ck = new Cookie("trande_no",videoOrderDto.getOutTradeNo());
            ck.setMaxAge(1000);
            response.addCookie(ck);
            //生成二维码配置
            Map 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();
        }

    }

}
package com.cj.wx_pay.service.impl;

import com.cj.wx_pay.config.WeChatConfig;
import com.cj.wx_pay.domain.User;
import com.cj.wx_pay.domain.Video;
import com.cj.wx_pay.domain.VideoOrder;
import com.cj.wx_pay.dto.VideoOrderDto;
import com.cj.wx_pay.mapper.UserMapper;
import com.cj.wx_pay.mapper.VideoMapper;
import com.cj.wx_pay.mapper.VideoOrderMapper;
import com.cj.wx_pay.service.VideoOrderService;
import com.cj.wx_pay.utils.CommonUtils;
import com.cj.wx_pay.utils.HttpUtils;
import com.cj.wx_pay.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

@Service
public class VideoOrderServiceImpl implements VideoOrderService {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Autowired
    private UserMapper userMapper;

    /**
     * 下单接口
     * @param videoOrderDto
     * @return
     * @throws Exception
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(VideoOrderDto videoOrderDto) throws Exception {

        //查找视频信息
        Video video =  videoMapper.findById(videoOrderDto.getVideoId());

        //查找用户信息
        User user = userMapper.findByid(videoOrderDto.getUserId());

        //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());
        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());
        videoOrder.setRandom(videoOrderDto.getRandom());
        videoOrderMapper.insert(videoOrder);

        //微信的统一下单方法,获取codeurl
        String codeUrl = unifiedOrder(videoOrder);
        return codeUrl;

    }

    /**
     * 微信的统一下单方法
     * 生成微信支付sign,首先需要传入一个按照字典序排列好的map,然后拼接生成一个按字典序排列的参
     * 数,最后再拼接上微信支付后台拿到的秘钥参数,就可以生成一个微信统一下单接口那边需要的sign 
     * 签名了。然后把这个sign再拼接上微信需要的其他参数,并组织成一个XML格式的数据传给微信统一下 
     * 单接口就OK了
     * @return
     */
    private String unifiedOrder(VideoOrder videoOrder) throws Exception {
        //int i = 1/0;   //模拟异常
        //生成签名
        SortedMap params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        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());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
        params.put("sign",sign);

        //map转xml
        String payXml = WXPayUtil.mapToXml(params);

        //System.out.println(payXml);
        //统一下单
        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
        if(null == orderStr) {
            return null;
        }

        Map unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
        //System.out.println(new String(unifiedOrderMap.toString().getBytes("ISO-8859-1"), "UTF-8"));
        if(unifiedOrderMap != null) {
            return unifiedOrderMap.get("code_url");
        }
        return null;
    }

}

生成签名的具体步骤可以看下边的这个工具类

package com.cj.wx_pay.utils;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;

/**
 * 微信支付工具类,xml转map,map转xml,生成签名
 */
public class WXPayUtil {

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map xmlToMap(String strXML) throws Exception {
        try {
            Map data = new HashMap();
            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 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,首先需要传入一个按照字典序排列好的map,然后拼接生成一个按字典序排列的参
     * 数,最后再拼接上微信支付后台拿到的秘钥参数,就可以生成一个微信统一下单接口那边需要的sign 
     * 签名了。然后把这个sign再拼接上微信需要的其他参数,并组织成一个XML格式的数据传给微信统一下 
     * 单接口就OK了
     * @return
     */
    public static String createSign(SortedMap params, String key){
        StringBuilder sb = new StringBuilder();
        Set> es =  params.entrySet();
        Iterator> it =  es.iterator();

        //生成 类似这个的一个字符串stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
        while (it.hasNext()){
            Map.Entry entry = (Map.Entry)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 params, String key){
        String sign = createSign(params,key);

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

        return weixinPaySign.equals(sign);
    }

    /**
     * 获取有序map
     * @param map
     * @return
     */
    public static SortedMap getSortedMap(Map map){

        SortedMap sortedMap = new TreeMap<>();
        Iterator 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;
    }

}

第一个调用微信统一下单接口生成我们需要的二维码的接口就搞定了,下面我们先测试一下

SpringBoot实战接入微信扫一扫支付功能_第6张图片

可以看到访问我们的下单接口就回返回一个微信支付的二维码,我们扫一扫测试一下

SpringBoot实战接入微信扫一扫支付功能_第7张图片SpringBoot实战接入微信扫一扫支付功能_第8张图片

可以看到已经支付成功了

我们进入我们微信商家之后后台看下,钱已经到账了

SpringBoot实战接入微信扫一扫支付功能_第9张图片

用户支付后,微信需要调我们提供的接口来告诉我们用户支付成功了,然后我们去更新我们本地的订单状态为已支付,然后给用户发货啊等等操作。所以下边我们写一下微信回调我们的接口

2、微信支付回调接口

/**
     * 微信支付回调接口
     */
    @RequestMapping("/order/callback")
    public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
        InputStream inputStream =  request.getInputStream();
        //BufferedReader是包装设计模式,BufferedReader带缓冲而且可以一行一行的读,性能更高
        //stream和reader之间的转换需要一个转换流,InputStreamReader
        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 callbackMap = WXPayUtil.xmlToMap(sb.toString());
        //System.out.println(callbackMap.toString());
        SortedMap sortedMap = WXPayUtil.getSortedMap(callbackMap);
        //判断签名是否正确
        if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
            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.updateVideoOderByOutTradeNo(videoOrder);
                    if(rows == 1){ //通知微信订单处理成功
                        response.setContentType("text/xml");
                        response.getWriter().println("success");
                        return;
                    }
                }
            }
        }
        //处理失败给微信返回fail
        response.setContentType("text/xml");
        response.getWriter().println("fail");
    }

这样我们的接口就写完了

下面我们加上简单的前端代码,测试一下(前端页面的代码就不贴上来了,大家可以根据自己的实际项目情况去做)

点击购买,弹出二维码,扫一扫支付

SpringBoot实战接入微信扫一扫支付功能_第10张图片

扫一扫支付成功后,微信会回调我们的接口通知我们支付结果,根据如果微信通知我们该订单支付成功,那我们就去更新该订单的状态为已支付,然后前端页面隔3秒Ajax异步去轮询查询订单状态,等查询到的状态是已支付,页面会显示支付成功

SpringBoot实战接入微信扫一扫支付功能_第11张图片

以上就是用SpringBoot搭建项目,完成了一个接入微信支付的功能

你可能感兴趣的:(Java开发总结)