因日前工作需要,做了一次微信支付。其中一些關鍵點記錄下來,以備不時之需,也拿出來交流下,如有不足之處,還望多多指教。
第三方APP微信支付時序圖
詳見:"https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3"
1.準備工作獲取appid和appkey(api密匙)
微信支付申請審覈通過後,您就可以收到微信發給您的郵件,裏面有您賬戶相關參數。
第一個坑:公衆平臺的密鑰和商戶號的密鑰是不一樣的!!!微信支付審覈成功之後會收到一封郵件,郵件中有appid 商戶號,商戶後臺登錄上號和密碼,登錄到商戶後臺:賬戶設置-安全設置-切換到API安全,下載證書,下面有一個api密匙,進去填寫一個字符串,保存,後續兩次簽名都是用的這個手動設置的key!!!
2.服務端工作流程
這裏跳過流程圖中的前三步
2.1調用統一下單接口,獲取prepayid
2.1.1準備所需參數
統一下單接口必需參數:appid,mch_id,nonce_str,body,out_trade_no,total_fee,spbill_create_ip,notify_url,trade_type,這九個參數部分是客戶端傳來的,其中notify_url是異步通知(微信通知應用服務端的URL地址)
詳見https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
第二個坑:以上所需九個參數變量名必是小寫,total_fee以分爲單位,是大於0的整數;同一個應用的安卓和蘋果分配的appid不一樣,可以讓客戶端攜帶appid來請求
2.1.1對參數進行簽名
按照接口文檔給的簽名說明:需要先對參數按照ascii碼進行排序,值爲空的不參與排序,參與排序的字段不包括appkey(api密匙)。排完序後,將排好序的字段塞進xml中,同時用排好序的字符串與key及其值通過&拼接起來,之後再使用MD5加密,然後將小寫轉大寫,這就是生成的sign(簽名)值。最後將sign塞進xml中。然後就可以開始向微信統一下單接口發起請求了,接收prepayid。
以下是我自己的代碼,大部分和網上的一樣:
//這個是將參數從reqInfo對象放進一個map中
public StringgetXmlStr(WCPayGetPrePayIdReqInfo reqInfo) {
Map<String,Object>params = new HashMap<String,Object>();
params.put("appid",reqInfo.getAppId()); //上面的appid,注意大小寫
params.put("mch_id",reqInfo.getMchId()); //商戶id
params.put("key",reqInfo.getKey()); //appkey(api密匙)
params.put("nonce_str",WCPayUtils.getRandomNumber(32)); //32位隨機數
params.put("body",reqInfo.getBody()); //商品描述
params.put("out_trade_no",reqInfo.getOutTradeNo()); //應用後臺生成的訂單id
params.put("total_fee",reqInfo.getTotalFee()); //總金額
params.put("spbill_create_ip",reqInfo.getSpbillCreateIp()); //用戶終端ip
params.put("notify_url","application/test"); //異步通知URL
params.put("trade_type",reqInfo.getTradeType()); //交易方式,參見微信接口文檔
try{
returnWCPayUtils.getXmlFromParamsMap(params);
}catch (Exception e) {
LogFactory.getLog("Message").debug("生成xml字符串出錯");
}
return null;
}
2.1.2調用統一下單接口
將以上參xml字符串準備妥當後,用httpclient向微信統一下單接口發起請求,拿到返回的數據後,對其進行簽名認證,認證成功後封裝到WCPayGetPrePayIdRespInfo對象中,請求方法如下
private void getPrepayId(StringxmlStr,WCPayGetPrePayIdRespInfo respInfo) {
try {
HttpClientBuilderhttpClientBuilder = HttpClientBuilder.create();
CloseableHttpClientcloseableHttpClient = httpClientBuilder.build();
HttpPost httpPost =new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder");
HttpClientContextcontext = HttpClientContext.create();
StringEntity se = newStringEntity(xmlStr);
se.setContentType("text/xml");
se.setContentEncoding(newBasicHeader(HTTP.CONTENT_TYPE, "application/xml"));
httpPost.setEntity(se);
httpPost.setConfig(RequestConfig.DEFAULT);
HttpResponsehttpResponse = closeableHttpClient.execute(httpPost, context);
HttpEntity httpEntity= httpResponse.getEntity();
if (httpEntity !=null) {
// 打印響應內容
InputStreamcontent = httpEntity.getContent();
Map<String,String> params = WCPayUtils.getParamsMapFromXml(content);
params.put("key",pContect.getWcPayKey());
if(params.containsKey("sign")&& params.get("prepay_id") != null &&
!"".equals(params.get("prepay_id"))&& !"null".equals(params.get("prepay_id"))){
if(WCPayUtils.checkSign(params)){//簽名認證成功
for(Map.Entry<String, String> param : params.entrySet()) {
if("appid".equals(param.getKey()))
respInfo.setAppId(param.getValue());
elseif("mch_id".equals(param.getKey()))
respInfo.setPartnerId(param.getValue());
elseif("prepay_id".equals(param.getKey()))
respInfo.setPrepayId(param.getValue());//將prepayid放進WCPayGetPrePayIdRespInfo對象中
else
continue;
}
}
}else{
LogFactory.getLog("system").info("第一次響應簽名認證失敗");
}
}
closeableHttpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
2.2 給客戶端返回數據
對WCPayGetPrePayIdRespInfo對象中的其他字段值進行設置,WCPayGetPrePayIdRespInfo對象是返回給app端的對象,這裏有app端調起支付接口所需的七個必需參數:
appid,partnerid(商戶id),prepayid(預支付標識,微信返回給服務端的xml數據中有),noncestr(32位隨機字符串,建議新生成一個),package(取固定值 Sign=WXPay), timestamp(時間戳),sign(簽名)
第三個坑這裏的sign還是通過要對上面六個(不包括sign)參數進行排序,然後拼接appkey後在進行md5加密,再小寫變大寫獲取),
之後將sign也放進WCPayGetPrePayIdRespInfo對象中,然後傳遞給app客戶端,
2.3接收微信異步通知並處理相應業務邏輯
當app客戶端向微信發起支付請求,並付款成功後,微信會向異步通知URL也就是notify_url上面傳遞支付接口信息,也是xml字符串。我們需要將之解析完成後,並再次生成sign,然後將生成的sign與傳來的sign進行比對認證,認證成功則說明是微信發來的信息。然後從裏面拿到result_code,如果result_code是“SUCCESS”說明支付成功。處理相應業務邏輯。
以上,就是第三方APP微信支付Java服務端構建步驟。如有不對的之處,還請指正交流。
下面附上上面所用到的兩個工具類
WCPayUtils工具類
import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
public class WCPayUtils {
/**
* 將微信支付所需參數拼接爲xml字符串
* @param <T>
*
* @param paramsMap
* @return
* @throws Exception
*/
public static <T> String getXmlFromParamsMap(Map<String, T> paramsMap) throws Exception {
if (paramsMap != null && paramsMap.size() > 0) {
Map<String, Object> params = new TreeMap<String, Object>(new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
params.putAll(paramsMap);
StringBuffer ss = new StringBuffer("<xml>");
for (Entry<String, Object> param : params.entrySet()) {
if (!"key".equals(param.getKey())) {
ss.append("<" + param.getKey() + "><![CDATA[").append(param.getValue())
.append("]]></" + param.getKey() + ">");
}
}
String sign = getSignFromParamMap(params);
ss.append("<sign>" + sign + "</sign>");
ss.append("</xml>");
String xmlString = ss.toString();
return new String(xmlString.getBytes(), "ISO-8859-1");
}
return null;
}
/**
* 從xml字符串中解析參數
* @param xml
* @return
* @throws Exception
*/
public static Map<String, String> getParamsMapFromXml(InputStream xml) throws Exception {
Map<String, String> params = new HashMap<String, String>(0);
SAXReader saxReader = new SAXReader();
Document read = saxReader.read(xml);
Element node = read.getRootElement();
listNodes(node, params);
return params;
}
/**
* 生成count位的隨機數
*
* @param count
* @return
*/
public static String getRandomNumber(int count) {
StringBuffer ss = new StringBuffer(count);
for (int i = 0; i < count; i++) {
int a = (int) (Math.random() * 10);
ss.append(a);
}
return ss.toString();
}
@SuppressWarnings({ "unchecked" })
public static void listNodes(Element node, Map<String, String> params) {
// 獲取當前節點的所有屬性節點
List<Attribute> list = node.attributes();
// 遍歷屬性節點
if ((list == null || list.size() == 0) && !(node.getTextTrim().equals(""))) {
if(node.getTextTrim().contains("<![CDATA[")){
String[] split = node.getTextTrim().split("<![CDATA[");
split[1].replaceAll("]]>", "");
params.put(node.getName(), split[1]);
}else{
params.put(node.getName(),node.getTextTrim());
}
}
// 當前節點下面子節點迭代器
Iterator<Element> it = node.elementIterator();
// 遍歷
while (it.hasNext()) {
// 獲取某個子節點對象
Element e = it.next();
// 對子節點進行遍歷
listNodes(e, params);
}
}
/**
* 從map中獲取簽名sign
* @param paramsMap
* @return
* @throws Exception
*/
public static <T> String getSignFromParamMap(Map<String, T> paramsMap) throws Exception{
if (paramsMap != null && paramsMap.size() > 0) {
Map<String, T> params = new TreeMap<String, T>(new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
params.putAll(paramsMap);
StringBuffer tempStr = new StringBuffer();
for (Entry<String, T> param : params.entrySet()) {
if (!"sign".equals(param.getKey()) && !"key".equals(param.getKey())
&& !"".equals(param.getValue()) && param.getValue() != null) {
tempStr.append(param.getKey() + "=" + param.getValue() + "&");
}
}
String temp = tempStr.toString().concat("key="+params.get("key"));
return MD5Utils.getMD5(temp).toUpperCase();
}
return null;
}
/**
* 簽名認證
* @param paramsMap
* @return
* @throws Exception
*/
public static <T> boolean checkSign(Map<String, T> paramsMap) throws Exception {
String sign = getSignFromParamMap(paramsMap);
return paramsMap.get("sign").equals(sign);
}
}
MD5Utils工具類
import java.security.MessageDigest;
public class MD5Utils {
// 十六進制下數字到字符的映射數組
private final static String[] HEXDIGITS = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d","e", "f" };
public final static String getMD5(String str){
if (str != null) {
try {
// 創建具有指定算法名稱的信息摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 使用指定的字節數組對摘要進行最後更新,然後完成摘要計算
byte[] results = md.digest(str.getBytes()); // 將得到的字節數組變成字符串返回
StringBuffer resultSb = new StringBuffer();
String a = "";
for (int i = 0; i < results.length; i++) {
int n = results[i];
if (n < 0)
n = 256 + n;
int d1 = n / 16;
int d2 = n % 16;
a = HEXDIGITS[d1] + HEXDIGITS[d2];
resultSb.append(a);
}
return resultSb.toString();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}
}