1、開發前準備php
1)企業微信前端
2)商戶號(微信支付商戶平臺帳號)java
3)wx-pay SDK, JSSDKgit
2、開發前瞭解開發文檔,以及相關概念。算法
官方文檔地址:https://work.weixin.qq.com/api/doc#90000/90135/90280。json
先來了解企業支付須要的一些相關概念:小程序
一、corpid:每一個企業都擁有惟一的corpid,獲取此信息可在管理後臺"個人企業" - "企業信息"下查看"企業ID"(須要有管理員權限)。c#
二、userid:每一個成員都有惟一的userid,即所謂的"帳號"。在管理後臺->"通信錄"->點進某個成員的詳情頁,能夠看到。api
三、agentid:每一個應用都有惟一的agentid。在管理後臺->"應用與小程序"->"應用",點進某個應用,便可以看到agentid。瀏覽器
四、secret:secret是企業應用裏面用於報障數據安全的"鑰匙",每一個應用都有一個獨立的訪問密鑰,爲了保證數據的安全,secret不能泄露。
五、access_token:access_token是企業後臺去企業微信的後臺獲取信息時的重要票據,由corpid和secret產生。全部接口在通訊時都要攜帶此信息用於驗證接口的訪問權限。
企業支付分爲:企業紅包、向員工付款、向員工收款三類。以下圖所示,這次主要詳說向員工收款。
按文檔介紹,第一步,開通企業微信專區。第二步,獲取用戶openid。第三步,添加JSAPI的權限驗證。第四步,發起向員工收款。第五步,調用支付JSAPI完成支付。
第一步就不講了,由於我沒有企業微信管理員帳號,都是讓人給配置好了,我再用的。
將上面的步驟文字轉換爲圖來看 ,過程就比較清晰明瞭了。(以下圖)
1)首先獲取access_token。
請求方式: GET(HTTPS)
請求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
須要兩個參數,corpid和corpsecret,就是前面的概念介紹的企業id和應用id。獲取到的access_token有效時間爲7200秒即兩小時,由於獲取access_token的接口有訪問次數限制,因此咱們須要把獲
取到的access_token緩存起來。這個access_token是咱們訪問其餘接口要用到的。(注意access_token可能會提早失效,邏輯中要判斷失效從新調接口獲取)。
2)獲取用戶openid
在H5頁面上發起向員工收款,須要獲取到用戶的openid信息,但在企業微信上咱們沒有直接獲取openid的API,須要先獲取到userid,再經過userid轉換爲openid。獲取useid須要構造網頁受權連接,
經過連接獲取code參數,再經過code參數獲取用戶userid信息。
2.1)構造網頁受權地址:https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect
這裏的參數以下圖說明:
這裏state是選填的,可是通常仍是帶上,企業微信會原樣返回你給的參數,能夠拿這個區分本身是經過哪一個方法去請求網頁受權的。redirect_uri須要用urlencode處理。構造好網頁受權地址後,訪問後,頁面會跳轉到redirect_uri給的地址,並帶上code和原樣返回的state參數。拿到code後咱們就能夠用code獲取userid了。(注意:code只有5分鐘的有效期,而且只能使用一次)
2.2)獲取訪問用戶身份
請求方式:GET(HTTPS)
請求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
這裏兩個參數access_token和code就是前兩步咱們獲取到的。
權限說明:跳轉的域名須徹底匹配access_token對應應用的可信域名,不然會返回50001錯誤。(這個是在第一步,開通企業微信專區那兒設置的)
請求以後,就會返回用戶身份信息了,裏面包含userid。返回數據格式:{
"errcode": 0,
"errmsg": "ok",
"UserId":"USERID",
"DeviceId":"DEVICEID"
}
2.3)userid轉換爲openid
請求方式:POST(HTTPS)
請求地址: https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN
這裏須要兩個參數一個是access_token放在url地址裏,另外一個是userid。userid不能拼接在地址後面,須要放到body裏面而且是json格式。提供一個方法來進行post請求。
1 public static JSONObject doJsonPost(String url, JSONObject jsonObject) { 2 HttpClient client = HttpClientBuilder.create().build(); 3 HttpPost post = new HttpPost(url); 4 JSONObject response = null; 5 6 try { 7 StringEntity s = new StringEntity(jsonObject.toString()); 8 s.setContentEncoding("UTF-8"); 9 s.setContentType("application/json"); 10 post.setEntity(s); 11 HttpResponse res = client.execute(post); 12 if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { 13 HttpEntity entity = res.getEntity(); 14 String result = EntityUtils.toString(entity); 15 response = JSONObject.parseObject(result); 16 } 17 } catch (Exception e) { 18 throw new RuntimeException(e); 19 } 20 return response; 21 }
請求成功返回一個JSONObject{
"errcode": 0,
"errmsg": "ok",
"openid": "oDOGms-6yCnGrRovBj2yHij5JL6E"
},裏面包含 openid信息。
3)添加JSAPI權限驗證
拿到openid後,就差很少完成了發起支付的前期準備條件,不過調用JSAPI支付以前,須要對請求的JSAPI進行權限驗證。在調用JSAPI的頁面引入jssdk,地址爲https://res2.wx.qq.com/open/js/jweixin-1.4.0.js 而後,在頁面添加以下腳本。腳本所需的參數建議經過java後臺代碼一次性獲取後傳到頁面賦值。
wx.config({ debug: true, // 開啓調試模式,調用的全部api的返回值會在客戶端alert出來(開發時設爲true,發佈時記得調爲false) appId: 'appId', // 必填,企業微信的corpID timestamp: 'timestamp', // 必填,生成簽名的時間戳 nonceStr: 'noncestr', // 必填,生成簽名的隨機串 signature: 'signature',// 必填,簽名 jsApiList: ['getBrandWCPayRequest'] });
appid就是企業微信的corpid,timestamp能夠去當前時間的時間戳,noncestr能夠用微信sdk生成一個隨機字符串,signature簽名,須要經過簽名算法來生成了,其中又須要額外獲取一個jsapi_ticket票據。
3.1)JS-SDK簽名算法
生成簽名以前必須拿到一個調用企業微信的臨時票據jsapi_ticket,jsapi_ticket有效期爲7200,且一小時內只能獲取400次,單個應用獲取不能超過100次,因此須要緩存起來備用。
請求方式:GET(HTTPS)
請求URL:https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN
就一個參數access_token。請求後返回的數據格式:
{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}
3.1.1)簽名
參與簽名的參數有四個: noncestr(隨機字符串), jsapi_ticket, timestamp(時間戳), url(當前網頁的URL, 不包含#及其後面部分)。有兩個注意點:1. 字段值採用原始值,不要進行URL轉義;2. 必須嚴格按照以下格式拼接,不可變更字段順序。
jsapi_ticket=JSAPITICKET&noncestr=NONCESTR×tamp=TIMESTAMP&url=URL
而後對拼接出來的字符串做sha1加密便可獲得signature。注意:一、url是你調用JSAPI的頁面的url,包括參數,但不包括#及其後面部分。二、noncestr和timestamp必須和以前的wx.config中的nonceStr和timestamp一致。下面貼出SHA1加密方法。
1 public static String SHA1(String des){ 2 char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 3 'a', 'b', 'c', 'd', 'e', 'f'}; 4 MessageDigest mdTemp = null; 5 try { 6 mdTemp = MessageDigest.getInstance("SHA1"); 7 try { 8 mdTemp.update(des.getBytes("UTF-8")); 9 } catch (UnsupportedEncodingException e) { 10 e.printStackTrace(); 11 } 12 } catch (NoSuchAlgorithmException e) { 13 e.printStackTrace(); 14 } 15 16 17 byte[] md = mdTemp.digest(); 18 int j = md.length; 19 char[] buf = new char[j * 2]; 20 int k = 0; 21 for (int i = 0; i < j; i++) { 22 byte byte0 = md[i]; 23 buf[k++] = hexDigits[byte0 >>> 4 & 0xf]; 24 buf[k++] = hexDigits[byte0 & 0xf]; 25 } 26 String sign= new String(buf); 27 return sign; 28 }
這些參數在java後臺代碼獲取以後,輸出到頁面去,就完成了JSAPI權限驗證。接下來就是進行統一下單了。
4)統一下單
統一下單代碼:
1 public Map<String, String> GetUnifiedOrderResult(String body, String tradeno,String totalfee,String openid){ 2 3 try { 4 Map<String , String > data = new HashMap<>(); 5 data.put("body", body);//商品描述 6 data.put("attach", body);//附加數據 7 data.put("out_trade_no",tradeno );//本身生成的訂單號,必須惟一 8 data.put("fee_type", "CNY");//貨幣種類 9 data.put("total_fee", totalfee);//付款金額 10 data.put("trade_type", "JSAPI");//交易類型 11 data.put("notify_url", "你的回調地址,不能帶參數,外網可訪問"); 12 data.put("openid", openid);//付款用戶openid 13 data.put("sign_type","MD5");//加密方式 14 unifiedOrderResult = wxpay.unifiedOrder(data); 15 return unifiedOrderResult; 16 } catch (Exception e) { 17 //throw new Exception(String.format("UnifiedOrder response error!") ); 18 } 19 return null ; 20 }
統一下單就是調用微信接口,預下單。請求地址爲:https://api.mch.weixin.qq.com/pay/unifiedorder。這個在wx-pay sdk中封裝好了,咱們只須要調用就好了。
返回爲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爲預支付交易會話標識,有效期爲2小時,咱們須要這個數據去請求JSAPI完成支付。建議這個數據保存下來,以便不當心沒有支付成功,能夠再次使用(須在兩小時內支付)。
(統一下單的微信官方文檔連接爲:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)
5)前端頁面調用JSAPI完成支付
在頁面中調用WeixinJSBridge內置對象發起支付,注意一、這個頁面和前面進行jsapi權限驗證用於前面的url是同一個頁面。二、WeixinJSBridge內置對象只在微信瀏覽器有用,在其餘瀏覽器中無效。
//調用微信JS api 支付 function onBridgeReady() { WeixinJSBridge.invoke( 'getBrandWCPayRequest', ${jsapiPrarmeters}, function (res) { if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判斷前端返回,微信團隊鄭重提示: //res.err_msg將在用戶支付成功後返回ok,但並不保證它絕對可靠。 $.toast("支付成功"); location.href ='支付成功後返回的頁面' } else{ $.toast("${Ordercode}"); $.toast("支付失敗", "forbidden"); history.go(-1); } }); } 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(); } //這裏${jsapiParameters} 我是在java服務端代碼獲取到,在傳到頁面來的,其格式以下: { "appId":"wx2421b1c4370ec43b", //企業corpid "timeStamp":"1395712654", //時間戳,自1970年以來的秒數 "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //隨機串 "package":"prepay_id=u802345jgfjsdfgsdg888", //這個就是統一下單時生成的預支付交易會話標識 "signType":"MD5", //加密方式 注意此處需與統一下單的簽名類型一致 "paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信簽名 },
這裏paySign必須按照微信要求的算法來生成。我生成jsapiParameters的方法以下
1 public String GetJsApiParameters() 2 { 3 try { 4 Long time =System.currentTimeMillis()/1000; 5 String timestamp=time.toString(); 6 Map<String,String> jsApiParam = new HashMap<>() ; 7 jsApiParam.put("appId", unifiedOrderResult.get("appid")); 8 jsApiParam.put("timeStamp", timestamp); 9 jsApiParam.put("nonceStr", unifiedOrderResult.get("nonce_str")); 10 jsApiParam.put("package", "prepay_id=" + unifiedOrderResult.get("prepay_id"));//統一下單返回的預支付交易會話標識 11 jsApiParam.put("signType", "MD5"); 12 jsApiParam.put("paySign", WXPayUtil.generateSignature(jsApiParam, "key"));//kee是微信商戶平臺的密鑰( 微信商戶平臺(pay.weixin.qq.com)-->帳戶設置-->API安全-->密鑰設置),調用微信sdk封裝好的方法進行簽名 13 14 15 Object parameters = JSON.toJSON(jsApiParam); 16 return parameters.toString(); 17 } 18 catch (Exception e){ 19 20 } 21 22 return null; 23 }
這個方法其實就是按這個順序組裝好字符串,"appId=你的企業corpid&timeStamp=12312312&nonceStr=NONCESTR&package=prepay_id=統一下單預付交易會話標識&signType=MD5&key=商戶密鑰";而後調用微信SDK的generateSignature方法進行簽名。
至此企業微信支付的主體流程就完成了。下面再說說支付完成後回調的處理,在這裏遇到一個坑。
1 @RequestMapping(value = "/wxPayNotifyUrl",method = {RequestMethod.GET,RequestMethod.POST}) 2 @ResponseBody 3 public String wxPayNotifyUrl(HttpServletRequest request, HttpServletResponse response) { 4 5 String resXml = getWXNotifyXML(request,response); 6 return payBack(resXml); 7 } 8 9 10 public String payBack(String notifyData) { 11 String xmlBack = ""; 12 Map<String, String> notifyMap = null; 13 try { 14 15 notifyMap = WXPayUtil.xmlToMap(notifyData); 16 log.info("回調數據轉map結束"+notifyMap);// 轉換成map 17 //坑就在這裏,微信文檔前面涉及到的簽名通常默認都是MD5加密,而且微信SDK驗證簽名有效性的方法默認也是用MD5加密,因此由於這個回調失敗了,經過日誌才知道是由於驗證簽名有效性不經過,後來改爲HMAC-SHA256簽名方式纔回調成功 18 if (WXPayUtil.isSignatureValid(notifyMap, WxConfig.getInstance().getKey(), WXPayConstants.SignType.HMACSHA256)) { 19 String return_code = notifyMap.get("return_code");//狀態 20 if(return_code.equals("SUCCESS")){ 21 if(notifyMap.get("result_code").equals("SUCCESS")){ 22 String out_trade_no = notifyMap.get("out_trade_no");//訂單號 23 if (out_trade_no == null) { 24 log.info("微信支付回調失敗訂單號:", notifyMap); 25 xmlBack = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文爲空]]></return_msg>" + "</xml>"; 26 return xmlBack; 27 } 28 log.info("回調成功返回的訂單號"+out_trade_no); 29 //回調成功後對訂單狀態作改變 30 String ordercode = out_trade_no; 31 Order order = orderMapper.findByOrderCode(ordercode); 32 33 if (order.getOrderstatusname().equals("未付款")) { 34 order.setOrderstatus(1); 35 order.setOrderstatusname("已付款"); 36 orderMapper.updateOrder(order); 37 } 38 xmlBack = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[SUCCESS]]></return_msg>" + "</xml> "; 39 return xmlBack; 40 } 41 } 42 } else { 43 xmlBack = "<xml>;" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文爲空]]></return_msg>" + "</xml> "; 44 log.info("微信支付回調簽名驗證失敗:"+ notifyMap); 45 return xmlBack; 46 } 47 } catch (Exception e) { 48 log.info("處理回調數據時異常", e.getStackTrace()); 49 xmlBack = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文爲空]]></return_msg>" + "</xml> "; 50 } 51 log.info("處理回調數據結束", xmlBack); 52 return xmlBack; 53 } 54 55 56 public String getWXNotifyXML(HttpServletRequest request, HttpServletResponse response){ 57 String resXml = ""; 58 try { 59 InputStream inputStream; 60 StringBuffer sb = new StringBuffer(); 61 inputStream = request.getInputStream(); 62 63 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 64 String line = null; 65 try { 66 while ((line = reader.readLine()) != null) { 67 sb.append(line + "\n"); 68 } 69 } catch (IOException e) { 70 log.info("流轉xml失敗"+e.getStackTrace()); 71 e.printStackTrace(); 72 } finally { 73 try { 74 reader.close(); 75 inputStream.close(); 76 } catch (IOException e) { 77 e.printStackTrace(); 78 } 79 } 80 resXml = sb.toString(); 81 82 } catch (Exception e) { 83 log.info("流轉xml失敗"+ e.getStackTrace()); 84 } 85 return resXml; 86 }
爲何要回調呢,由於調用JSAPI只是把付款操做給到了微信,微信返回ok也只是表示接收到這個操做指令了,並不必定真正處理成功,因此須要回調告知處理結果。在回調方法裏,必定要再次進行一次簽名,和微信返回的簽名數據進行對比,以防被串改過。
回調成功後,返回給微信的數據格式以下,若是沒有返回這個數據給微信,微信會回調屢次,咱們在代碼中要能識別同一個訂單的回調。
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg> </xml>