微信扫码支付java版完整demo

示例说明:

    微信支付接口官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
    本 demo 使用的支付方式为: 模式二

    文章最下方有可以直接运行的demo的百度云下载地址

项目结构:

项目代码:

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhangye</groupId>
    <artifactId>wxpay</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>wxpay</name>
    <packaging>jar</packaging>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <commons-lang3.version>3.7</commons-lang3.version>
        <commons-collections.version>3.2.2</commons-collections.version>
        <com.google.zxing.version>3.3.3</com.google.zxing.version>
        <fastjson.version>1.2.46</fastjson.version>
    </properties>

    <dependencies>
        <!-- mvc支持-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- 热部署模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Commons utils begin -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>${commons-collections.version}</version>
        </dependency>
        <!-- Commons utils end -->

        <!-- google 生成二维码 begin-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>${com.google.zxing.version}</version>
        </dependency>
        <!-- google 生成二维码 end-->

        <!-- JSONObject JSONArray begin -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- JSONObject JSONArray end -->



    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

controller--WxPayController

package com.zhangye.wxpay.modules.controller;

import com.alibaba.fastjson.JSONObject;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import com.zhangye.wxpay.modules.model.Order;
import com.zhangye.wxpay.modules.service.WxMenuService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信扫码支付接口
 * @date 2019/12/19
 * <p>
 * 微信支付接口官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
 * 本 demo 使用的支付方式为: 模式二
 * <p>
 * 微信扫码支付流程说明:
 * 1.需要商户生成订单
 * 2.商户调用微信统一下单接口获取二维码链接 code_url (请求参数请见官方文档)
 * 请求参数中的 notify_url 为用户支付成功后, 微信服务端回调商户的接口地址
 * 3.商户根据 code_url 生成二维码
 * 4.用户使用微信扫码进行支付
 * 5.支付成功后, 微信服务端会调用 notify_url 通知商户支付结果
 * 6.商户接到通知后, 执行业务操作(修改订单状态等)并告知微信服务端接收通知成功
 * <p>
 * 查询微信支付订单、关闭微信支付订单流程较为简单,请自行查阅官方文档
 */
@Controller
public class WxPayController {

    @Autowired
    private WxMenuService wxMenuService;

    /**
     * 二维码首页 测试用
     */
    @RequestMapping(value = {"/"}, method = RequestMethod.GET)
    public String wxPayList(Model model) {
        //商户订单号
        model.addAttribute("outTradeNo", WxUtil.mchOrderNo());
        return "/wxPayList";
    }

    /**
     * 获取订单流水号 测试用
     */
    @RequestMapping(value = {"/wxPay/outTradeNo"})
    @ResponseBody
    public String getOutTradeNo(Model model) {
        //商户订单号
        return WxUtil.mchOrderNo();
    }

    /**
     * 默认 signType 为 md5
     */
    final private String signType = WxConstants.SING_MD5;

    /**
     * 微信支付统一下单-生成二维码
     * 1.请求微信预下单接口
     * 2.根据预下单返回的 code_url 生成二维码
     * 3.将二维码 write 到前台页面
     */
    @RequestMapping(value = {"/wxPay/payUrl"})
    public void payUrl(HttpServletRequest request, HttpServletResponse response,
                       @RequestParam(value = "totalFee") int totalFee,
                       @RequestParam(value = "outTradeNo") String outTradeNo,
                       @RequestParam(value = "productId") String productId) throws Exception {
        //模拟测试订单信息
        Order order = new Order();
        order.setClintIp("123.12.12.123");
        order.setOrderNo(outTradeNo);
        order.setProductId(productId);
        order.setSubject("ESM365充值卡");
        order.setTotalFee(totalFee);
        //获取二维码链接
        String codeUrl = wxMenuService.wxPayUrl(order, signType);
        if (!StringUtils.isNotBlank(codeUrl)) {
            System.out.println("----生成二维码失败----");
            WxConfig.setPayMap(outTradeNo, "CODE_URL_ERROR");
        } else {
            //根据链接生成二维码
            WxUtil.writerPayImage(response, codeUrl);
        }
    }

    /**
     * 微信支付统一下单-通知链接
     * 1.用户支付成功后
     * 2.微信回调该方法
     * 3.商户最终通知微信已经收到结果
     */
    @RequestMapping(value = {"/wxPay/unifiedorderNotify"})
    public void unifiedorderNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {

        //商户订单号
        String outTradeNo = null;
        String xmlContent = "<xml>" +
                "<return_code><![CDATA[FAIL]]></return_code>" +
                "<return_msg><![CDATA[签名失败]]></return_msg>" +
                "</xml>";

        try {
            String requestXml = WxUtil.getStreamString(request.getInputStream());
            System.out.println("requestXml : " + requestXml);
            Map<String, String> map = WxUtil.xmlToMap(requestXml);
            String returnCode = map.get(WxConstants.RETURN_CODE);
            //校验一下 ,判断是否已经支付成功
            if (StringUtils.isNotBlank(returnCode) && StringUtils.equals(returnCode, "SUCCESS") && WxUtil.isSignatureValid(map, WxConfig.key, signType)) {
                //商户订单号
                outTradeNo = map.get("out_trade_no");
                System.out.println("outTradeNo : " + outTradeNo);
                //微信支付订单号
                String transactionId = map.get("transaction_id");
                System.out.println("transactionId : " + transactionId);
                //支付完成时间
                SimpleDateFormat payFormat = new SimpleDateFormat("yyyyMMddHHmmss");
                Date payDate = payFormat.parse(map.get("time_end"));

                SimpleDateFormat systemFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                System.out.println("支付时间:" + systemFormat.format(payDate));
                //临时缓存
                WxConfig.setPayMap(outTradeNo, "SUCCESS");

                //根据支付结果修改数据库订单状态
                //其他操作
                //......

                //给微信的应答 xml, 通过 response 回写
                xmlContent = "<xml>" +
                        "<return_code><![CDATA[SUCCESS]]></return_code>" +
                        "<return_msg><![CDATA[OK]]></return_msg>" +
                        "</xml>";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        WxUtil.responsePrint(response, xmlContent);
    }

    /**
     * 前台页面定时器查询是否已支付
     * 1.前台页面轮询
     * 2.查询订单支付状态
     */
    @RequestMapping(value = {"/wxPay/payStatus"})
    @ResponseBody
    public String payStatus(@RequestParam(value = "outTradeNo") String outTradeNo) {
        JSONObject responseObject = new JSONObject();
        //从临时缓存中取
        String outTradeNoValue = WxConfig.getPayMap(outTradeNo);
        String status = "200";
        //判断是否已经支付成功
        if (StringUtils.isNotBlank(outTradeNoValue)) {
            if (StringUtils.equals(outTradeNoValue, "SUCCESS")) {
                status = "0";
            } else if (StringUtils.equals(outTradeNoValue, "CODE_URL_ERROR")) {
                //生成二维码失败
                status = "1";
            }
        } else {
            //如果临时缓存中没有 去数据库读取
            //......
        }
        responseObject.put("status", status);
        return responseObject.toJSONString();
    }

    /**
     * 微信支付订单查询
     * 1.如果由于网络通信问题 导致微信没有通知到商户支付结果
     * 2.商户主动去查询支付结果 而后执行其他业务操作
     */
    @RequestMapping(value = {"/wxPay/orderQuery"})
    @ResponseBody
    public String orderQuery(@RequestParam(value = "orderNo") String orderNo) throws Exception {
        String result = wxMenuService.wxOrderQuery(orderNo, signType);
        return result;
    }

    /**
     * 关闭微信支付订单
     * 1.商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付
     * 2.系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口
     */
    @RequestMapping(value = {"/wxPay/closeOrder"})
    @ResponseBody
    public String closeOrder(@RequestParam(value = "orderNo") String orderNo) throws Exception {
        String result = wxMenuService.wxCloseOrder(orderNo, signType);
        return result;
    }

    //申请退款
    //查询退款
}

service--WxMenuService

package com.zhangye.wxpay.modules.service;

import com.zhangye.wxpay.modules.model.Order;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信支付接口类
 * @date 2019/12/19
 */
public interface WxMenuService {


    /**
     * 生成支付二维码URL
     *
     * @param order    订单类
     * @param signType 签名类型
     * @throws Exception
     */
    String wxPayUrl(Order order, String signType) throws Exception;

    /**
     * 查询微信订单
     *
     * @param orderNo  订单号
     * @param signType 签名类型
     * @return
     */
    String wxOrderQuery(String orderNo, String signType) throws Exception;

    /**
     * 关闭微信支付订单
     *
     * @param orderNo  订单号
     * @param signType 签名类型
     * @return
     */
    String wxCloseOrder(String orderNo, String signType) throws Exception;
}

service--impl--WxMenuServiceImpl

package com.zhangye.wxpay.modules.service.impl;

import com.zhangye.wxpay.modules.common.http.HttpsClient;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import com.zhangye.wxpay.modules.model.Order;
import com.zhangye.wxpay.modules.service.WxMenuService;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信支付实现类
 * @date 2019/12/19
 */
@Service("wxMenuService")
public class WxMenuServiceImpl implements WxMenuService {

    @Override
    public String wxPayUrl(Order order, String signType) throws Exception {
        HashMap<String, String> data = new HashMap<String, String>();
        //公众账号ID
        data.put("appid", WxConfig.appID);
        //商户号
        data.put("mch_id", WxConfig.mchID);
        //随机字符串
        data.put("nonce_str", WxUtil.getNonceStr());
        //商品描述
        data.put("body", order.getSubject());
        //商户订单号
        data.put("out_trade_no", order.getOrderNo());
        //标价币种
        data.put("fee_type", "CNY");
        //标价金额
        data.put("total_fee", String.valueOf(order.getTotalFee()));
        //用户的IP
        data.put("spbill_create_ip", order.getClintIp());
        //通知地址
        data.put("notify_url", WxConfig.unifiedorderNotifyUrl);
        //交易类型
        data.put("trade_type", "NATIVE");
        //签名类型
        data.put("sign_type", signType);
        //商品id
        data.put("product_id", order.getProductId());
        //签名 签名中加入key
        data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));

        String requestXML = WxUtil.mapToXml(data);
        String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_UNIFIEDORDER, HttpsClient.METHOD_POST, requestXML);
        //解析返回的xml
        Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
        if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
            return resultMap.get("code_url");
        }
        return null;
    }

    @Override
    public String wxOrderQuery(String orderNo, String signType) throws Exception {
        HashMap<String, String> data = new HashMap<String, String>();
        //公众账号ID
        data.put("appid", WxConfig.appID);
        //商户号
        data.put("mch_id", WxConfig.mchID);
        //随机字符串
        data.put("nonce_str", WxUtil.getNonceStr());
        //商户订单号
        data.put("out_trade_no", orderNo);
        //签名类型
        data.put("sign_type", signType);
        //签名 签名中加入key
        data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));
        String requestXML = WxUtil.mapToXml(data);
        String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_ORDERQUERY, HttpsClient.METHOD_POST, requestXML);
        //解析返回的xml
        Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
        if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
            /**
             * 订单支付状态
             * SUCCESS—支付成功
             * REFUND—转入退款
             * NOTPAY—未支付
             * CLOSED—已关闭
             * REVOKED—已撤销(刷卡支付)
             * USERPAYING--用户支付中
             * PAYERROR--支付失败(其他原因,如银行返回失败)
             */
            return resultMap.get("trade_state");
        }
        return null;
    }

    @Override
    public String wxCloseOrder(String orderNo, String signType) throws Exception {
        HashMap<String, String> data = new HashMap<String, String>();
        //公众账号ID
        data.put("appid", WxConfig.appID);
        //商户号
        data.put("mch_id", WxConfig.mchID);
        //随机字符串
        data.put("nonce_str", WxUtil.getNonceStr());
        //商户订单号
        data.put("out_trade_no", orderNo);
        //签名类型
        data.put("sign_type", signType);
        //签名 签名中加入key
        data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));
        String requestXML = WxUtil.mapToXml(data);
        String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_CLOSEORDER, HttpsClient.METHOD_POST, requestXML);
        //解析返回的xml
        Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
        if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
            /**
             * 关闭订单状态
             * SUCCESS—关闭成功
             * FAIL—关闭失败
             */
            return resultMap.get("result_code");
        }
        return null;
    }

}

common--http--HttpsClient

package com.zhangye.wxpay.modules.common.http;

import com.alibaba.fastjson.JSONObject;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import org.apache.commons.lang3.StringUtils;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.OutputStream;
import java.net.URL;


/**
 * @author zhangye
 * @version 1.0
 * @description HttpsClient类
 * @date 2019/12/19
 */
public class HttpsClient {

    /**
     * GET请求方式
     */
    public static final String METHOD_GET = "GET";
    /**
     * POST请求方式
     */
    public static final String METHOD_POST = "POST";
    /**
     * 连接超时时间
     */
    private static Integer CONNECTION_TIMEOUT = WxConfig.connectionTimeout;
    /**
     * 请求超时时间
     */
    private static Integer READ_TIMEOUT = WxConfig.readTimeout;

    /**
     * 发起https请求
     *
     * @param requestUrl    请求地址
     * @param requestMethod 请求方式(Get或者post)
     * @param postData      提交数据
     * @return JSONObject
     */
    public static JSONObject httpsRequestReturnJSONObject(String requestUrl, String requestMethod, String postData) throws Exception {
        JSONObject jsonObject = JSONObject.parseObject(HttpsClient.httpsRequestReturnString(requestUrl, requestMethod, postData));
        System.out.println("jsonObjectDate:  " + jsonObject);
        return jsonObject;
    }


    /**
     * 发起https请求
     *
     * @param requestUrl    请求地址
     * @param requestMethod 请求方式(Get或者post)
     * @param postData      提交数据
     * @return String
     */
    public static String httpsRequestReturnString(String requestUrl, String requestMethod, String postData) throws Exception {
        String response;
        HttpsURLConnection httpsUrlConnection = null;
        try {
            //创建https请求证书
            TrustManager[] tm = {new MyX509TrustManager()};
            //创建SSLContext管理器对像,使用我们指定的信任管理器初始化
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            // 创建URL对象
            URL url = new URL(requestUrl);
            // 创建HttpsURLConnection对象,并设置其SSLSocketFactory对象
            httpsUrlConnection = (HttpsURLConnection) url.openConnection();
            //设置ssl证书
            httpsUrlConnection.setSSLSocketFactory(ssf);

            //设置header信息
            httpsUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            //设置User-Agent信息
            httpsUrlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");
            //设置可接受信息
            httpsUrlConnection.setDoOutput(true);
            //设置可输入信息
            httpsUrlConnection.setDoInput(true);
            //不使用缓存
            httpsUrlConnection.setUseCaches(false);
            //设置请求方式(GET/POST)
            httpsUrlConnection.setRequestMethod(requestMethod);
            //设置连接超时时间
            if (CONNECTION_TIMEOUT > 0) {
                httpsUrlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
            } else {
                //默认10秒超时
                httpsUrlConnection.setConnectTimeout(10000);
            }
            //设置请求超时
            if (READ_TIMEOUT > 0) {
                httpsUrlConnection.setReadTimeout(READ_TIMEOUT);
            } else {
                //默认10秒超时
                httpsUrlConnection.setReadTimeout(10000);
            }
            //设置编码
            httpsUrlConnection.setRequestProperty("Charsert", WxConstants.DEFAULT_CHARSET);

            //判断是否需要提交数据
            if (StringUtils.equals(requestMethod, HttpsClient.METHOD_POST) && StringUtils.isNotBlank(postData)) {
                //讲参数转换为字节提交
                byte[] bytes = postData.getBytes(WxConstants.DEFAULT_CHARSET);
                //设置头信息
                httpsUrlConnection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
                //开始连接
                httpsUrlConnection.connect();
                //防止中文乱码
                OutputStream outputStream = httpsUrlConnection.getOutputStream();
                outputStream.write(postData.getBytes(WxConstants.DEFAULT_CHARSET));
                outputStream.flush();
                outputStream.close();
            } else {
                //开始连接
                httpsUrlConnection.connect();
            }
            response = WxUtil.getStreamString(httpsUrlConnection.getInputStream());
        } catch (Exception e) {
            throw new Exception();
        } finally {
            if (httpsUrlConnection != null) {
                // 关闭连接
                httpsUrlConnection.disconnect();
            }
        }
        return response;
    }


}

common--http--MyX509TrustManager

package com.zhangye.wxpay.modules.common.http;

import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * @author zhangye
 * @version 1.0
 * @description X509TrustManager用于实现SSL证书的安全校验
 * @date 2019/12/19
 */
public class MyX509TrustManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

common--util--SHA1

package com.zhangye.wxpay.modules.common.util;

import java.security.MessageDigest;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信SHA1算法
 * @date 2019/12/19
 */
public final class SHA1 {

    private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * 将字节并格式化
     *
     * @param bytes 原始字节
     * @return 格式化字节
     */
    private static String getFormattedText(byte[] bytes) {
        int len = bytes.length;
        StringBuilder buf = new StringBuilder(len * 2);
        // 把密文转换成十六进制的字符串形式
        for (int j = 0; j < len; j++) {
            buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
            buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
        }
        return buf.toString();
    }

    public static String encode(String str) {
        if (str == null) {
            return null;
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
            messageDigest.update(str.getBytes());
            return getFormattedText(messageDigest.digest());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

common--wx--WxConfig

package com.zhangye.wxpay.modules.common.wx;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.HashMap;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信公众号开发配置类
 * @date 2019/12/19
 */
@Component
public class WxConfig {

    /**
     * 开发者ID
     */
    public static String appID;
    @Value("${wx.appID}")
    public void setAppID(String appID) {
        this.appID = appID;
    }

    /**
     * 开发者密码
     */
    public static String appSecret;
    @Value("${wx.appSecret}")
    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }

    /**
     * 商户号
     */
    public static String mchID;
    @Value("${wx.mchID}")
    public void setMchID(String mchID) {
        this.mchID = mchID;
    }


    /**
     * API密钥
     */
    public static String key;
    @Value("${wx.key}")
    public void setKey(String key) {
        this.key = key;
    }

    /**
     * 统一下单-通知链接
     */
    public static String unifiedorderNotifyUrl;
    @Value("${wx.unifiedorder.notifyUrl}")
    public void setUnifiedorderNotifyUrl(String unifiedorderNotifyUrl) {
        this.unifiedorderNotifyUrl = unifiedorderNotifyUrl;
    }

    /**
     * 连接超时时间
     */
    public static Integer connectionTimeout;
    @Value("${https.connectionTimeout}")
    public void setConnectionTimeout(Integer connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    /**
     * 连接超时时间
     */
    public static Integer readTimeout;
    @Value("${https.readTimeout}")
    public void setReadTimeout(Integer readTimeout) {
        this.readTimeout = readTimeout;
    }

    //支付map缓存处理
    private static HashMap<String,String> payMap = new HashMap<String,String>();
    public static String getPayMap(String key) {
        return payMap.get(key);
    }
    public static void setPayMap(String key,String value) {
        payMap.put(key,value);
    }


}

common--wx--WxConstants

package com.zhangye.wxpay.modules.common.wx;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信公众号常量类
 * @date 2019/12/19
 */
public class WxConstants {

    /**
     * 默认编码
     */
    public static final String DEFAULT_CHARSET = "UTF-8";

    /**
     * 统一下单-扫描支付
     */
    public static String PAY_UNIFIEDORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";

    /**
     * 统一下单-查询订单
     */
    public static String PAY_ORDERQUERY = "https://api.mch.weixin.qq.com/pay/orderquery";

    /**
     * 统一下单-关闭订单
     */
    public static String PAY_CLOSEORDER = "https://api.mch.weixin.qq.com/pay/closeorder";

    /**
     * 请求成功返回码
     */
    public final static String ERRCODE_OK_CODE = "0";
    /**
     * 错误的返回码的Key
     */
    public final static String ERRCODE = "errcode";

    /**
     * 返回状态码
     */
    public final static String RETURN_CODE = "return_code";

    /**
     * access_token 字符串
     */
    public final static String ACCESS_TOKEN = "access_token";

    /**
     * 签名类型 MD5
     */
    public final static String SING_MD5 = "MD5";

    /**
     * 签名类型 HMAC-SHA256
     */
    public final static String SING_HMACSHA256 = "HMAC-SHA256";

}

common--wx--WxUtil

package com.zhangye.wxpay.modules.common.wx;

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 com.zhangye.wxpay.modules.common.util.SHA1;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
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.*;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author zhangye
 * @version 1.0
 * @description 微信公众号接口工具类
 * 在微信提供的 skk 中的 WXPayUtil 基础上根据自己的需求做出了一些修改
 * 微信 sdk 下载地址: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
 * @date 2019/12/19
 */
public class WxUtil {

    /**
     * 加密/校验流程如下:
     * 1. 将token、timestamp、nonce 三个参数进行字典序排序
     * 2. 将三个参数字符串拼接成一个字符串进行 sha1 加密
     * 3. 开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信
     *
     * @param token     Token验证密钥
     * @param signature 微信加密签名,signature 结合了开发者填写的 token 参数和请求中的 timestamp 参数,nonce 参数
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @return 验证成功返回:true, 失败返回:false
     */
    public static boolean checkSignature(String token, String signature, String timestamp, String nonce) {
        List<String> params = new ArrayList<String>();
        params.add(token);
        params.add(timestamp);
        params.add(nonce);
        //1. 将token、timestamp、nonce三个参数进行字典序排序
        Collections.sort(params, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        });
        //2. 将三个参数字符串拼接成一个字符串进行sha1加密
        String temp = SHA1.encode(params.get(0) + params.get(1) + params.get(2));
        //3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
        return temp.equals(signature);
    }

    /**
     * 输入流转化为字符串
     *
     * @param inputStream 流
     * @return String 字符串
     * @throws Exception
     */
    public static String getStreamString(InputStream inputStream) throws Exception {
        StringBuffer buffer = new StringBuffer();
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            inputStreamReader = new InputStreamReader(inputStream, WxConstants.DEFAULT_CHARSET);
            bufferedReader = new BufferedReader(inputStreamReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                buffer.append(line);
            }
        } catch (Exception e) {
            throw new Exception();
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close();
            }
            if (inputStreamReader != null) {
                inputStreamReader.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }
        return buffer.toString();
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String getNonceStr() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key  API密钥
     * @return 签名
     */
    public static String getSignature(final Map<String, String> data, String key, String signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals("sign")) {
                continue;
            }
            //参数值为空,则不参与签名
            if (data.get(k).trim().length() > 0) {
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);//加上key 再生成签名
        if (signType.equals(WxConstants.SING_MD5)) {
            return MD5(sb.toString()).toUpperCase();
        } else if (signType.equals(WxConstants.SING_HMACSHA256)) {
            return HMACSHA256(sb.toString(), key);
        } else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 HMACSHA256
     *
     * @param data 待处理数据
     * @param key  密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 将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;
    }

    /**
     * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。
     *
     * @param xmlStr API返回的XML格式数据
     * @return Map类型数据
     * @throws Exception
     */
    public static Map<String, String> processResponseXml(String xmlStr, String signType) throws Exception {
        String RETURN_CODE = WxConstants.RETURN_CODE;
        String return_code;
        Map<String, String> respData = xmlToMap(xmlStr);
        if (respData.containsKey(RETURN_CODE)) {
            return_code = respData.get(RETURN_CODE);
        } else {
            throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
        }

        if (return_code.equals("FAIL")) {
            return respData;
        } else if (return_code.equals("SUCCESS")) {
            //如果通信正常 验证签名
            if (isResponseSignatureValid(respData, signType)) {
                return respData;
            } else {
                throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
            }
        } else {
            throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
        }
    }


    /**
     * 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();

            String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
            documentBuilderFactory.setFeature(FEATURE, true);

            FEATURE = "http://xml.org/sax/features/external-general-entities";
            documentBuilderFactory.setFeature(FEATURE, false);

            FEATURE = "http://xml.org/sax/features/external-parameter-entities";
            documentBuilderFactory.setFeature(FEATURE, false);

            FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
            documentBuilderFactory.setFeature(FEATURE, false);

            documentBuilderFactory.setXIncludeAware(false);
            documentBuilderFactory.setExpandEntityReferences(false);
            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;
        }
    }

    /**
     * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。
     *
     * @param reqData 向wxpay post的请求数据
     * @return 签名是否有效
     * @throws Exception
     */
    private static boolean isResponseSignatureValid(final Map<String, String> reqData, String signType) throws Exception {
        // 返回数据的签名方式和请求中给定的签名方式是一致的 由于签名的时候加上了key 所以验证的时候也需要
        return isSignatureValid(reqData, WxConfig.key, signType);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data     Map类型数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, String signType) throws Exception {
        if (!data.containsKey("sign")) {
            return false;
        }
        String sign = data.get("sign");
        return getSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成支付二维码
     *
     * @param response 响应
     * @param contents url链接
     * @throws Exception
     */
    public static void writerPayImage(HttpServletResponse response, String contents) throws Exception {
        ServletOutputStream out = response.getOutputStream();
        try {
            Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
            hints.put(EncodeHintType.MARGIN, 0);
            BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, 300, 300, hints);
            MatrixToImageWriter.writeToStream(bitMatrix, "jpg", out);
        } catch (Exception e) {
            throw new Exception("生成二维码失败!");
        } finally {
            if (out != null) {
                out.flush();
                out.close();
            }
        }
    }

    /**
     * 生成商户订单号
     * 1.此方法只用在 demo 中生成假订单号
     * 2.生产环境中需要根据自己的业务做调整
     *
     * @return 测试用的订单号
     */
    public static String mchOrderNo() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String date = sdf.format(new Date());

        Random random = new Random();
        String fourRandom = String.valueOf(random.nextInt(10000));
        int randLength = fourRandom.length();
        //不足4位继续补充
        if (randLength < 4) {
            for (int remain = 1; remain <= 4 - randLength; remain++) {
                fourRandom += random.nextInt(10);
            }
        }
        return date + fourRandom;
    }

    /**
     * 返回信息给微信 商户已经接收到回调
     *
     * @param response
     * @param content  内容
     * @throws Exception
     */
    public static void responsePrint(HttpServletResponse response, String content) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/xml");
        response.getWriter().print(content);
        response.getWriter().flush();
        response.getWriter().close();
    }


}

common--model--Order

package com.zhangye.wxpay.modules.model;

import java.io.Serializable;

/**
 * @author zhangye
 * @version 1.0
 * @description 商户订单实体类(测试)
 * @date 2019/12/19
 */
public class Order implements Serializable {
    private String productId;//商品id
    private String subject;//商品名称
    private String orderNo;//订单号
    private String clintIp;//客户端ip
    private int totalFee;//订单金额 以分为单位

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getOrderNo() {
        return orderNo;
    }

    public void setOrderNo(String orderNo) {
        this.orderNo = orderNo;
    }

    public String getClintIp() {
        return clintIp;
    }

    public void setClintIp(String clintIp) {
        this.clintIp = clintIp;
    }

    public int getTotalFee() {
        return totalFee;
    }

    public void setTotalFee(int totalFee) {
        this.totalFee = totalFee;
    }
}

 

resources--application.properties

# ---微信扫码支付开始
#开发者ID
wx.appID=wxab8acb865bb1637e
#开发者密码
wx.appSecret=86ae4a77893342f7568947e243c84d9aa
#商户号
wx.mchID=11473623
#API密钥,key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
wx.key=2ab9071b06b9f739b950ddb41db2690d


#内网穿透的链接(由于测试demo没有外网地址及域名,所以使用工具穿透)
#穿透工具使用方法请见 /resources/natapp/readme.md
#生产环境下将此链接修改为正确的域名即可
intranet.penetrateUrl=http://vaiiak.natappfree.cc
#统一下单-通知链接
wx.unifiedorder.notifyUrl=${intranet.penetrateUrl}/wxPay/unifiedorderNotify
# ---微信扫码支付结束


#连接超时时间
https.connectionTimeout=15000
#请求超时时间
https.readTimeout=15000
spring.mvc.view.prefix=/templates
spring.mvc.view.suffix=.html
spring.mvc.static-path-pattern=/**
#禁止thymeleaf缓存(建议:开发环境设置为false,生成环境设置为true)
spring.thymeleaf.cache=false

resources--templates--wxPayList.html

<html>
<head>
    <title>微信支付测试DEMO</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <script type="text/javascript" src="/js/jquery/jquery-3.3.1.min.js"></script>
    <script type="text/javascript" src="/js/jquery/jquery.timers-1.2.js"></script>
    <script type='text/javascript'>

        $(function () {
            getOutTradeNo();
        });

        function save() {
            var outTradeNo = $("#outTradeNo").val(); //订单号
            var productId = "00001"; //商品id
            var totalFee = $("#totalFee").val(); //订单金额 单位为分
            //生成二维码
            $("#payImg").attr("src", '/wxPay/payUrl' + "?totalFee=" + totalFee + "&outTradeNo=" + outTradeNo + "&productId=" + productId);

            //轮询获取支付状态
            $('body').everyTime('2s', 'payStatusTimer', function () {
                $.ajax({
                    type: "POST",
                    url: '/wxPay/payStatus?outTradeNo=' + outTradeNo + "&random=" + new Date().getTime(),
                    contentType: "application/json",
                    dataType: "json",
                    async: "false",
                    success: function (json) {
                        if (json != null && json.status == 0) {
                            alert("支付成功!");
                            $('body').stopTime('payStatusTimer');
                            return false;
                        } else if (json.status == 1) {
                            alert("生成二维码失败!")
                            $('body').stopTime('payStatusTimer');
                            return false;
                        }
                    },
                    error: function (XMLHttpRequest, textStatus, errorThrown) {
                        alert("服务器错误!状态码:" + json.status);
                        // 状态
                        console.log(json.readyState);
                        // 错误信息
                        console.log(json.statusText);
                        return false;
                    }
                })
            });


        }

        //获取测试订单流水号
        function getOutTradeNo() {
            $.ajax({
                type: "POST",
                url: '/wxPay/outTradeNo',
                success: function (json) {
                    if (json != null) {
                        $("h3").html(json);
                        $("#outTradeNo").val(json);
                    } else {
                        alert("获取流水号失败!");
                    }
                    return false;
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    alert("服务器错误!状态码:" + XMLHttpRequest.status);
                    // 状态
                    console.log(XMLHttpRequest.readyState);
                    // 错误信息
                    console.log(textStatus);
                    return false;
                }
            });
        }

        //查询订单
        function queryOrder() {
            var orderNo = $("#orderNo").val();
            $.ajax({
                type: "POST",
                url: '/wxPay/orderQuery?orderNo=' + orderNo,
                success: function (data) {
                    alert(data);
                    return false;
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    alert("服务器错误!状态码:" + XMLHttpRequest.status);
                    // 状态
                    console.log(XMLHttpRequest.readyState);
                    // 错误信息
                    console.log(textStatus);
                    return false;
                }
            });
        }

        //关闭订单
        function closeOrder() {
            var orderNo = $("#orderNo2").val();
            $.ajax({
                type: "POST",
                url: '/wxPay/closeOrder?orderNo=' + orderNo,
                success: function (data) {
                    alert(data);
                    return false;
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    alert("服务器错误!状态码:" + XMLHttpRequest.status);
                    // 状态
                    console.log(XMLHttpRequest.readyState);
                    // 错误信息
                    console.log(textStatus);
                    return false;
                }
            });
        }

    </script>

</head>
<body>
<p>订单流水号:
<h3></h3></p>
支付金额:<input id="totalFee" type="text" value="1"/>&nbsp;
<button type="button" onclick="save();">生成二维码</button>
<input id="outTradeNo" type="hidden" value="${outTradeNo}"/>
&nbsp;&nbsp;<img id="payImg" width="300" height="300">
<br/><br/><br/>

<p>查询订单:
订单号<input id="orderNo" type="text" value=""/>
<button type="button" onclick="queryOrder();">查询订单</button>
<br/>

<p>关闭订单:
订单号<input id="orderNo2" type="text" value=""/>
<button type="button" onclick="closeOrder();">关闭订单</button>
</body>
</html>

其他内容为jquery文件和natapp的使用方法:(如果不需要使用natapp测试,上面的代码基本上是全部的demo实现代码了)

 

最后附上整个demo百度云下载的地址:

链接:https://pan.baidu.com/s/1C-hi_TTxAxpiWF_5T_PDIw
提取码:p1yb

posted @ 2020-01-03 11:28  青衫仗剑  阅读(4230)  评论(2编辑  收藏  举报