第三方APP微信支付Java服務端構建步驟

因日前工作需要,做了一次微信支付。其中一些關鍵點記錄下來,以備不時之需,也拿出來交流下,如有不足之處,還望多多指教。

第三方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;
	}
	
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章