@author StormMa
@date 2017-05-23 01:41
生命不息,奮鬥不止!
最近一個項目中用到了微信開發,之前沒有做過支付相關的東西,算是拿這個來練練手,剛開始接觸支付時候很懵逼,加上微信支付開發文檔本來就講得不清楚,我是徹底蒙圈了,參考了很多代碼之後,算是有一點思路了。
用戶認證獲取openId
如果你知識關注支付流程,這塊可以跳過,因爲我知道這些你已經做過了,在開始所有的流程之前,我覺得你應該把所有微信相關的配置放到一個properties文件中去,這樣不僅顯得更規範,而且會避免犯很多錯誤,真是一個完美的選擇!
######## 配置文件
######## 公衆號開發配置中的token(自定義)
wechat.token=
######## 應用id
wechat.appId=
######## 密鑰(同token查看地址)
wechat.appSecret=
######## 靜默授權微信回調url
wechat.callBackSlientUrl=
######## 商戶Id(支付相關)
wechat.MCHID=
######## 微信下單地址
wechat.wxorder=https://api.mch.weixin.qq.com/pay/unifiedorder
######## 支付api密鑰
wechat.KEY=
######## 支付結果回調地址
wechat.NOTIFYURL=
接着你可以考慮把這個properties注入到一個bean中,使用更方便,當然你還可以選擇使用java來讀取properties的配置,對比這兩個方法,我更喜歡第一個,我就使用第一種方法來演示一下(這裏使用spring boot框架,spring mvc類似)
/**
* <p>Created on 2017/3/13.</p>
*
* @author StormMma
*
* @Description: 微信相關常量
*/
@Component
@ConfigurationProperties(locations = {"classpath:config/wechat.properties"}, prefix = "wechat")
public class WeChatConfigBean {
/**
* token
*/
private String token;
/**
* app id
*/
private String appId;
/**
* app secret
*/
private String appSecret;
/**
* 靜默授權回調地址
*/
private String callBackSlientUrl;
/**
* 商戶id
*/
private String MCHID;
/**
* 異步回調地址
*/
private String NOTIFYURL;
/**
* 微信統一下單地址
*/
private String wxorder;
/**
* key
*/
private String KEY;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public String getCallBackSlientUrl() {
return callBackSlientUrl;
}
public void setCallBackSlientUrl(String callBackSlientUrl) {
this.callBackSlientUrl = callBackSlientUrl;
}
public String getMCHID() {
return MCHID;
}
public void setMCHID(String MCHID) {
this.MCHID = MCHID;
}
public String getNOTIFYURL() {
return NOTIFYURL;
}
public void setNOTIFYURL(String NOTIFYURL) {
this.NOTIFYURL = NOTIFYURL;
}
public String getWxorder() {
return wxorder;
}
public void setWxorder(String wxorder) {
this.wxorder = wxorder;
}
public String getKEY() {
return KEY;
}
public void setKEY(String KEY) {
this.KEY = KEY;
}
封裝請求工具(這次我選擇使用HttpClient, 此處的json工具我選擇了ali的fastjson)
RequestUtil.java
/**
* 發送Get請求到url,獲得response的json實體
* @param url
* @return
* @throws IOException
*/
private JSONObject doGetUrl(String url) throws WechatException, ServerSystemException {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response;
String result;
try {
response = httpclient.execute(httpGet);
HttpEntity entity = response.getEntity();
result = EntityUtils.toString(entity, "UTF-8");
httpclient.close();
} catch (IOException e) {
logger.error("執行GET請求發生錯誤!", e);
throw new ServerSystemException("執行GET請求發生錯誤!{}", e);
}
return JSONObject.parseObject(result);
}
/**
* 發送post請求
* @param url
* @param param
* @return
* @throws ServerSystemException
*/
private JSONObject doPostUrl(String url, String param) throws ServerSystemException {
final String CONTENT_TYPE_TEXT_JSON = "application/json";
DefaultHttpClient httpClient = new DefaultHttpClient(new PoolingClientConnectionManager());
HttpPost httpPost = new HttpPost(url);
HttpResponse response;
String result;
try {
StringEntity stringEntity = new StringEntity(param);
stringEntity.setContentType(CONTENT_TYPE_TEXT_JSON);
stringEntity.setContentEncoding("UTF-8");
httpPost.setEntity(stringEntity);
response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
result = EntityUtils.toString(entity, "UTF-8");
httpClient.close();
} catch (IOException e) {
logger.error("執行POST請求發生錯誤!", e);
throw new ServerSystemException("執行POST請求發生錯誤!{}", e);
}
return JSONObject.parseObject(result);
}
獲取code
在此之前,我想我們應該抽出一個微信工具類,專門來封裝各種請求和RequestUtil來結合使用,是的,這是一個很好的選擇。
WxRequestUtil.java
public calss WxRequestUtil {
@AutoWired
private WechatConfigBean config;
/**
* <p>獲得靜默授權的url</p>
* @return
*/
public String getSlientUrl() {
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + config.getAppId() +
"&redirect_uri=" + URLEncoder.encode(config.getCallBackSlientUrl()) +
"&response_type=code" +
"&scope=snsapi_base" +
"&state=STATE#wechat_redirect";
return url;
}
}
接着我想我們應該參照開發文檔來重定向到這個url,然後微信服務器會檢查參數接着重定向到我們的回調地址,嗯嗯,你猜對了,就是參數帶的那個redirect_uri,那麼我們應該補充一下回調接口
獲取openId
WechatController.java
/**
* 獲得openId,靜默授權
* @param code
* @param session
* @param response
*/
@RequestMapping(value = "/slient/check")
public RequestResult<String> callBackBase(@RequestParam(value = "code", required = false) String code, HttpServletResponse response) {
String openId = wechatService.getOpenIdBySlientAuthy(
return ResultUtil.success(openId);
}
我想我應該解釋一下,控制器層我用的都是規範化的請求響應,不知道的可以參考我前面的博文。另外一點我需要說明的就是我們還需要一個service來處理獲取openId的邏輯。
WechatService.java
/**
* 靜默授權獲得openId
* @param code
* @return
*/
public String getOpenIdBySlientAuthy(String code) {
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + config.getAppId() +
"&secret=" + config.getAppSecret() +
"&code=" + code +
"&grant_type=authorization_code";
//爲了代碼簡便,此處省略異常處理
JSONObject jsonObject = doGetUrl(url);
return jsonObject.getString("openid");
}
至此,我們獲得了openId,那麼接着我們回到支付的話題上
微信支付
首先,我需要說明的是,微信支付的一個流程,至於爲什麼呢,我的目的很明確就是要描述清楚微信支付。我做支付的時候看過很多資料,有一個很深的體會就是代碼複製來複制去,一大片一大片的代碼看着心碎。在這裏,我就不貼微信官方的流程圖了,我相信你看着流程圖會嚇一跳,所以我選擇不殘害你。回到正題,微信支付最重要的就是三個步驟。
- 統一下單,得到預支付id, 次數需要你提供商戶的信息以及商品的信息,然後得到一個預支付id(請相信我,其他返回的數據並沒有什麼實際的意義)
- 組裝調起支付參數(我不知道叫什麼名字更貼切,索性就這麼叫吧,這個步驟其實就是使用預支付id,和其他的配置信息簽名生成請求數據,返回至前臺調用)
- 調起支付(使用jssdk或者h5接口調起支付)
其他的步驟就不是那麼重要了,比如支付接口通知接口,可以根據自己的需求進行改寫,這裏我就不多說了。
統一下單
PayService.java
/**
* 獲得統一下單參數
* @param openId
* @param totalFee
* @param ip
* @param body
* @return
*/
public String getPayParam(String openId, String totalFee, String ip, String body) {
Map<String, String> datas = new TreeMap<>();
datas.put("appid", weChatConfigBean.getAppId());
datas.put("mch_id", weChatConfigBean.getMCHID());
//設備
datas.put("device_info", "WEB");
//商品描述
datas.put("body", body);
//支付類型,這裏使用公衆號支付,所以是JSAPI
datas.put("trade_type", "JSAPI");
//隨機字符串,32字符以內
datas.put("nonce_str", WXUtil.getNonceStr());
//支付結果通知地址
datas.put("notify_url", config.getNOTIFYURL());
//訂單號,自己生成一個唯一的訂單號就行
datas.put("out_trade_no", createOutTradeNO());
//支付金額,以分爲單位
datas.put("total_fee", totalFee);
//用戶openId
datas.put("openid", openId);
//ip
datas.put("spbill_create_ip", ip);
String sign = SignatureUtils.signature(datas, config.getKEY());
datas.put("sign", sign);
看到這裏,你可能有點懵逼,我想我需要解釋一下,開始之前我們用Map把所有的參數封裝起來,至於爲什麼用TreeMapp,因爲我們後面的簽名要將Map的參數轉換成一個字符串的形式(字段名=字段值&字段名=字段值)並且字段名字典序排序,這樣,我們就只需要關注簽名算法的實現,官方文檔有解釋簽名算法,就像我前面說的,我們需要把Map轉換成字符串的形式,並且後面要追加一個&key=#{key}(注意:#{key}是你的字段值)的參數,然後進行加密。我想此處我應該給出我的簽名:
SignatureUtils.java
/**
* 微信支付加密工具
* @param key
* @param map
*/
public static String signature(Map<String, String> map, String key) {
Set<String> keySet = map.keySet();
String[] str = new String[map.size()];
StringBuilder tmp = new StringBuilder();
str = keySet.toArray(str);
for (int i = 0; i < str.length; i++) {
String t = str[i] + "=" + map.get(str[i]) + "&";
tmp.append(t);
}
if (StringUtils.isNotBlank(key)) {
tmp.append("key=" + key);
}
String tosend = tmp.toString();
MessageDigest md = null;
byte[] bytes = null;
try {
md = MessageDigest.getInstance("MD5");
bytes = md.digest(tosend.getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
}
String singe = byteToStr(bytes);
return singe.toUpperCase();
}
/**
* 字節數組轉換爲字符串
* @param byteArray
* @return
*/
public static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 字節轉換爲字符串
* @param mByte
* @return
*/
public static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A',
'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
這個工具類,具體我就不多介紹了,可以查看一下官方文檔,瞭解一下簽名算法,然後回來看代碼,我相信你可以看懂。
我想我應該說一聲對不起,我忘了解釋其實我們最終下單的參數是一個xml的String類型,所以我們還要把Map轉換成xml,這個就很簡單了。我們可以考慮把它加到PayService裏面(其他地方用不着,你可以考慮私有,相信我,這樣會更優雅)。
/**
* 得到統一下單參數的xml形式
*
* @param parameters
* @return
*/
public static String getRequestXml(Map<String, String> parameters) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = parameters.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
} else {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
於是,我們得到了統一下單的參數,接下來就是去請求微信服務器了。
/**
* 支付接口
* @param body
* @param totalFee
* @param user
* @param response
* @return
* @throws Exception
*/
@PostMapping(value = "/pay")
public RequestResult<Map<String, String>> order(@RequestParam("body")String body,
@RequestParam("totalFee")String totalFee,
@SessionAttribute(name = "user", required = false)User user,
HttpServletResponse response) throws Exception {
//之前我們獲得了openId,這裏我使用假數據測試
String openId = "oxxjlv1dWSkielTGFfWQGNK-RHSc";
String ip = this.getIpAddress();
String requestParam = payService.getPayParam(openId, totalFee, ip, body);
//stop here ,下面我會講
Map<String, String> result = payService.requestWechatPayServer(requestParam);
Map<String, String> datas = new TreeMap<>();
if (result.get("return_code").equals("SUCCESS")) {
String prepayId = result.get("prepay_id");
datas.put("appId", weChatConfigBean.getAppId());
datas.put("package", "prepay_id=" + prepayId);
datas.put("signType", "MD5");
datas.put("timeStamp", Long.toString(new Date().getTime()).substring(0, 10));
datas.put("nonceStr", WXUtil.getNonceStr());
String sign = SignatureUtils.signature(datas, weChatConfigBean.getKEY());
datas.put("paySign", sign);
return ResultUtil.success(datas);
}
return ResultUtil.fail();
}
組裝調起支付參數
繼續上面的控制器,我們已經得到了預支付id,那麼我們離成功不遠了 請相信我,我沒有騙你。然後我們要封裝調起支付參數,我們先看一下jssdk調起支付需要的參數。
wx.chooseWXPay({
timestamp: 0, // 支付簽名時間戳,注意微信jssdk中的所有使用timestamp字段均爲小寫。但最新版的支付後臺生成簽名使用的timeStamp字段名需大寫其中的S字符
nonceStr: '', // 支付簽名隨機串,不長於 32 位
package: '', // 統一支付接口返回的prepay_id參數值,提交格式如:prepay_id=***)
signType: '', // 簽名方式,默認爲'SHA1',使用新版支付需傳入'MD5'
paySign: '', // 支付簽名
success: function (res) {
// 支付成功後的回調函數
}
});
那麼我們就根據這個參數列表來生成參數,不過我很好奇,爲什麼timeStamp一陣大寫一陣小寫的,我想估計腦子抽了吧。現在我們看看上面的控制器剩餘的代碼,其實就是組裝這個參數到Map,我想這個應該沒有疑惑的地方吧。說到這,微信開發基本結束了,剩下的就是js調起支付,輸入密碼,微信服務器判斷,給你返回結果的過程,處理結果的接口我就不貼了,簡單到不行。
結束
在做微信支付的時候,我有時候真的很無奈,沒有好的官方文檔,更沒有好的博文,這篇博客旨在能講清楚微信支付的步驟,我知道在這麼短的時間講清楚顯然不可能,希望各位多多指正,有問題的可以發郵件給我,[email protected]。哦對了,最後別忘記配置支付目錄,不然會顯示url未註冊。應部分人的要求,最後寫了一個demo,附上鍊接:https://github.com/StormMaybin/wxpay.git。本文章來自我的個人站點: http://blog.stormma.me, 轉發請註明來源!