微信支付介面的呼叫
在上週的部落格中我講了呼叫支付寶的介面實現支付,這周我們繼續來講一講如何呼叫微信的支付介面。
在講之前依然先給出微信的官方介面說明。官方的場景介紹圖如下:
其實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引數作對比,若相同,則驗籤成功,若不相同,則驗籤失敗。
驗證簽名之後的方法就屬於業務邏輯範疇了,每個人業務邏輯都不相同,我這裡也就不再贅述了。那麼到此,呼叫微信的最基本操作我就已經講完了,想看如何呼叫支付寶的同學可以去看我的支付寶支付介面的呼叫這篇文章。那麼這次部落格就到這裡,拜拜。