實戰技能:小小微信支付業務,何須虛驚一場

記得上次接觸微信支付是2016年末,那次也是我程序生涯中首次碰及支付業務,慌張談不上可是懵逼懷疑時時都有。提及第三方登陸或者支付,想必都清楚是直接調用人家現成的API,徹底沒有開發成本和技術含量。可是我想說:若是你沒有過一次從零開發並維護過一個完整支付模塊的話,我相信有不少坑對你來講都是黑盒的,也許如今的公司或多或少都涵蓋了支付業務,可能被早先的老程序員們已經維護的差很少完美了,徹底是能夠當成模板功能來使用,新手只需複製大致功能架子填充支付後的業務邏輯便可。html

好了,切入正題吧。爲何忽然在各位前輩面前擺上這麼一篇沒有任何技術含量的文章呢?由於很久沒有在博客園發佈過文章了,剛剛瞻仰完各位大佬分享的東西后內心很空虛。其次是主要緣由,因爲前幾天休假剛回來就被剛剛入職的哥們拉住繞了一下子,說這倆天測試人員反應充一次錢以後會看到好幾條充值記錄而且是偶現的,並且相應的遊戲幣也累加了不少次等等,而後還有一個更坑的現象就是照着微信開發文檔調用接口,喚醒微信支付的組件開始調用預支付(統一下單)接口時拋簽名錯誤的異常,居然調不通(聲明:簽名方式已是確保無誤的)。。。真的,聽他一說還真回憶起當年那個手忙腳亂的本身,在開發過程當中各類懵逼、各類懷疑人生。因此今天就把這些踩坑經歷分享一下,僅獻給圈裏剛剛接觸或要開始接手支付業務的朋友,但願各位在鑄造支付模塊時可以一馬平川。前端

準備工做:

  • 申請註冊微信開發平臺帳號,申請需支付300元,內心有個數啊。

  • 在註冊經過以後,到管理中心申請添加大家的APP應用。

  • 若是APP應用審覈經過以後,就能夠申請開放微信支付功能,好比像分享到朋友圈、微信登陸等功能都是自動開放的,無需申請。

  • 最後若是微信支付功能審覈也經過後,微信會給你分配一個微信商戶號,基本包含了:登陸帳戶號、密碼、APPID和支付商戶號等信息,這些信息要本身保留好,後續調用支付相關的接口時會用到。

 服務端支付業務開發步驟:

 一、登陸微信商戶平臺,注意是商戶平臺不是開放平臺,根據業務場景選擇適合本身的支付類型,進入以後就能夠看看具體的API列表,除此以外還提供了業務場景舉例、業務流程時序圖等很是清晰。提醒一點微信還提供了專門的demo壓縮包,裏面包含了工具類WXPayUtil建議下載,由於相信你能用到,是百分百能用到。java

  1 package com.qy.utils;
  2 
  3 import java.io.ByteArrayInputStream;
  4 import java.io.InputStream;
  5 import java.io.StringWriter;
  6 import java.util.*;
  7 import java.security.MessageDigest;
  8 import org.w3c.dom.Node;
  9 import org.w3c.dom.NodeList;
 10 
 11 import com.qy.utils.WXPayConstants.SignType;
 12 
 13 import javax.crypto.Mac;
 14 import javax.crypto.spec.SecretKeySpec;
 15 import javax.xml.parsers.DocumentBuilder;
 16 import javax.xml.parsers.DocumentBuilderFactory;
 17 import javax.xml.transform.OutputKeys;
 18 import javax.xml.transform.Transformer;
 19 import javax.xml.transform.TransformerFactory;
 20 import javax.xml.transform.dom.DOMSource;
 21 import javax.xml.transform.stream.StreamResult;
 22 import org.slf4j.Logger;
 23 import org.slf4j.LoggerFactory;
 24 
 25 
 26 public class WXPayUtil {
 27 
 28     /**
 29      * XML格式字符串轉換爲Map
 30      *
 31      * @param strXML XML字符串
 32      * @return XML數據轉換後的Map
 33      * @throws Exception
 34      */
 35     public static Map<String, String> xmlToMap(String strXML) throws Exception {
 36         try {
 37             Map<String, String> data = new HashMap<String, String>();
 38             DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 39             DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
 40             InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
 41             org.w3c.dom.Document doc = documentBuilder.parse(stream);
 42             doc.getDocumentElement().normalize();
 43             NodeList nodeList = doc.getDocumentElement().getChildNodes();
 44             for (int idx = 0; idx < nodeList.getLength(); ++idx) {
 45                 Node node = nodeList.item(idx);
 46                 if (node.getNodeType() == Node.ELEMENT_NODE) {
 47                     org.w3c.dom.Element element = (org.w3c.dom.Element) node;
 48                     data.put(element.getNodeName(), element.getTextContent());
 49                 }
 50             }
 51             try {
 52                 stream.close();
 53             } catch (Exception ex) {
 54                 // do nothing
 55             }
 56             return data;
 57         } catch (Exception ex) {
 58             WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
 59             throw ex;
 60         }
 61 
 62     }
 63 
 64     /**
 65      * 將Map轉換爲XML格式的字符串
 66      *
 67      * @param data Map類型數據
 68      * @return XML格式的字符串
 69      * @throws Exception
 70      */
 71     public static String mapToXml(Map<String, String> data) throws Exception {
 72         DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 73         DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
 74         org.w3c.dom.Document document = documentBuilder.newDocument();
 75         org.w3c.dom.Element root = document.createElement("xml");
 76         document.appendChild(root);
 77         for (String key: data.keySet()) {
 78             String value = data.get(key);
 79             if (value == null) {
 80                 value = "";
 81             }
 82             value = value.trim();
 83             org.w3c.dom.Element filed = document.createElement(key);
 84             filed.appendChild(document.createTextNode(value));
 85             root.appendChild(filed);
 86         }
 87         TransformerFactory tf = TransformerFactory.newInstance();
 88         Transformer transformer = tf.newTransformer();
 89         DOMSource source = new DOMSource(document);
 90         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
 91         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 92         StringWriter writer = new StringWriter();
 93         StreamResult result = new StreamResult(writer);
 94         transformer.transform(source, result);
 95         String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
 96         try {
 97             writer.close();
 98         }
 99         catch (Exception ex) {
100         }
101         return output;
102     }
103 
104 
105     /**
106      * 生成帶有 sign 的 XML 格式字符串
107      *
108      * @param data Map類型數據
109      * @param key API密鑰
110      * @return 含有sign字段的XML
111      */
112     public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
113         return generateSignedXml(data, key, SignType.MD5);
114     }
115 
116     /**
117      * 生成帶有 sign 的 XML 格式字符串
118      *
119      * @param data Map類型數據
120      * @param key API密鑰
121      * @param signType 簽名類型
122      * @return 含有sign字段的XML
123      */
124     public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
125         String sign = generateSignature(data, key, signType);
126         data.put(WXPayConstants.FIELD_SIGN, sign);
127         return mapToXml(data);
128     }
129 
130 
131     /**
132      * 判斷簽名是否正確
133      *
134      * @param xmlStr XML格式數據
135      * @param key API密鑰
136      * @return 簽名是否正確
137      * @throws Exception
138      */
139     public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
140         Map<String, String> data = xmlToMap(xmlStr);
141         if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
142             return false;
143         }
144         String sign = data.get(WXPayConstants.FIELD_SIGN);
145         return generateSignature(data, key).equals(sign);
146     }
147 
148     /**
149      * 判斷簽名是否正確,必須包含sign字段,不然返回false。使用MD5簽名。
150      *
151      * @param data Map類型數據
152      * @param key API密鑰
153      * @return 簽名是否正確
154      * @throws Exception
155      */
156     public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
157         return isSignatureValid(data, key, SignType.MD5);
158     }
159 
160     /**
161      * 判斷簽名是否正確,必須包含sign字段,不然返回false。
162      *
163      * @param data Map類型數據
164      * @param key API密鑰
165      * @param signType 簽名方式
166      * @return 簽名是否正確
167      * @throws Exception
168      */
169     public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
170         if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
171             return false;
172         }
173         String sign = data.get(WXPayConstants.FIELD_SIGN);
174         return generateSignature(data, key, signType).equals(sign);
175     }
176 
177     /**
178      * 生成簽名
179      *
180      * @param data 待簽名數據
181      * @param key API密鑰
182      * @return 簽名
183      */
184     public static String generateSignature(final Map<String, String> data, String key) throws Exception {
185         return generateSignature(data, key, SignType.MD5);
186     }
187 
188     /**
189      * 生成簽名. 注意,若含有sign_type字段,必須和signType參數保持一致。
190      *
191      * @param data 待簽名數據
192      * @param key API密鑰
193      * @param signType 簽名方式
194      * @return 簽名
195      */
196     public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
197         Set<String> keySet = data.keySet();
198         String[] keyArray = keySet.toArray(new String[keySet.size()]);
199         Arrays.sort(keyArray);
200         StringBuilder sb = new StringBuilder();
201         for (String k : keyArray) {
202             if (k.equals(WXPayConstants.FIELD_SIGN)) {
203                 continue;
204             }
205             if (data.get(k).trim().length() > 0) // 參數值爲空,則不參與簽名
206                 sb.append(k).append("=").append(data.get(k).trim()).append("&");
207         }
208         sb.append("key=").append(key);
209         if (SignType.MD5.equals(signType)) {
210             return MD5(sb.toString()).toUpperCase();
211         }
212         else if (SignType.HMACSHA256.equals(signType)) {
213             return HMACSHA256(sb.toString(), key);
214         }
215         else {
216             throw new Exception(String.format("Invalid sign_type: %s", signType));
217         }
218     }
219 
220 
221     /**
222      * 獲取隨機字符串 Nonce Str
223      *
224      * @return String 隨機字符串
225      */
226     public static String generateNonceStr() {
227         return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
228     }
229 
230 
231     /**
232      * 生成 MD5
233      *
234      * @param data 待處理數據
235      * @return MD5結果
236      */
237     public static String MD5(String data) throws Exception {
238         java.security.MessageDigest md = MessageDigest.getInstance("MD5");
239         byte[] array = md.digest(data.getBytes("UTF-8"));
240         StringBuilder sb = new StringBuilder();
241         for (byte item : array) {
242             sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
243         }
244         return sb.toString().toUpperCase();
245     }
246 
247     /**
248      * 生成 HMACSHA256
249      * @param data 待處理數據
250      * @param key 密鑰
251      * @return 加密結果
252      * @throws Exception
253      */
254     public static String HMACSHA256(String data, String key) throws Exception {
255         Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
256         SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
257         sha256_HMAC.init(secret_key);
258         byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
259         StringBuilder sb = new StringBuilder();
260         for (byte item : array) {
261             sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
262         }
263         return sb.toString().toUpperCase();
264     }
265 
266     /**
267      * 日誌
268      * @return
269      */
270     public static Logger getLogger() {
271         Logger logger = LoggerFactory.getLogger("wxpay java sdk");
272         return logger;
273     }
274 
275     /**
276      * 獲取當前時間戳,單位秒
277      * @return
278      */
279     public static long getCurrentTimestamp() {
280         return System.currentTimeMillis()/1000;
281     }
282 
283     /**
284      * 獲取當前時間戳,單位毫秒
285      * @return
286      */
287     public static long getCurrentTimestampMs() {
288         return System.currentTimeMillis();
289     }
290 
291     /**
292      * 生成 uuid, 即用來標識一筆單,也用作 nonce_str
293      * @return
294      */
295     public static String generateUUID() {
296         return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
297     }
298 
299 }
View Code

二、移動端喚起微信支付的組件,首先發起開始支付的動做,服務端就會相應的調用預支付(統一下單)接口https://api.mch.weixin.qq.com/pay/unifiedorder,注意該過程須要對參數進行簽名,而後將帶有簽名字段的參數做爲接口形參,過程比較繁瑣建議封裝一下,後續的操做還有會用到的,最後拿到微信服務器返回的結果後解析出移動端須要的參數,記得簽名後返給前端。好了部分源碼能夠參考一下:node

 1        String body = "抓樂GO";
 2             String nonceStr = WXPayUtil.generateNonceStr();     //獲取隨機字符串
 3             String outTradeNo = WXPayUtil.generateUUID();         //商戶訂單號(本次交易訂單號)
 4             String tradeType = "APP";                            //支付類型:APP
 5             Double money = tokenInfo.getMoney() * 100.0;         //金額(單位/分)
 6             //int totalFee = (new Double(money)).intValue();
 7             int totalFee = (int) Math.ceil(money);
 8             logger.info("------------------支付金額爲:{}",totalFee);
 9             String parames = "<xml><appid>"+appID+"</appid><body>"+body+"</body><mch_id>"
10                     +mchID+"</mch_id><nonce_str>"+nonceStr+"</nonce_str><notify_url>"+notifyUrl
11                     +"</notify_url><out_trade_no>"+outTradeNo+"</out_trade_no><spbill_create_ip>"+spbillCreateIp
12                     +"</spbill_create_ip><total_fee>"+totalFee+"</total_fee><trade_type>"+tradeType
13                     +"</trade_type></xml>";
14             Map<String, String> mapXML = WXPayUtil.xmlToMap(parames);    //將不包含sign字段的xml字符串轉成map
15             String sign = WXPayUtil.generateSignature(mapXML, appKey);    //生成簽名
16 
17             String signParames = "<xml><appid>"+appID+"</appid><body>"+body+"</body><mch_id>"
18                     +mchID+"</mch_id><nonce_str>"+nonceStr+"</nonce_str><notify_url>"+notifyUrl
19                     +"</notify_url><out_trade_no>"+outTradeNo+"</out_trade_no><spbill_create_ip>"+spbillCreateIp
20                     +"</spbill_create_ip><total_fee>"+totalFee+"</total_fee><trade_type>"+tradeType
21                     +"</trade_type><sign>"+sign+"</sign></xml>";        //預支付接口xml參數(包含sign)
22             Map<String, String> mapXML1 = WXPayUtil.xmlToMap(signParames);
23             boolean boo = WXPayUtil.isSignatureValid(mapXML1, appKey);    //校驗簽名是否正確
 1   if("SUCCESS".equals(dataMap.get("result_code"))){
 2                     logger.info("預支付接口調用成功:");
 3                     //預支付調用成功
 4                     //二次簽名
 5                     Map<String,String> signMap = new LinkedHashMap<String,String>();
 6                     signMap.put("appid", dataMap.get("appid"));
 7                     signMap.put("partnerid", dataMap.get("mch_id"));
 8                     signMap.put("prepayid", dataMap.get("prepay_id"));
 9                     signMap.put("package", "Sign=WXPay");
10                     signMap.put("noncestr", WXPayUtil.generateNonceStr());
11                     signMap.put("timestamp", String.valueOf(System.currentTimeMillis()/1000));
12                     String appSign = WXPayUtil.generateSignature(signMap, appKey);
13                     signMap.put("sign", appSign);
14                     signMap.put("outTradeNo", outTradeNo);        //支付訂單號
15                     //String signXml = WXPayUtil.mapToXml(signMap);
16                     //System.out.println(signXml);
17                     dataJson.put("code", Constants.HTTP_RESPONSE_SUCCESS);
18                     dataJson.put("msg", Constants.HTTP_RESPONSE_SUCCESS_MSG);
19                     dataJson.put("data", signMap);
20                     logger.info("返給APP的二次簽名數據:{}",signMap);

三、移動端拿到支付參數後就會真正調起支付操做,這時後端只需作一個供微信回調的接口,該接口的做用主要是接受每次支付的支付結果。注意:該回調地址須要在發起預支付接口時務必告訴微信服務器,並且還要保證可以暢通無阻。當完成一筆支付操做後,微信服務器就馬上會調用你提供的自定義回調接口告訴你支付結果,你只需完成支付成功後的業務邏輯,即視爲本次支付過程結束。程序員

 1 public String payCallback(HttpServletRequest request, HttpServletResponse response){
 2         BufferedReader reader = null;
 3         try {
 4             reader = request.getReader();
 5             String line = "";
 6             String xmlString = null;
 7             StringBuffer inputString = new StringBuffer();
 8             while ((line = reader.readLine()) != null) {
 9                 inputString.append(line);
10             }
11             xmlString = inputString.toString();
12             if(WXPayUtil.isSignatureValid(xmlString,appKey)){
13                 logger.info("微信支付結果{}",xmlString);
14                 request.getReader().close();
15                 Map<String, String> resultMap = WXPayUtil.xmlToMap(xmlString);     //支付結果
16                 if("SUCCESS".equals(resultMap.get("return_code"))){
               支付成功後的業務邏輯....

四、固然,微信也專門提供了查詢某筆支付訂單的支付結果的接口https://api.mch.weixin.qq.com/pay/orderquery,詳情自行查詢。數據庫

 1 public String getOrderResult(String outTradeNo){
 2         logger.info("查詢支付訂單號{}支付結果================",outTradeNo);
 3         Map<String,Object> dataJson = new LinkedHashMap<String,Object>();
 4         try {
 5             String nonceStr = WXPayUtil.generateNonceStr();     //獲取隨機字符串
 6             //請求參數
 7             String parames = "<xml><appid>"+appID+"</appid><mch_id>"+mchID+"</mch_id>"
 8                             +"<nonce_str>"+nonceStr+"</nonce_str>"
 9                             + "<out_trade_no>"+outTradeNo+"</out_trade_no></xml>";
10             String signXMLData = WXPayUtil.generateSignedXml(WXPayUtil.xmlToMap(parames), appKey);   //生成帶有簽名的xml
11             String queryResult = HttpClient.doPostXML(orderQueryURL, signXMLData);        //查詢結果xml
12             if(WXPayUtil.isSignatureValid(queryResult,appKey)){
13                 Map<String, String> wxPayResult = WXPayUtil.xmlToMap(queryResult);            //微信支付的結果通知
View Code

OK,到這兒整個支付業務算是真正跑通了,勉強畫條暫時的分割線吧。後端

 


  •  爲何調用統一下單(預支付)接口在正確簽名後,仍是調不通,總提示簽名錯誤?

這個問題確實對於不少新手來講是狠TM扯淡的,調不通還老提示簽名錯誤多是由於:http請求的參數列表中body那個字段你傳的是中文,而且微信開發文檔中的案例模板也是中文。api

解決方案:只需將最終發送的參數列表進行編碼處理便可,可是你也能夠所有傳入英文。安全

1 //若是校驗經過,則調用預支付
2                 logger.info("開始調用微信預支付接口:https://api.mch.weixin.qq.com/pay/unifiedorder");
3                 signParames = new String(signParames.getBytes("UTF-8"), "ISO-8859-1");
4                 String result = HttpClient.doPostXML(payURL, signParames);
5                 Map<String, String> dataMap = WXPayUtil.xmlToMap(result);
6                 logger.info("預支付結果:{}",result);
  • 爲何支付會給產生多條充值明細,而且還給用戶累加屢次餘額?

這個問題就有點考驗你寫接口的質量了,出現僅支付一次產生多條支付明細記錄的狀況,首先是由於你沒有作好在成功拿到微信回調結果後及時對當前支付記錄作好重複處理的邏輯,由於那哥們兒發起一筆支付請求後在成功拿到支付結果沒有告訴微信支付成功,因此微信認爲通知失敗,微信會經過必定的策略按期從新發起通知,儘量提升通知的成功率,但微信不保證通知最終能成功,通知頻率爲15/15/30/180/1800/1800/1800/1800/3600,單位:秒。因此不斷處理同一筆支付訂單。其次就是沒有考慮併發的狀況,須要對拿到的回調結果作線程安全的處理,能夠有倆種方案:第一種就是在數據庫層面上作限制,設置聯合主鍵將重複操做的支付記錄數據擋在外面不容許插入數據庫;第二種是在業務層加鎖,在處理每筆支付結果時判斷是否已經處理過了,若是處理過就忽略當前回調結果不然正常處理。提醒一點:不要直接在方法上直接添加synchronized,還有在加鎖的時候儘可能將鎖的粒度控制到最小,不然會影響接口的性能。(參考:http://www.cnblogs.com/1315925303zxz/p/7561236.html服務器

更多實戰技能,請關注本人技術文章公衆號:xz_303  期待與君共勉!

相關文章
相關標籤/搜索