实战技能:小小微信支付业务,何必虚惊一场

记得上次接触微信支付是2016年底,那次也是我程序生涯中首次碰及支付业务,慌张谈不上但是懵逼怀疑时时都有。说起第三方登录或者支付,想必都清楚是直接调用人家现成的API,完全没有开发成本和技术含量。但是我想说:如果你没有过一次从零开发并维护过一个完整支付模块的话,我相信有很多坑对你来说都是黑盒的,也许现在的公司或多或少都涵盖了支付业务,可能被早先的老程序员们已经维护的差不多完美了,完全是可以当成模板功能来使用,新手只需复制大体功能架子填充支付后的业务逻辑即可。

好了,切入正题吧。为什么突然在各位前辈面前摆上这么一篇没有任何技术含量的文章呢?因为好久没有在博客园发布过文章了,刚刚瞻仰完各位大佬分享的东西后心里很空虚。其次是主要原因,由于前几天休假刚回来就被刚刚入职的哥们拉住绕了一会儿,说这俩天测试人员反应充一次钱之后会看到好几条充值记录并且是偶现的,而且相应的游戏币也累加了很多次等等,然后还有一个更坑的现象就是照着微信开发文档调用接口,唤醒微信支付的组件开始调用预支付(统一下单)接口时抛签名错误的异常,竟然调不通(声明:签名方式已经是确保无误的)。。。真的,听他一说还真回忆起当年那个手忙脚乱的自己,在开发过程中各种懵逼、各种怀疑人生。所以今天就把这些踩坑经历分享一下,仅献给圈里刚刚接触或要开始接手支付业务的朋友,希望各位在铸造支付模块时能够一马平川。

准备工作:

  • 申请注册微信开发平台账号,申请需支付300元,心里有个数啊。

  • 在注册通过之后,到管理中心申请添加你们的APP应用。

  • 如果APP应用审核通过之后,就可以申请开放微信支付功能,比如像分享到朋友圈、微信登录等功能都是自动开放的,无需申请。

  • 最后如果微信支付功能审核也通过后,微信会给你分配一个微信商户号,基本包含了:登录账户号、密码、APPID和支付商户号等信息,这些信息要自己保留好,后续调用支付相关的接口时会用到。

 服务端支付业务开发步骤:

 1、登录微信商户平台,注意是商户平台不是开放平台,根据业务场景选择适合自己的支付类型,进入之后就可以看看具体的API列表,除此之外还提供了业务场景举例、业务流程时序图等非常清晰。提醒一点微信还提供了专门的demo压缩包,里面包含了工具类WXPayUtil建议下载,因为相信你能用到,是百分百能用到。

  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

2、移动端唤起微信支付的组件,首先发起开始支付的动作,服务端就会相应的调用预支付(统一下单)接口https://api.mch.weixin.qq.com/pay/unifiedorder,注意该过程需要对参数进行签名,然后将带有签名字段的参数作为接口形参,过程比较繁琐建议封装一下,后续的操作还有会用到的,最后拿到微信服务器返回的结果后解析出移动端需要的参数,记得签名后返给前端。好了部分源码可以参考一下:

 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);

3、移动端拿到支付参数后就会真正调起支付操作,这时后端只需做一个供微信回调的接口,该接口的作用主要是接受每次支付的支付结果。注意:该回调地址需要在发起预支付接口时务必告诉微信服务器,而且还要保证能够畅通无阻。当完成一笔支付操作后,微信服务器就立刻会调用你提供的自定义回调接口告诉你支付结果,你只需完成支付成功后的业务逻辑,即视为本次支付过程结束。

 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"))){
               支付成功后的业务逻辑....

4、当然,微信也专门提供了查询某笔支付订单的支付结果的接口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那个字段你传的是中文,并且微信开发文档中的案例模板也是中文。

解决方案:只需将最终发送的参数列表进行编码处理即可,但是你也可以全部传入英文。

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  期待与君共勉!

 

posted @ 2018-06-01 16:46  禁心尽力  阅读(1683)  评论(2编辑  收藏  举报