记得上次接触微信支付是2016年底,那次也是我程序生涯中首次碰及支付业务,慌张谈不上但是懵逼怀疑时时都有。说起第三方登录或者支付,想必都清楚是直接调用人家现成的API,完全没有开发成本和技术含量。但是我想说:如果你没有过一次从零开发并维护过一个完整支付模块的话,我相信有很多坑对你来说都是黑盒的,也许现在的公司或多或少都涵盖了支付业务,可能被早先的老程序员们已经维护的差不多完美了,完全是可以当成模板功能来使用,新手只需复制大体功能架子填充支付后的业务逻辑即可。
好了,切入正题吧。为什么突然在各位前辈面前摆上这么一篇没有任何技术含量的文章呢?因为好久没有在博客园发布过文章了,刚刚瞻仰完各位大佬分享的东西后心里很空虚。其次是主要原因,由于前几天休假刚回来就被刚刚入职的哥们拉住绕了一会儿,说这俩天测试人员反应充一次钱之后会看到好几条充值记录并且是偶现的,而且相应的游戏币也累加了很多次等等,然后还有一个更坑的现象就是照着微信开发文档调用接口,唤醒微信支付的组件开始调用预支付(统一下单)接口时抛签名错误的异常,竟然调不通(声明:签名方式已经是确保无误的)。。。真的,听他一说还真回忆起当年那个手忙脚乱的自己,在开发过程中各种懵逼、各种怀疑人生。所以今天就把这些踩坑经历分享一下,仅献给圈里刚刚接触或要开始接手支付业务的朋友,希望各位在铸造支付模块时能够一马平川。
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 }
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); //微信支付的结果通知
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)