寫這篇文章的目的有2個,一是自己的項目剛開發完微信支付功能,趁熱回個爐溫習一下,二也是幫助像我這樣對微信支付不熟悉,反復看了多天文檔還是一知半解,原理都沒摸清,更不要說實現了。本以為網上的微信開發教程會和“java的重寫與重載”一樣鋪天蓋地,可搜出來的結果,要么是PHP的教程(微信支付官網推薦就是PHP),要么星星點點就那么幾篇,想對比的看看思路都成問題,官網下載的JAVA-SDK-DEMO也恕我技術低下,看的糊里糊塗。等自己開發完的那一刻,才豁然開朗,才知道走通完支付這條路的過程走了多少彎路,我是第一次接觸支付,想必大部分能看這篇文章的兄弟也是被微信官方文檔給繞的出不來才出此下策,內容有誤請指正。好了這回真正的正題了:
步驟一:獲取微信支付四大參數
首先要想支持微信支付,必須擁有兩個賬號:①微信公眾已認證的服務號,並且需要開通微信支付該能(必須是企業才有資格申請,請你找你家產品去申請吧),②微信商戶平台賬號;這兩個賬號一個不能少。此處已默認你已有上兩個賬號。
此處是賬號模板,請參考:
微信公眾平台:賬戶:con*******om 登錄密碼 ******
公眾APPID:wx15*********a8
APPSECEPT : c210***************892d7
微信商戶平台:賬戶:149**********6742 登錄密碼:******
商戶ID:14******42
API密鑰:5d5************b35b
其中比較不好找的是商戶的API密鑰:在商戶平台的賬戶中心下:需要用戶自行下載證書及安裝,(略)
至此我們需要的APPID(appid),APPSECEPT(appsecret),商戶ID(mch_id),API密鑰(paternerKey),四個重要參數已拿到,括號中是我們代碼所用的變量名稱請提前熟悉。
步驟二:平台配置
1.配置支付目錄:商戶平台:
配置此目錄是代碼中“微信支付”所在頁面的地址,可以是目錄不一定是全路徑-如http://www.wangtao.com/order/-----此一級域名需要ICP備案。
點擊添加
2.配置授權域名:微信公眾平台:
支付過程需要獲取用戶openid,必須經過網頁授權配置才可以,要不然獲取不到openid。
點擊設置,按說明設置
步驟三:開發流程:
微信支付原理(說白了就是調用官方文檔的“統一下單”接口,之后將微信服務器返回的參數做個加工后,返回到前台(JSP頁面),就OK了。咱們要做的就是想方設法的湊齊統一下單的所有參數“而已”,“而已”,“而已”,這個而已也就是最大的挑戰)。所有參數解釋請參考:官方文檔:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1;
咱們只考慮必填參數,其他參數可以就看你的了。
先來看看所有參數:
其中的必填參數有:
1. appid APPID (已有)
2. mch_id 商戶ID (已有)
3. nonce_str 隨機字符串
4. sign 簽名
5. body 所支付的名稱
6. out_trade_no 咱們自己所提供的訂單號,需要唯一
7. total_fee 支付金額
8. spbill_create_ip IP地址
9. notify_url 回調地址
10. trade_type 支付類型
11. openid 支付人的微信公眾號對應的唯一標識
只要把這11個湊齊就齊活,現在咱們從第3個開始一個一個的獲取;在這之前先從官網把公眾號支付的sdk下載下來,如圖
主要是用其中的WXPayUtil工具類中的一些方法。當然其他的類我看不懂,要是看懂了,就不至於這么費勁了。
好了開始咱們的取值之旅了:
1. appid APPID (已有)
2. mch_id 商戶ID (已有)
3. nonce_str 隨機字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
4. sign 簽名 用WXPayUtil中的generateSignature(finalMap<String, String> data, String key)方法,data是將除了sign外,其他10個參數放到map中,key是四大配置參數中的API秘鑰(paternerKey)(這里不要着急管它,最后處理它);
5. body 所支付的名稱
6. out_trade_no 自己后台生成的訂單號,只要保證唯一就好:如“2018013000001”
7. total_fee 支付金額 單位:分,為了測試此值給1,表示支付1分錢
8. spbill_create_ip IP地址 網上很多ip的方法,自己找,此處測試給“127.0.0.1”
9. notify_url 回調地址:這是微信支付成功后,微信那邊會帶着一大堆參數(XML格式)請求這個地址多次,這個地址做我們業務處理如:修改訂單狀態,贈送積分等。Ps:支付還沒成功還想這么遠干嘛,最后再說。地址要公網可以訪問。
10. trade_type 支付類型 咱們是公眾號支付此處給“JSAPI”
11. openid 支付人的微信公眾號對應的唯一標識,每個人的openid在不同的公眾號是不一樣的,這11個參數里,最費勁的就是他了,其他的幾乎都已經解決,現在開發得到這個參數。
獲得openid的部分內容應該不屬於微信支付的范疇,屬於微信公眾號網頁授權的東西,詳情請參考微信網頁授權:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
獲得openid步驟:
第一步:用戶同意授權,獲取code
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
注意:1. redirect_uri參數:授權后重定向的回調鏈接地址, 請使用 urlEncode 對鏈接進行處理。
2. scope:用snsapi_base 。
通過此鏈接可以獲取code,可以在一個空頁面設置一個a標簽,鏈接至其redirect_uri的地址。點擊a標簽,即可鏈接到redirect_uri的地址,並攜帶code。
<a href="https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx15c*********&redirect_uri=http%3a%2f%2fwww.***.com%2fpay.jsp&response_type=code&cope=snsapi_base#wechat_redirect">去支付頁面pay.jsp並攜帶code</a>
第二步:通過code換取網頁授權access_token(其實微信支付就沒有必要獲取access_token了,咱們只要其中openid,不是要用戶信息,此步結果已經就含有咱們需要的openid了)
獲取code后,請求以下鏈接獲取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
上一步的code有了,對於此鏈接的參數就容易了。可是在頁面上如何處理是個問題,我是在pay.jsp頁面加載完成后將獲取code當做參數傳異步到后台,在后台中用http相關類發送get請求(可以自行網上查找)。返回的JSON結果為:
{ "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID",//就是它,只要這個值 "scope":"SCOPE" }
好了,access_token是有了,不過咱們不關心它,咱們關心的是openid,有了它一就回到咱們“統一下單”接口里,所有的參數已經就位就等發送了。在回顧下11個必填參數:
1. appid APPID (已有)
2. mch_id 商戶ID (已有)
3. nonce_str 隨機字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
4. sign 簽名 用WXPayUtil中的publicstatic String generateSignature(final Map<String, String> data, Stringkey)方法,data是將除了sign外,其他10個參數放到map中,key是四大配置參數中的API秘鑰(paternerKey)(此時可以處理它了,不過其他10個參數都有了,它就easy了,先new一個map,依次put其他10個參數,就可以用generateSignature方法了,得到了sign后,不要忘記再將sign put到只有10個參數的map中,這樣才能湊齊最后的第11個參數。准備召喚神龍吧。);
5. body 所支付的名稱
6. out_trade_no 自己后台生成的訂單號,只要保證唯一就好:如“2018013000001”
7. total_fee 支付金額 單位:分,為了測試此值給1,表示支付1分錢
8. spbill_create_ip IP地址 網上很多ip的方法,自己找,此處測試給“127.0.0.1”
9. notify_url 回調地址:這是微信支付成功后,微信那邊會帶着一大堆參數(XML格式)請求這個地址多次,這個地址做我們業務處理如:修改訂單狀態,贈送積分等。Ps:支付還沒成功還想這么遠干嘛,最后再說。地址要公網可以訪問。
10. trade_type 支付類型 咱們是公眾號支付此處給“JSAPI”
11. openid (已有)
好了,准備工作完成,開始發送POST請求吧,上面提到網上找到的get請求的方法,此處用到post請求的方法,請求微信"統一下單接口https://api.mch.weixin.qq.com/pay/unifiedorder。發送前先用WXPayUtil工具類中的public static String mapToXml(Map<String,String> data)方法將有11個參數的map轉成XML格式。發送后會返回String類型的返回值,如果你夠幸運的話應該會得到XML的字符串:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str> <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid> <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign> <result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id> <trade_type><![CDATA[JSAPI]]></trade_type> </xml>
如果你得到了以上的字符串,那么先恭喜你,堅持看到這,說明你的耐心還是不錯的,因為“統一下單”接口調用完畢,可是並沒有什么實際的效果,因為微信里想出現支付的界面是在前台完成的現在咱們還在后台玩耍,前面提到的我是頁面加載完成時異步到后台的,咱們要返回異步的結果了,好了趁熱繼續吧。“統一下單”這么費勁的完成其實搞那么麻煩,就是為了得到上面紅色的prepay_id(丫的,就這么一個參數給咱們搞的都想說***了)。
先用WXPayUtil類中的public static Map<String, String> xmlToMap(String strXML)方法,將剛才返回的XML格式的字符串轉成map(為了方便取值)。map.get(“prepay_id”)就得到了prepay_id的值(比如得到的是:“wx2018…250…9981…666”),記住它,先保留此值。
看看前台都需要接收哪些值吧。
6個參數,咱們還是一個一個分析:
1. appId:四大參數之一的APPID;
2. timestamp:時間戳(newDate()即可)
3. nonceStr:隨機字符串,再次用WXPayUtil中的generateNonceStr()即可;
4. package:就tm是它用到了prepay_id,但是還不是直接取值,還非要固定格式的,值的格式例如:”prepay_id= wx2018…250…9981…666”
5. signType:寫MD5就好
6. paySign:又來了還是簽名算法 ,按照上面的方法,用WXPayUtil中的publicstatic String generateSignature(final Map<String, String> data, Stringkey)方法,data是將除了paySign外,其他5個參數放到map中,key是四大配置參數中的API秘鑰(paternerKey),得到了paySign后,不要忘記再將paySign put到只有5個參數的map中,這樣才能湊齊最后的第6個參數。);
注意:此處有個小bug,很多人會被坑的很慘,不注意就掉坑里,我是掉進去了,就是最關鍵的第4個參數package,眼熟不眼熟,這tm是JAVA的關鍵字,不能用來當變量名。
所有的參數有了,返回給前端的方法有很多,簡易用springMVC的@ResponseBody注解,即可將這個有6個參數的map按json格式傳給前端。好了,后台工作完成。
前端的工作就容易多了,格式比較固定因為是微信固定格式,所以直接貼出我的代碼,你只要更換觸發支付的事件和異步的地址即可.
前端簡單來說:1.一個空jsp頁面上有個a標簽,用來獲取code,並跳轉到pay.jsp(上面提到過)。
2.pay.jsp中需要異步到后台需要帶code參數,pay.jsp中頁面的地址上帶着code,想獲取code的方法很多,拋磚引引玉:(定義一個按鈕,按鈕上綁定一個code的屬性值是頁面鏈接的code的值,用EL表達式取的參數值,點擊按鈕觸發點擊事件)。
3.接收后台傳過來值,調用固定方法。
Pay.jsp中內容只有一個”微信支付”的按鈕,和js的代碼,以下是js內容(獲取code方法可以修改),其它內容不要修改
<!—pay.jsp中點擊”微信支付”按鈕執行pay()方法> <input id="code"type="button" value="微信支付"onclick="pay()" code="${param.code }"/> <script type="text/javascript"> var appId,timeStamp,nonceStr,package,signType,paySign; function pay(){ var code = $("#code").attr("code");//頁面鏈接上的code參數 if(code){ var url = "http://異步地址?code="+code+"; $.get(url,function(result) { appId = result.appId; timeStamp = result.timeStamp; nonceStr = result.nonceStr; package = result.package; signType = result.signType; paySign = result.paySign; if (typeof WeixinJSBridge == "undefined") { if (document.addEventListener) { document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); } else if (document.attachEvent) { document.attachEvent('WeixinJSBridgeReady', onBridgeReady); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); } } else { onBridgeReady(); } }); } else { alert(“服務器錯誤”) } } function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId":appId, //公眾號名稱,由商戶傳入 "timeStamp":timeStamp, //時間戳,自1970年以來的秒數 "nonceStr":nonceStr, //隨機串 "package":package, "signType":signType, //微信簽名方式: "paySign":paySign //微信簽名 }, function(res){ if(res.err_msg == "get_brand_wcpay_request:ok" ) { console.log('支付成功'); //支付成功后跳轉的頁面 }else if(res.err_msg == "get_brand_wcpay_request:cancel"){ console.log('支付取消'); }else if(res.err_msg == "get_brand_wcpay_request:fail"){ console.log('支付失敗'); WeixinJSBridge.call('closeWindow'); } //使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功后返回ok,但並不保證它絕對可靠。 }); } </script>
以下是后台部分
/** * @Description 微信瀏覽器內微信支付/公眾號支付(JSAPI) * @param request * @param code * @return Map */ @RequestMapping(value="orders", method = RequestMethod.GET) @ResponseBody public Map orders(HttpServletRequest request,String code) { try { //頁面獲取openId接口 String getopenid_url = https://api.weixin.qq.com/sns/oauth2/access_token; String param= "appid="+你appid+"&secret="+你secret+"&code="+code+"&grant_type=authorization_code"; //向微信服務器發送get請求獲取openIdStr String openIdStr = HttpRequest.sendGet(getopenid_url, param); JSONObject json = JSONObject.parseObject(openIdStr);//轉成Json格式 String openId = json.getString("openid");//獲取openId //拼接統一下單地址參數 Map<String, String> paraMap = new HashMap<String, String>(); //獲取請求ip地址 String ip = request.getHeader("x-forwarded-for"); if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ ip = request.getHeader("Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ ip = request.getHeader("WL-Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ ip = request.getRemoteAddr(); } if(ip.indexOf(",")!=-1){ String[] ips = ip.split(","); ip = ips[0].trim(); } paraMap.put("appid", 你appid); paraMap.put("body", "堯舜商城-訂單結算"); paraMap.put("mch_id", 你mchId); paraMap.put("nonce_str", WXPayUtil.generateNonceStr()); paraMap.put("openid", openId); paraMap.put("out_trade_no", 你的訂單號);//訂單號 paraMap.put("spbill_create_ip", ip); paraMap.put("total_fee",”1”); paraMap.put("trade_type", "JSAPI"); paraMap.put("notify_url",www.*******.com/***/**);// 此路徑是微信服務器調用支付結果通知路徑隨意寫 String sign = WXPayUtil.generateSignature(paraMap, paternerKey); paraMap.put("sign", sign); String xml = WXPayUtil.mapToXml(paraMap);//將所有參數(map)轉xml格式 // 統一下單 https://api.mch.weixin.qq.com/pay/unifiedorder String unifiedorder_url = https://api.mch.weixin.qq.com/pay/unifiedorder; String xmlStr = HttpRequest.sendPost(unifiedorder_url, xml);//發送post請求"統一下單接口"返回預支付id:prepay_id //以下內容是返回前端頁面的json數據 String prepay_id = "";//預支付id if (xmlStr.indexOf("SUCCESS") != -1) { Map<String, String> map = WXPayUtil.xmlToMap(xmlStr); prepay_id = (String) map.get("prepay_id"); } Map<String, String> payMap = new HashMap<String, String>(); payMap.put("appId", appid); payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp()+""); payMap.put("nonceStr", WXPayUtil.generateNonceStr()); payMap.put("signType", "MD5"); payMap.put("package", "prepay_id=" + prepay_id); String paySign = WXPayUtil.generateSignature(payMap, paternerKey); payMap.put("paySign", paySign); return payMap; } catch (Exception e) { e.printStackTrace(); } return null; }
以下是網上找的一個發送get和post的類.僅供參考:提前導入相關jar包可網上查找(很容易)
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.Map; public class HttpRequest { /** * 向指定URL發送GET方法的請求 * * @param url * 發送請求的URL * @param param * 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。 * @return URL 所代表遠程資源的響應結果 */ public static String sendGet(String url, String param) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; System.out.println(urlNameString); URL realUrl = new URL(urlNameString); // 打開和URL之間的連接 URLConnection connection = realUrl.openConnection(); // 設置通用的請求屬性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 建立實際的連接 connection.connect(); // 獲取所有響應頭字段 Map<String, List<String>> map = connection.getHeaderFields(); // 遍歷所有的響應頭字段 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } // 定義 BufferedReader輸入流來讀取URL的響應 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("發送GET請求出現異常!" + e); e.printStackTrace(); } // 使用finally塊來關閉輸入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } /** * 向指定 URL 發送POST方法的請求 * * @param url * 發送請求的 URL * @param param * 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。 * @return 所代表遠程資源的響應結果 */ public static String sendPost(String url, String param) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); // 打開和URL之間的連接 URLConnection conn = realUrl.openConnection(); // 設置通用的請求屬性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 發送POST請求必須設置如下兩行 conn.setDoOutput(true); conn.setDoInput(true); // 獲取URLConnection對象對應的輸出流 out = new PrintWriter(conn.getOutputStream()); // 發送請求參數 out.print(param); // flush輸出流的緩沖 out.flush(); // 定義BufferedReader輸入流來讀取URL的響應 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("發送 POST 請求出現異常!"+e); e.printStackTrace(); } //使用finally塊來關閉輸出流、輸入流 finally{ try{ if(out!=null){ out.close(); } if(in!=null){ in.close(); } } catch(IOException ex){ ex.printStackTrace(); } } return result; } }
----------------------------------------------------------------------------------
以下是微信支付成功后,向自己項目發送請求,自己來做一些業務處理。
/** * @ClassName WxPayController * @Description 微信支付成功后回調次接口 * @author wtao wangtao@eyaoshun.com * @date 2018年1月11日 下午3:10:59 */ //回調路徑是自己在之前已經填寫過的 @RequestMapping("/pay/") @Controller public class WxPayController { @Autowired private OrdersService ordersService; @Autowired private AccountService accountService; @Autowired private PointService pointService; @RequestMapping("callback") public String callBack(HttpServletRequest request,HttpServletResponse response){ //System.out.println("微信支付成功,微信發送的callback信息,請注意修改訂單信息"); InputStream is = null; try { is = request.getInputStream();//獲取請求的流信息(這里是微信發的xml格式所有只能使用流來讀) String xml = WXPayUtil.inputStream2String(is, "UTF-8"); Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);//將微信發的xml轉map if(notifyMap.get("return_code").equals("SUCCESS")){ if(notifyMap.get("result_code").equals("SUCCESS")){ String ordersSn = notifyMap.get("out_trade_no");//商戶訂單號 String amountpaid = notifyMap.get("total_fee");//實際支付的訂單金額:單位 分 BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);//將分轉換成元-實際支付金額:元 //String openid = notifyMap.get("openid"); //如果有需要可以獲取 //String trade_type = notifyMap.get("trade_type"); /*以下是自己的業務處理------僅做參考 * 更新order對應字段/已支付金額/狀態碼 */ Orders order = ordersService.selectOrdersBySn(ordersSn); if(order != null) { order.setLastmodifieddate(new Date()); order.setVersion(order.getVersion().add(BigDecimal.ONE)); order.setAmountpaid(amountPay);//已支付金額 order.setStatus(2L);//修改訂單狀態為待發貨 int num = ordersService.updateOrders(order);//更新order String amount = amountPay.setScale(0, BigDecimal.ROUND_FLOOR).toString();//實際支付金額向下取整-123.23--123 /* * 更新用戶經驗值 */ Member member = accountService.findObjectById(order.getMemberId()); accountService.updateMemberByGrowth(amount, member); /* * 添加用戶積分數及添加積分記錄表記錄 */ pointService.updateMemberPointAndLog(amount, member, "購買商品,訂單號為:"+ordersSn); } } } //告訴微信服務器收到信息了,不要在調用回調action了========這里很重要回復微信服務器信息用流發送一個xml即可 response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); is.close(); } catch (Exception e) { e.printStackTrace(); } return null; } }
聲明:此文章為轉載文章