微信掃碼支付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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM