一、準備工作
首先吐槽一下微信關于支付這塊,本身支持的支付模式就好幾種,但是官方文檔特別零散,連像樣的Java相關的demo也沒幾個。本人之前沒有搞過微信支付,一開始真是被它搞暈,折騰兩天終于調通了,特此寫下來,以享后人吧!
關于準備工作,就“微信掃碼支付模式二”官方文檔地址在這 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1 可以先看看,實際上需要準備的東西有以下幾個:
其中APP_ID和APP_SECRET可以在公眾平臺找著,MCH_ID和API_KEY則在商戶平臺找到,特別是API_KEY要在商戶平臺設置好,對于“微信掃碼支付模式二”(支付與回調)實際只會用到APP_ID、MCH_ID和API_KEY,其他的都不用。
關于開發環境,我就不羅嗦了,不管你是springMVC、struts2又或者直接serverlet,都差不多,只要你能保證對應的方法能調用起來就行。關于引用第三方的jar包,我這里只用到了一個xml操作的jdom,記住是1.*的版本,不是官網上最新的2.*,兩者不兼容。具體是jdom-1.1.3.jar,依賴包jaxen-1.1.6.jar,就這兩個包,我沒用到有些例子中使用的httpclient,感覺沒必要,而且依賴包特別繁雜,當然你是maven當我沒說。
二、開發實戰
1、首先是接入微信接口,獲取微信支付二維碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public String weixin_pay() throws Exception { // 賬號信息 String appid = PayConfigUtil.APP_ID; // appid //String appsecret = PayConfigUtil.APP_SECRET; // appsecret String mch_id = PayConfigUtil.MCH_ID; // 商業號 String key = PayConfigUtil.API_KEY; // key String currTime = PayCommonUtil.getCurrTime(); String strTime = currTime.substring( 8 , currTime.length()); String strRandom = PayCommonUtil.buildRandom( 4 ) + "" ; String nonce_str = strTime + strRandom; String order_price = 1 ; // 價格 注意:價格的單位是分 String body = "goodssssss" ; // 商品名稱 String out_trade_no = "11338" ; // 訂單號 // 獲取發起電腦 ip String spbill_create_ip = PayConfigUtil.CREATE_IP; // 回調接口 String notify_url = PayConfigUtil.NOTIFY_URL; String trade_type = "NATIVE" ; SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); packageParams.put( "appid" , appid); packageParams.put( "mch_id" , mch_id); packageParams.put( "nonce_str" , nonce_str); packageParams.put( "body" , body); packageParams.put( "out_trade_no" , out_trade_no); packageParams.put( "total_fee" , order_price); packageParams.put( "spbill_create_ip" , spbill_create_ip); packageParams.put( "notify_url" , notify_url); packageParams.put( "trade_type" , trade_type); String sign = PayCommonUtil.createSign( "UTF-8" , packageParams,key); packageParams.put( "sign" , sign); String requestXML = PayCommonUtil.getRequestXml(packageParams); System.out.println(requestXML); String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); Map map = XMLUtil.doXMLParse(resXml); //String return_code = (String) map.get("return_code"); //String prepay_id = (String) map.get("prepay_id"); String urlCode = (String) map.get( "code_url" ); return urlCode; } |
如果不出意外的話,這里就從微信服務器獲取了一個支付url,形如weixin://wxpay/bizpayurl?pr=pIxXXXX,之后我們就需要把這個url生成一個二維碼,然后就可以使用自己手機微信端掃碼支付了。關于二維碼生成有很多種方法,各位各取所需吧,我這里提供一個google的二維碼生成接口:
1
2
3
4
5
6
7
8
9
10
|
public static String QRfromGoogle(String chl) throws Exception { int widhtHeight = 300 ; String EC_level = "L" ; int margin = 0 ; chl = UrlEncode(chl); String QRfromGoogle = "http://chart.apis.google.com/chart?chs=" + widhtHeight + "x" + widhtHeight + "&cht=qr&chld=" + EC_level + "|" + margin + "&chl=" + chl; return QRfromGoogle; } |
1
2
3
4
|
// 特殊字符處理 public static String UrlEncode(String src) throws UnsupportedEncodingException { return URLEncoder.encode(src, "UTF-8" ).replace( "+" , "%20" ); } |
上面代碼中涉及到幾個工具類:PayConfigUtil、PayCommonUtil、HttpUtil和XMLUtil,其中PayConfigUtil放的就是上面提到一些配置及路徑,PayCommonUtil涉及到了獲取當前事件、產生隨機字符串、獲取參數簽名和拼接xml幾個方法,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
public class PayCommonUtil { /** * 是否簽名正確,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。 * @return boolean */ public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.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 (! "sign" .equals(k) && null != v && ! "" .equals(v)) { sb.append(k + "=" + v + "&" ); } } sb.append( "key=" + API_KEY); //算出摘要 String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase(); String tenpaySign = ((String)packageParams.get( "sign" )).toLowerCase(); //System.out.println(tenpaySign + " " + mysign); return tenpaySign.equals(mysign); } /** * @author * @date 2016-4-22 * @Description:sign簽名 * @param characterEncoding * 編碼格式 * @param parameters * 請求參數 * @return */ public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.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 ( null != v && ! "" .equals(v) && ! "sign" .equals(k) && ! "key" .equals(k)) { sb.append(k + "=" + v + "&" ); } } sb.append( "key=" + API_KEY); String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); return sign; } /** * @author * @date 2016-4-22 * @Description:將請求參數轉換為xml格式的string * @param parameters * 請求參數 * @return */ public static String getRequestXml(SortedMap<Object, Object> 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 length * int 設定所取出隨機數的長度。length小于11 * @return int 返回生成的隨機數。 */ public static int buildRandom( int length) { int num = 1 ; double random = Math.random(); if (random < 0.1 ) { random = random + 0.1 ; } for ( int i = 0 ; i < length; i++) { num = num * 10 ; } return ( int ) ((random * num)); } /** * 獲取當前時間 yyyyMMddHHmmss * * @return String */ public static String getCurrTime() { Date now = new Date(); SimpleDateFormat outFormat = new SimpleDateFormat( "yyyyMMddHHmmss" ); String s = outFormat.format(now); return s; } } |
HttpUtil和XMLUtil如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public class HttpUtil { private static final Log logger = Logs.get(); private final static int CONNECT_TIMEOUT = 5000 ; // in milliseconds private final static String DEFAULT_ENCODING = "UTF-8" ; public static String postData(String urlStr, String data){ return postData(urlStr, data, null ); } public static String postData(String urlStr, String data, String contentType){ BufferedReader reader = null ; try { URL url = new URL(urlStr); URLConnection conn = url.openConnection(); conn.setDoOutput( true ); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(CONNECT_TIMEOUT); if (contentType != null ) conn.setRequestProperty( "content-type" , contentType); OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); if (data == null ) data = "" ; writer.write(data); writer.flush(); writer.close(); reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); StringBuilder sb = new StringBuilder(); String line = null ; while ((line = reader.readLine()) != null ) { sb.append(line); sb.append( "\r\n" ); } return sb.toString(); } catch (IOException e) { logger.error( "Error connecting to " + urlStr + ": " + e.getMessage()); } finally { try { if (reader != null ) reader.close(); } catch (IOException e) { } } return null ; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public class XMLUtil { /** * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml數據。 * @param strxml * @return * @throws JDOMException * @throws IOException */ public static Map doXMLParse(String strxml) throws JDOMException, IOException { strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); if(null == strxml || "".equals(strxml)) { return null; } Map m = new HashMap(); InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if(children.isEmpty()) { v = e.getTextNormalize(); } else { v = XMLUtil.getChildrenText(children); } m.put(k, v); } //關閉流 in.close(); return m; } /** * 獲取子結點的xml * @param children * @return String */ public static String getChildrenText(List children) { StringBuffer sb = new StringBuffer(); if(!children.isEmpty()) { Iterator it = children.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String name = e.getName(); String value = e.getTextNormalize(); List list = e.getChildren(); sb.append("<" + name + ">"); if(!list.isEmpty()) { sb.append(XMLUtil.getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } } |
當然還有一個MD5計算工具類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public class MD5Util { private static String byteArrayToHexString( byte b[]) { StringBuffer resultSb = new StringBuffer(); for ( int i = 0 ; i < b.length; i++) resultSb.append(byteToHexString(b[i])); return resultSb.toString(); } private static String byteToHexString( byte b) { int n = b; if (n < 0 ) n += 256 ; int d1 = n / 16 ; int d2 = n % 16 ; return hexDigits[d1] + hexDigits[d2]; } public static String MD5Encode(String origin, String charsetname) { String resultString = null ; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance( "MD5" ); if (charsetname == null || "" .equals(charsetname)) resultString = byteArrayToHexString(md.digest(resultString .getBytes())); else resultString = byteArrayToHexString(md.digest(resultString .getBytes(charsetname))); } catch (Exception exception) { } return resultString; } private static final String hexDigits[] = { "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "a" , "b" , "c" , "d" , "e" , "f" }; } |
2、支付回調
支付完成后,微信會把相關支付結果和用戶信息發送到我們上面指定的那個回調地址,我們需要接收處理,并返回應答。對后臺通知交互時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功。 (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
關于支付回調接口,我們首先要對于支付結果通知的內容進行簽名驗證,然后根據支付結果進行相應的處理流程即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
public void weixin_notify(HttpServletRequest request,HttpServletResponse response) throws Exception{ //讀取參數 InputStream inputStream ; StringBuffer sb = new StringBuffer(); inputStream = request.getInputStream(); String s ; BufferedReader in = new BufferedReader( new InputStreamReader(inputStream, "UTF-8" )); while ((s = in.readLine()) != null ){ sb.append(s); } in.close(); inputStream.close(); //解析xml成map Map<String, String> m = new HashMap<String, String>(); m = XMLUtil.doXMLParse(sb.toString()); //過濾空 設置 TreeMap SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); Iterator it = m.keySet().iterator(); while (it.hasNext()) { String parameter = (String) it.next(); String parameterValue = m.get(parameter); String v = "" ; if ( null != parameterValue) { v = parameterValue.trim(); } packageParams.put(parameter, v); } // 賬號信息 String key = PayConfigUtil.API_KEY; // key logger.info(packageParams); //判斷簽名是否正確 if (PayCommonUtil.isTenpaySign( "UTF-8" , packageParams,key)) { //------------------------------ //處理業務開始 //------------------------------ String resXml = "" ; if ( "SUCCESS" .equals((String)packageParams.get( "result_code" ))){ // 這里是支付成功 //////////執行自己的業務邏輯//////////////// String mch_id = (String)packageParams.get( "mch_id" ); String openid = (String)packageParams.get( "openid" ); String is_subscribe = (String)packageParams.get( "is_subscribe" ); String out_trade_no = (String)packageParams.get( "out_trade_no" ); String total_fee = (String)packageParams.get( "total_fee" ); logger.info( "mch_id:" +mch_id); logger.info( "openid:" +openid); logger.info( "is_subscribe:" +is_subscribe); logger.info( "out_trade_no:" +out_trade_no); logger.info( "total_fee:" +total_fee); //////////執行自己的業務邏輯//////////////// logger.info( "支付成功" ); //通知微信.異步確認成功.必寫.不然會一直通知后臺.八次之后就認為交易失敗了. resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> " ; } else { logger.info( "支付失敗,錯誤信息:" + packageParams.get( "err_code" )); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> " ; } //------------------------------ //處理業務完畢 //------------------------------ BufferedOutputStream out = new BufferedOutputStream( response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } else { logger.info( "通知簽名驗證失敗" ); } } |
簽名驗證算法和簽名生成的算法類似,在上面PayCommonUtil工具類中提供。
三、后話
感覺微信掃描支付體驗效果還是挺好的,唯一缺點就是相關文檔零散,官方的demo居然沒有java編寫的,希望之后微信官方能夠逐步完善吧!
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。