一、準備工作
無數人來追問模式一的開發,所以在這就貼出來,僅供參考。關于模式一和模式二的區別,我有解釋過很多次,無非就是模式一的二維碼是針對商品的,模式二的二維碼是針對訂單的,其他具體細節我就不費口舌了,各位可以自行去官方查看文檔,然后是選模式一還是模式二就得看自己的業務了。
1.1、有關配置參數
還是之前那四樣,APP_ID和APP_SECRET可以在公眾平臺找著,MCH_ID和API_KEY則在商戶平臺找到,特別是API_KEY要在商戶平臺設置好,這個東東關系到參數校驗的正確與否,所以一定要設置正確。掃碼支付模式一其實與掃碼支付模式二類似,實際只會用到APP_ID、MCH_ID和API_KEY,其他的都不用。模式一的官方文檔地址在這:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
1.2、有關概念
在這里我想先修正一個概念,在之前模式二開發過程中我曾提到了一個“支付回調地址”這樣的概念,其作用實際就是客戶在掃描完成支付后,微信服務器要訪問我們提供的這個地址,給我們發送支付結果,以便我們核實訂單進行發貨,這是其他支付工具比較普遍的概念和叫法。不過后來我翻了一下微信官網的文檔,發現在模式一開發中,他們把這個叫做“異步通知url”而不是什么“支付回調地址”,但本質這指的是一個意思。可是為什么我要在這提到這個東東呢?這是因為在模式一中,實際上還有另外一個所謂的“支付回調”稱之為“掃碼支付回調URL”,這東東與上面的“異步通知url”可就不一樣了,簡單理解可以認為是咱們的服務器上一個用來輔助完成下單的接口。模式一的開發同時需要“掃碼支付回調URL”與“異步通知url”兩個接口配合才能完成,所以這里大家要辨別好了。
“異步通知url”在調用統一下單接口時進行設置,可以動態設置,只要這個接口按照有關規則接收參數響應參數即可。而“掃碼支付回調URL”則較為固定,它在微信公眾平臺設置,設置后需要10分鐘左右才能生效,大家登錄微信公眾平臺后,選擇微信支付,在開發配置選項卡下面就可以找著:
這里咱們要設置一個自己服務器的地址(再說一遍公網地址,就是讓微信服務器能找著你)。
1.3、開發環境
我這里以最基本的Servlet 3.0作為示例環境。關于引用第三方的jar包,相比較于模式二開發,除了用到了xml操作的jdom,以外就一個Google ZXing的二維碼包和log4j包。如下圖:
為了方便調試,建議各位先在這個環境下調通了再移植到真實項目當中去。
二、開發實戰
在動手之前,我建議大家先去官方文檔那好好看看那個時序圖,理解了那個時序圖,寫代碼也就不是什么難事了,當然如果看圖你沒辦法理解,也可以結合我下面的代碼來試著理解。
2.1、二維碼生成
首先是二維碼,二維碼中的內容為鏈接,形式為:
weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
具體可以參考官方文檔模式一生成二維碼規則。接下來我們需要將該鏈接生成二維碼,我這里使用了Google ZXing來生成二維碼。
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
116
117
118
119
120
121
122
123
124
|
package com.wqy; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.WriterException; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.wqy.util.PayCommonUtil; import com.wqy.util.PayConfigUtil; /** * Servlet implementation class Pay1 */ @WebServlet ( "/Pay1" ) public class Pay1 extends HttpServlet { private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(Pay1. class ); public static int defaultWidthAndHeight= 200 ; /** * @see HttpServlet#HttpServlet() */ public Pay1() { super (); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub String nonce_str = PayCommonUtil.getNonce_str(); long time_stamp = System.currentTimeMillis() / 1000 ; String product_id = "hd_goodsssss_10" ; String key = PayConfigUtil.API_KEY; // key SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); packageParams.put( "appid" , PayConfigUtil.APP_ID); packageParams.put( "mch_id" , PayConfigUtil.MCH_ID); packageParams.put( "time_stamp" , String.valueOf(time_stamp)); packageParams.put( "nonce_str" , nonce_str); packageParams.put( "product_id" , product_id); String sign = PayCommonUtil.createSign( "UTF-8" , packageParams,key); //MD5哈希 packageParams.put( "sign" , sign); //生成參數 String str = ToUrlParams(packageParams); String payurl = "weixin://wxpay/bizpayurl?" + str; logger.info( "payurl:" +payurl); //生成二維碼 Map<EncodeHintType, Object> hints= new HashMap<EncodeHintType, Object>(); // 指定糾錯等級 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); // 指定編碼格式 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8" ); hints.put(EncodeHintType.MARGIN, 1 ); try { BitMatrix bitMatrix = new MultiFormatWriter().encode(payurl,BarcodeFormat.QR_CODE, defaultWidthAndHeight, defaultWidthAndHeight, hints); OutputStream out = response.getOutputStream(); MatrixToImageWriter.writeToStream(bitMatrix, "png" , out); //輸出二維碼 out.flush(); out.close(); } catch (WriterException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public String ToUrlParams(SortedMap<Object, Object> packageParams){ //實際可以不排序 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)) { sb.append(k + "=" + v + "&" ); } } sb.deleteCharAt(sb.length()- 1 ); //刪掉最后一個& return sb.toString(); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } } |
2.2、掃描支付回調url接口
當客戶用微信掃了上面的二位碼之后,微信服務器就會訪問此接口,在這里我們要完成統一下單獲取交易會話標識,處理的主要流程如下:
1)、接收微信服務器發送過來的參數,對參數進行簽名校驗;
2)、取出參數product_id,這是二維碼上唯一能夠透傳過來的參數,其他參數可參照官方文檔模式一3.1 輸入參數;
3)、根據product_id處理自己的業務,比如計算支付金額,生成訂單號等;
4)、調用統一下單接口獲取交易會話標識prepay_id;
4.1)、準備好相關參數(如appid、mch_id、支付金額、訂單號、商品描述等),調用微信統一下單接口(與模式二調用統一下單接口類似),留意一下這里要加上上面提到的“異步通知url”,也就是后面會說道的異步通知url接口,具體參數參考官方文檔統一下單請求參數;
4.2)、接收統一下單接口返回的參數,對參數進行驗簽;
4.3)、取出參數prepay_id,這是交易會話標識,極其重要,其他參數可參考官方文檔統一下單返回結果;
5)、準備好相關參數(如appid、mch_id、return_code、prepay_id等),響應最開始的支付回調(如果上面步驟如果錯誤,如驗簽失敗則可以返回錯誤參數給微信服務器),具體參數可參照官方文檔模式一3.2 輸出參數。
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
package com.wqy; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.SortedMap; import java.util.TreeMap; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import com.wqy.util.HttpUtil; import com.wqy.util.PayCommonUtil; import com.wqy.util.PayConfigUtil; /** * Servlet implementation class Notify1 */ @WebServlet ( "/Notify1" ) public class Notify1 extends HttpServlet { private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(Notify1. class ); /** * @see HttpServlet#HttpServlet() */ public Notify1() { super (); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // 讀取xml 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(); SortedMap<Object, Object> packageParams = PayCommonUtil.xmlConvertToMap(sb.toString()); logger.info(packageParams); // 賬號信息 String key = PayConfigUtil.API_KEY; // key String resXml= "" ; //反饋給微信服務器 // 驗簽 if (PayCommonUtil.isTenpaySign( "UTF-8" , packageParams, key)) { //appid openid mch_id is_subscribe nonce_str product_id sign //統一下單 String openid = (String)packageParams.get( "openid" ); String product_id = (String)packageParams.get( "product_id" ); //解析product_id,計算價格等 String out_trade_no = String.valueOf(System.currentTimeMillis()); // 訂單號 String order_price = "1" ; // 價格 注意:價格的單位是分 String body = product_id; // 商品名稱 這里設置為product_id String attach = "XXX店" ; //附加數據 String nonce_str0 = PayCommonUtil.getNonce_str(); // 獲取發起電腦 ip String spbill_create_ip = PayConfigUtil.CREATE_IP; String trade_type = "NATIVE" ; SortedMap<Object,Object> unifiedParams = new TreeMap<Object,Object>(); unifiedParams.put( "appid" , PayConfigUtil.APP_ID); // 必須 unifiedParams.put( "mch_id" , PayConfigUtil.MCH_ID); // 必須 unifiedParams.put( "out_trade_no" , out_trade_no); // 必須 unifiedParams.put( "product_id" , product_id); unifiedParams.put( "body" , body); // 必須 unifiedParams.put( "attach" , attach); unifiedParams.put( "total_fee" , order_price); // 必須 unifiedParams.put( "nonce_str" , nonce_str0); // 必須 unifiedParams.put( "spbill_create_ip" , spbill_create_ip); // 必須 unifiedParams.put( "trade_type" , trade_type); // 必須 unifiedParams.put( "openid" , openid); unifiedParams.put( "notify_url" , PayConfigUtil.NOTIFY_URL); //異步通知url String sign0 = PayCommonUtil.createSign( "UTF-8" , unifiedParams,key); unifiedParams.put( "sign" , sign0); //簽名 String requestXML = PayCommonUtil.getRequestXml(unifiedParams); logger.info(requestXML); //統一下單接口 String rXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); //統一下單響應 SortedMap<Object, Object> reParams = PayCommonUtil.xmlConvertToMap(rXml); logger.info(reParams); //驗簽 if (PayCommonUtil.isTenpaySign( "UTF-8" , reParams, key)) { // 統一下單返回的參數 String prepay_id = (String)reParams.get( "prepay_id" ); //交易會話標識 2小時內有效 String nonce_str1 = PayCommonUtil.getNonce_str(); SortedMap<Object,Object> resParams = new TreeMap<Object,Object>(); resParams.put( "return_code" , "SUCCESS" ); // 必須 resParams.put( "return_msg" , "OK" ); resParams.put( "appid" , PayConfigUtil.APP_ID); // 必須 resParams.put( "mch_id" , PayConfigUtil.MCH_ID); resParams.put( "nonce_str" , nonce_str1); // 必須 resParams.put( "prepay_id" , prepay_id); // 必須 resParams.put( "result_code" , "SUCCESS" ); // 必須 resParams.put( "err_code_des" , "OK" ); String sign1 = PayCommonUtil.createSign( "UTF-8" , resParams,key); resParams.put( "sign" , sign1); //簽名 resXml = PayCommonUtil.getRequestXml(resParams); logger.info(resXml); } else { logger.info( "簽名驗證錯誤" ); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[簽名驗證錯誤]]></return_msg>" + "</xml> " ; } } else { logger.info( "簽名驗證錯誤" ); 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(); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } } |
至此,用戶的微信單就會顯示出要支付的金額及商品描述等,接下來就是等待客戶完成支付。
2.3、異步通知url接口
當用戶在微信上完成支付操作后,微信服務器就會異步通知這個接口,給我們發送最終的支付結果,以便我們核實訂單進行發貨等操作,注意這個接口和模式二的開發是一模一樣的。大概流程如下:
1)、接收微信服務器發送過來的參數,對參數進行簽名校驗;
2)、取出參數result_code、訂單號out_trade_no、訂單金額total_fee及其他業務相關的參數,具體參數可參照官方文檔支付結果通用通知的通知參數;
3)、處理業務,如校驗訂單號及訂單金額、修改訂單狀態等;
4)、準備好相關參數(return_code和return_msg),應答微信服務器。
注意,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功。 (通知頻率為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
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
116
117
118
119
120
121
122
|
package com.wqy; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.SortedMap; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import com.wqy.util.PayCommonUtil; import com.wqy.util.PayConfigUtil; /** * Servlet implementation class Re_notify */ @WebServlet ( "/Re_notify" ) public class Re_notify extends HttpServlet { private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(Re_notify. class ); /** * @see HttpServlet#HttpServlet() */ public Re_notify() { super (); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // 讀取參數 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(); SortedMap<Object, Object> packageParams = PayCommonUtil.xmlConvertToMap(sb.toString()); logger.info(packageParams); // 賬號信息 String key = PayConfigUtil.API_KEY; // key String resXml = "" ; // 反饋給微信服務器 // 判斷簽名是否正確 if (PayCommonUtil.isTenpaySign( "UTF-8" , packageParams, key)) { // ------------------------------ // 處理業務開始 // ------------------------------ 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> " ; } } else { logger.info( "簽名驗證錯誤" ); 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(); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } } |
三、測試結果
3.1、生成的支付二維碼鏈接
3.2、支付回調url接口接收到的參數
3.3、發起統一下單請求參數
3.4、統一下單返回參數
3.5、支付回調url接口最終的響應參數
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。