示例說明:
微信支付接口官方文檔地址: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"/> 分 <button type="button" onclick="save();">生成二維碼</button> <input id="outTradeNo" type="hidden" value="${outTradeNo}"/> <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