微信支付接口的調用
在上周的博客中我講了調用支付寶的接口實現支付,這周我們繼續來講一講如何調用微信的支付接口。
在講之前依然先給出微信的官方接口說明。官方的場景介紹圖如下:
其實pc端的支付場景都差不多,用戶點擊按鈕,生成一個二維碼,微信掃碼之后支付成功。要調用微信的接口,首先你需要引入微信支付的jar包,如下:
<dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version> </dependency>
我把微信官方的調用示例拿來改了一下,成為了下面這個工具類:
package com.example.ffmpeg; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.wxpay.sdk.WXPay; public class WXService { private static Logger logger = LoggerFactory.getLogger(WXService.class); private WXPay wxpay; private WXPayConfigImpl config; private static WXService INSTANCE; private WXService() throws Exception { config = WXPayConfigImpl.getInstance(); wxpay = new WXPay(config); } public static WXService getInstance() throws Exception { if (INSTANCE == null) { synchronized (WXPayConfigImpl.class) { if (INSTANCE == null) { INSTANCE = new WXService(); } } } return INSTANCE; } /** * 微信下單接口 * * @param out_trade_no * @param body * @param money * @param applyNo * @return */ public String doUnifiedOrder(String out_trade_no, String body, Double money, String applyNo) { String amt = String.valueOf(money * 100); HashMap<String, String> data = new HashMap<String, String>(); data.put("body", body); data.put("out_trade_no", out_trade_no); data.put("device_info", "web"); data.put("fee_type", "CNY"); data.put("total_fee", amt.substring(0, amt.lastIndexOf("."))); data.put("spbill_create_ip", config.getSpbillCreateIp()); data.put("notify_url", config.getNotifUrl()); data.put("trade_type", config.getTradeType()); data.put("product_id", applyNo); System.out.println(String.valueOf(money * 100)); // data.put("time_expire", "20170112104120"); try { Map<String, String> r = wxpay.unifiedOrder(data); logger.info("返回的參數是" + r); return r.get("code_url"); } catch (Exception e) { e.printStackTrace(); logger.info(e.getMessage()); return null; } } /** * 退款 已測試 */ public void doRefund(String out_trade_no, String total_fee) { logger.info("退款時的訂單號為:" + out_trade_no + "退款時的金額為:" + total_fee); String amt = String.valueOf(Double.parseDouble(total_fee) * 100); logger.info("修正后的金額為:" + amt); logger.info("最終的金額為:" + amt.substring(0, amt.lastIndexOf("."))); HashMap<String, String> data = new HashMap<String, String>(); data.put("out_trade_no", out_trade_no); data.put("out_refund_no", out_trade_no); data.put("total_fee", amt.substring(0, amt.lastIndexOf("."))); data.put("refund_fee", amt.substring(0, amt.lastIndexOf("."))); data.put("refund_fee_type", "CNY"); data.put("op_user_id", config.getMchID()); try { Map<String, String> r = wxpay.refund(data); logger.info("退款操作返回的參數為" + r); } catch (Exception e) { e.printStackTrace(); } } /** * 微信驗簽接口 * * @param out_trade_no * @param body * @param money * @param applyNo * @return * @throws DocumentException */ public boolean checkSign(String strXML) throws DocumentException { SortedMap<String, String> smap = new TreeMap<String, String>(); Document doc = DocumentHelper.parseText(strXML); Element root = doc.getRootElement(); for (Iterator iterator = root.elementIterator(); iterator.hasNext();) { Element e = (Element) iterator.next(); smap.put(e.getName(), e.getText()); } return isWechatSign(smap,config.getKey()); } private boolean isWechatSign(SortedMap<String, String> smap,String apiKey) { StringBuffer sb = new StringBuffer(); Set<Entry<String, String>> es = smap.entrySet(); Iterator<Entry<String, String>> it = es.iterator(); while (it.hasNext()) { Entry<String, String> entry = it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if (!"sign".equals(k) && null != v && !"".equals(v) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + apiKey); /** 驗證的簽名 */ String sign = MD5Util.MD5Encode(sb.toString(), "utf-8").toUpperCase(); /** 微信端返回的合法簽名 */ String validSign = ((String) smap.get("sign")).toUpperCase(); return validSign.equals(sign); } }
我把微信的下單,退款,驗簽操作封裝到了WXService 這個工具類里面。這個類需要兩個成員變量wxpay和config,分別是WXPay和WXPayConfigImpl的實例化對象。WXPay是引自微信的工具包。WXPayConfigImpl則是自己寫的一個類,代碼如下:
package com.example.ffmpeg; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import com.github.wxpay.sdk.WXPayConfig; public class WXPayConfigImpl implements WXPayConfig{ private byte[] certData; private static WXPayConfigImpl INSTANCE; private WXPayConfigImpl() throws Exception{ String certPath = "F:\\weixin\\apiclient_cert.p12"; File file = new File(certPath); InputStream certStream = new FileInputStream(file); this.certData = new byte[(int) file.length()]; certStream.read(this.certData); certStream.close(); } public static WXPayConfigImpl getInstance() throws Exception{ if (INSTANCE == null) { synchronized (WXPayConfigImpl.class) { if (INSTANCE == null) { INSTANCE = new WXPayConfigImpl(); } } } return INSTANCE; } public String getAppID() { return "你的appid"; } public String getMchID() { return "你的商戶id"; } public String getKey() { return "你設置的key值"; } public String getNotifUrl() { return "微信通知回調的url接口"; } public String getTradeType() { return "NATIVE"; } public InputStream getCertStream() { ByteArrayInputStream certBis; certBis = new ByteArrayInputStream(this.certData); return certBis; } public int getHttpConnectTimeoutMs() { return 2000; } public int getHttpReadTimeoutMs() { return 10000; } // IWXPayDomain getWXPayDomain() { // return WXPayDomainSimpleImpl.instance(); // } public String getPrimaryDomain() { return "api.mch.weixin.qq.com"; } public String getAlternateDomain() { return "api2.mch.weixin.qq.com"; } public int getReportWorkerNum() { return 1; } public int getReportBatchSize() { return 2; } public String getSpbillCreateIp() { // TODO Auto-generated method stub return "192.168.1.1"; } }
可以看到,這個類實現了微信提供的WXPayConfig這個接口,里面封裝了一些方法,主要是返回微信接口所需要的一些參數。值得注意的是,這里需要去讀取一個文件名叫apiclient_cert.p12的證書文件。這個證書文件你可以登錄微信的商戶平台。在這里去下載你所需要的證書。WXPayConfigImpl 在構造方法里面去讀取這個文件,所以構造方法拋了異常。因為構造器拋出異常,所以這里沒有采用靜態內部類而是采用雙檢鎖的方式去實現單例。
回到WXService這個類中,代碼往下走,在WXService的構造器中對config和wxpay進行了實例化。接下來同樣是用雙檢鎖的方式實現的單例。往下走,微信的下單接口,分別傳入out_trade_no(外部訂單號),body(商品描述), money(付款金額), applyNo(對應微信的product_id:商品id,由商戶自定義)四個參數。進入方法后第一句話String amt = String.valueOf(money * 100);是把傳入的錢數乘以100,並轉換成字符串。這里之所以乘以100是因為微信那邊會把我們傳過去的錢數除以100得到應付金額,且不能傳小數,所以下面的那一句amt.substring(0, amt.lastIndexOf(“.”))就是為了把金額中的小數點去掉。往下走,new出了一個hashmap,將參數傳入hashmap中,然后調用wxpay.unifiedOrder(data);下單接口下單。得到返回的map集合,從map中獲得的code_url這個參數就是微信返回給我們生成二維碼的字符串。這樣,下單的整個流程就跑通了,現在寫個測試類來測試一下。
package com.example.ffmpeg;
public class Test { public static void main(String[] args) throws Exception { WXService wx = WXService.getInstance(); String QRcode = wx.doUnifiedOrder("test001", "測試下單接口", 0.01, "a123456"); System.out.println("得到的二維碼是:"+QRcode); } }
運行結果如下圖:
如何檢驗該二維碼是否是正確的喃?很簡單,打開百度,搜索二維碼生成器,如下圖所示:
點擊進入第二個百度應用里面的進入應用,出現如下圖所示:
選擇通用文本,在中間的文本框中粘貼剛才拿到的二維碼字符串,點擊生成按鈕,右邊就會生成一個二維碼了。如下:
當然,這只是我們后台人員測試時使用的方法,實際生產環境中前端可以用一些javascript的插件去生成二維碼。
下單接口完了之后,緊接着就是退款的方法,該方法比較簡單且和下單方法大同小異,同學們自己看看注釋應該可以理解了。再往后走是微信驗簽的方法。在講這個方法之前,先來看看微信的回調方法:
/** * 微信回調的接口 * * @param uuid * @return * @throws Exception */ @RequestMapping(value = "/wxReturnPay") public void wxReturnPay(HttpServletResponse response, HttpServletRequest request) throws Exception { logger.info("****************************************wxReturnPay微信的回調函數被調用******************************"); String inputLine; String notityXml = ""; request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); response.setHeader("Access-Control-Allow-Origin", "*"); // 微信給返回的東西 try { while ((inputLine = request.getReader().readLine()) != null) { notityXml += inputLine; } request.getReader().close(); } catch (Exception e) { e.printStackTrace(); logger.info("xml獲取失敗"); response.getWriter().write(setXml("fail", "xml獲取失敗")); return; } if (StringUtils.isEmpty(notityXml)) { logger.info("xml為空"); response.getWriter().write(setXml("fail", "xml為空")); return; } WXService wxService = WXService.getInstance(); if(!wxService.checkSign(notityXml)) { response.getWriter().write(setXml("fail", "驗簽失敗")); } logger.info("xml的值為:" + notityXml); XMLSerializer xmlSerializer = new XMLSerializer(); JSON json = xmlSerializer.read(notityXml); logger.info(json.toString()); JSONObject jsonObject=JSONObject.fromObject(json.toString()); UnifiedOrderRespose returnPay = (UnifiedOrderRespose) JSONObject.toBean(jsonObject, UnifiedOrderRespose.class); logger.info(("轉換后的實體bean為:"+returnPay.toString())); logger.info(("訂單號:"+returnPay.getOut_trade_no()+"價格:"+returnPay.getTotal_fee())); if (returnPay.getReturn_code().equals("SUCCESS") && returnPay.getOut_trade_no() != null && !returnPay.getOut_trade_no().isEmpty()) { double fee = Double.parseDouble(returnPay.getTotal_fee()); returnPay.setTotal_fee(String.valueOf(fee/100)); logger.info("微信的支付狀態為SUCCESS"); tbPaymentRecordsService.wxPaySuccess(returnPay); } }
在支付成功后,微信會回調該方法(回調的url是我們在調用下單接口時傳過去的)。進入方法,首先會獲得HttpServletRequest 實例對象的流,將他讀取出來,這里面notityXml 是微信讀取出來的結果,是一串xml格式的字符串,里面有各種回調的參數信息,示例返回結果如下:
<xml> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <attach><![CDATA[支付測試]]></attach> <bank_type><![CDATA[CFT]]></bank_type> <fee_type><![CDATA[CNY]]></fee_type> <is_subscribe><![CDATA[Y]]></is_subscribe> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str> <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid> <out_trade_no><![CDATA[1409811653]]></out_trade_no> <result_code><![CDATA[SUCCESS]]></result_code> <return_code><![CDATA[SUCCESS]]></return_code> <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign> <sub_mch_id><![CDATA[10000100]]></sub_mch_id> <time_end><![CDATA[20140903131540]]></time_end> <total_fee>1</total_fee> <coupon_fee><![CDATA[10]]></coupon_fee> <coupon_count><![CDATA[1]]></coupon_count> <coupon_type><![CDATA[CASH]]></coupon_type> <coupon_id><![CDATA[10000]]></coupon_id> <coupon_fee><![CDATA[100]]></coupon_fee> <trade_type><![CDATA[JSAPI]]></trade_type> <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id> </xml>
返回參數是這個樣子的那后面自然涉及到對xml參數的解析。進入驗簽的方法:
/** * 微信驗簽接口 * * @param out_trade_no * @param body * @param money * @param applyNo * @return * @throws DocumentException */ public boolean checkSign(String strXML) throws DocumentException { SortedMap<String, String> smap = new TreeMap<String, String>(); Document doc = DocumentHelper.parseText(strXML); Element root = doc.getRootElement(); for (Iterator iterator = root.elementIterator(); iterator.hasNext();) { Element e = (Element) iterator.next(); smap.put(e.getName(), e.getText()); } return isWechatSign(smap,config.getKey()); } private boolean isWechatSign(SortedMap<String, String> smap,String apiKey) { StringBuffer sb = new StringBuffer(); Set<Entry<String, String>> es = smap.entrySet(); Iterator<Entry<String, String>> it = es.iterator(); while (it.hasNext()) { Entry<String, String> entry = it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if (!"sign".equals(k) && null != v && !"".equals(v) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + apiKey); /** 驗證的簽名 */ String sign = MD5Util.MD5Encode(sb.toString(), "utf-8").toUpperCase(); /** 微信端返回的合法簽名 */ String validSign = ((String) smap.get("sign")).toUpperCase(); return validSign.equals(sign); }
首先用dom4j的DocumentHelper解析字符串得到Document 對象,然后得到跟元素對象,遍歷,將key和value值存入SortedMap中,然后將SortedMap與微信的key一同傳入isWechatSign方法,在方法中講該SortedMap用迭代器遍歷,把key和value拼接成key1=value1&key2=value2這樣的形式,拼接時且注意以下幾點:
◆ 參數名ASCII碼從小到大排序(字典序);
◆ 如果參數的值為空不參與簽名;
◆ 參數名區分大小寫;
◆ 驗證調用返回或微信主動通知簽名時,傳送的sign參數不參與簽名,將生成的簽名與該sign值作校驗。
◆ 微信接口可能增加字段,驗證簽名時必須支持增加的擴展字段
字符串拼接完成后,使用MD5加密,然后把得到的字符串全部轉換成大寫。將轉換后的字符串與微信傳給我們的sign參數作對比,若相同,則驗簽成功,若不相同,則驗簽失敗。
驗證簽名之后的方法就屬於業務邏輯范疇了,每個人業務邏輯都不相同,我這里也就不再贅述了。那么到此,調用微信的最基本操作我就已經講完了,想看如何調用支付寶的同學可以去看我的支付寶支付接口的調用這篇文章。那么這次博客就到這里,拜拜。