微信公众号支付备忘及填坑之路-java

一、背景  

  最近公司给第三方开发了一个公众号,其中最重要的功能是支付,由于是第一次开发,遇到的坑特别的多,截止我写博客时,支付已经完成,在这里我把遇到的坑记录一下(不涉及退款)。不得不吐槽一下,腾讯这么大的公司,写的代码真的烂,变量命名不规范(后来又开发了公众号分享,发现对于同一个变量,两个地方的变量名不同),文档写的垃圾,而且好多时候,有返回的错误码但是文档上没说明,另外线上线下测试极其不方便。

二、参考资料

  1、JSAPI支付开发文档:主要是支付开发过程中接口、流程、注意事项、常见问题等;  

  2、微信公众平台:里面有公众号的运维配置,当然也有开发配置,咱们主要是用到了开发配置;  

  3、微信商户平台:要支付必须有微信商户,里面需配置开发设置;  

  4、花生壳:主要是用到花生壳的内网穿透,由于微信支付开发过程中,需要公网的微信服务器访问本地开发的电脑,包括重定向之类的,所以需要内网穿透,这样微信才能访问到局域网电脑;花生壳可以免费注册使用,但是呢,免费的带宽为1M,由于带宽太小,根本穿透不进来,所以我们后来购买了带宽,发现带宽为3M时,足以应付日场使用。当然Ngrok也可以内网穿透,但是带宽太低了;  

  5、微信公众平台支付接口调试工具:这个平台可以校验自己生成的sign与微信服务器生成的sign是否一致,很有用,尤其是提示签名失败时。

 

三、说明  

  1、在支付接入过程中,如果不顺利,不要怀疑微信服务器有问题,多想想自己哪儿可能出问题了,因为接入微信支付的商户已经达好几亿了,微信不可能出问题的;

  2、微信的开发文档中说到,为保证商户接入质量,提升交易安全及用户体验,微信做了一个支付仿真沙箱环境,还说什么“微信支付的合作服务商在正式上线交易前,必须先根据本文指引完成验收”。这个别听微信的,他们的这个沙箱环境根本不成熟,用这个是瞎折腾,咱们可以直接用真实环境测试;

  3、微信支付开发文档中提到“协议规则”,首先传输方式是HTTPS,是指微信服务器和商户服务器通讯协议是HTTPS,但是不要求微信客户端与商户服务器之间的通讯协议是HTTPS,用HTTP也行,也就是说商户服务器不用自己准备SSL证书。其次所有微信支付有关的接口都用POST。最后,提交和返回的数据都是XML格式,不是目前普标接受的JSON格式,这个要注意。 

 

四、微信支付流程

  第3步:微信客户端请求商户后台系统,主要是携带商品的价格,其次如有必要携带商品的描述,或者传递一个参数可以让服务器唯一确定商品价格与商品描述;

  第4步:这一步是在我们商户后台的数据库的"订单表"里面插入一条数据,数据里包含的基本信息有:用户id,订单金额,订单商品及描述,支付状态;

  第5步:整个微信公众号支付流程图中,最重要的一步,这一步的目的是返回预支付订单号prepay_id,这一步起到承上启下的作用;这一步的详细代码见后文 “统一下单”;开发文档 ;

  第6步:根据第5步形成的prepay_id附带其他参数一起发送给微信客户端,开发文档

  第7步:携带第6步的参数请求微信,这一步也是次重要的一步,开发文档常见问题

  第10步、第11步:支付完成后,微信服务器通知商户后台系统,后台系统验证完毕后,需要改变商户订单表里订单的状态,即第4步的订单状态。之后需要后台系统给微信服务器返回特定的信息。但是开发文档中也说了,绝大部分情况下,我们支付成功,能接收到微信的通知,但是有时收不到,这时我们需要主动调用微信支付【查询订单API】确认订单状态 。这一步的详细代码见后文 “微信支付结果通知及商户服务器响应”,开发文档 。

 

 

五、微信公众号开通微信支付

  见参考文档:微信公众号怎么申请微信支付。开通之后,从公众号内可以看到已经关联的商户(图1),同时从微信商户里可以看到关联的微信公众号(图2)。 

 

六、公众号开发参数配置

  1、设置——公众号设置——功能设置:设置网页授权域名

  微信是这么说的“用户在网页授权页同意授权给公众号后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠”,我是这里理解的:网页需要重定向时才可以跳转到该域名下的地址。由于是在微信里面嵌套h5网页,所以网页的域名需要受到微信的监管。注意这里的设置的是域名,不是ip地址之类的,也不能带有端口号。这里就需要用到花生壳的内网穿透了。

    假如服务器是tomcat,端口号是8080,微信给的文件是xxx.txt,花生壳地址是http://123asd.zicp.vip

    1)首先你将微信给的xxx.txt文件下载到ROOT目录,然后启动tomcat,在本地浏览器用localhost:8080/xxx.txt访问,如果正常不能正常访问,那就是tomcat启动失败了,自己检查下log日志看看;

    2)启动花生壳客户端,并且配置内网主机为localhost:8080,在本地浏览器访问http://123asd.zicp.vip/xxx.txt,这个受制于带宽的原因,可能需要等一会时间,也可能很快,如果不能访问成功,花生壳重启试试;

    3)配置网页授权域名为123asd.zicp.vip。

    

 

2、开发——基本配置。

  1)这里需要配置开发者密码(特别重要)、ip白名单(通过开发者ID及密码调用获取access_token接口时,需要设置访问来源IP为白名单)。

  2)服务器配置,验证token。这一步也是微信防止没有经过鉴权的服务器访问自己的服务器做的一个验证吧,可以看做是为了安全考虑。这个需要服务端代码支持,代码见后面。这个配置有时候需要多试几次,有时提示“系统繁忙”。

 

七、微信商户开发参数配置

  这里的支付授权目录配置的就是微信公众号调起支付页面的URL,和后端服务器无关。这里有坑,详见下面的填坑之路。

 

八、后端代码

  1、微信常量类

 1 public class WeixinConstant {
 2 
 3     /**
 4      * 公众号-id
 5      */
 6     public static final String WX_CON_APPID = "xxx";
 7     /**
 8      * 公众号-秘钥
 9      */
10     public static final String WX_CON_APPSECRET = "yyy";
11 
12 
13     /**
14      * 商户-商户号
15      */
16     public static final String WX_CON_MCH_ID = "zzz";
17     /**
18      * 商户-商户密钥
19      */
20     public static final String WX_CON_MCHSECRET = "uuu";
21 
22 
23     /**
24      * 请求结果-成功
25      */
26     public static final String RESULT_SUCCESS = "SUCCESS";
27     /**
28      * 请求结果-失败
29      */
30     public static final String RESULT_FAIL = "FAIL";
31 
32 
33     /**
34      * 微信支付成功回调响应Map
35      */
36     public static final Map<String, String> WX_CON_PAY_RESPMAP = new HashMap<String, String>() {{
37         put("return_code", "SUCCESS");
38         put("return_msg", "OK");
39     }};
40 
41 
42     /**
43      * 接口-用户-通过code换取网页授权access_token URL
44      */
45     public static final String WX_URL_USER_GETOPENIDANDTOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";
46 
47     /**
48      * 接口-用户-拉取用户信息
49      */
50     public static final String WX_URL_USER_GETUSERINFO = "https://api.weixin.qq.com/sns/userinfo";
51 
52     /**
53      * 接口-支付-统一下单URL
54      */
55     public static final String WX_URL_PAY_UNIFIEDORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";
56 
57     /**
58      * 接口-支付-查询订单URL
59      */
60     public static final String WX_URL_PAY_ORDERQUERY = "https://api.mch.weixin.qq.com/pay/orderquery";
61 
62 }
View Code

  2、工具类/方法

  2.1、map与xml转化工具(微信工具类提供的。因为我们熟悉使用map,所以我们把请求参数放在有序map里,然后再将map转化为xml)

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

  2.2、服务器token验证工具

 1 public class CheckoutUtil {
 2 
 3 
 4     // 与接口配置信息中的Token要一致
 5     private static String token = "83cea0ca4c9ee2dd60c15f2df17de4a2";
 6 
 7     /**
 8      * 验证签名
 9      *
10      * @param signature
11      * @param timestamp
12      * @param nonce
13      * @return
14      */
15     public static boolean checkSignature(String signature, String timestamp, String nonce) {
16         String[] arr = new String[] { token, timestamp, nonce };
17         // 将token、timestamp、nonce三个参数进行字典序排序
18         // Arrays.sort(arr);
19         sort(arr);
20         StringBuilder content = new StringBuilder();
21         for (int i = 0; i < arr.length; i++) {
22             content.append(arr[i]);
23         }
24         MessageDigest md = null;
25         String tmpStr = null;
26 
27         try {
28             md = MessageDigest.getInstance("SHA-1");
29             // 将三个参数字符串拼接成一个字符串进行sha1加密
30             byte[] digest = md.digest(content.toString().getBytes());
31             tmpStr = byteToStr(digest);
32         } catch (NoSuchAlgorithmException e) {
33             e.printStackTrace();
34         }
35         content = null;
36         // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
37         return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
38     }
39 
40     /**
41      * 将字节数组转换为十六进制字符串
42      *
43      * @param byteArray
44      * @return
45      */
46     private static String byteToStr(byte[] byteArray) {
47         String strDigest = "";
48         for (int i = 0; i < byteArray.length; i++) {
49             strDigest += byteToHexStr(byteArray[i]);
50         }
51         return strDigest;
52     }
53 
54     /**
55      * 将字节转换为十六进制字符串
56      *
57      * @param mByte
58      * @return
59      */
60     private static String byteToHexStr(byte mByte) {
61         char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
62         char[] tempArr = new char[2];
63         tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
64         tempArr[1] = Digit[mByte & 0X0F];
65         String s = new String(tempArr);
66         return s;
67     }
68     public static void sort(String a[]) {
69         for (int i = 0; i < a.length - 1; i++) {
70             for (int j = i + 1; j < a.length; j++) {
71                 if (a[j].compareTo(a[i]) < 0) {
72                     String temp = a[i];
73                     a[i] = a[j];
74                     a[j] = temp;
75                 }
76             }
77         }
78     }
79 
80 }
View Code

  2.3、支付加签工具

 1 public class WeixinSignUtil {
 2 
 3     private final static Log logger = LogFactory.getLog(WeixinSignUtil.class);
 4 
 5 
 6     /**
 7      * @param requestDataMap
 8      * @return
 9      * @function 微信参数签名
10      * @remark flag=true 加密的参数为sign  flag=false 加密的参数是paySign
11      */
12     public static void getWeixinSign(Map<String, String> requestDataMap, Boolean isRequestFlag) {
13         String paramStrTemp = "";
14         Iterator<Map.Entry<String, String>> entries = requestDataMap.entrySet().iterator();
15         while (entries.hasNext()) {
16             Map.Entry<String, String> entry = entries.next();
17             paramStrTemp += (entry.getKey() + "=" + entry.getValue() + "&");
18         }
19         String paramStr = paramStrTemp + "key=" + WeixinConstant.WX_CON_MCHSECRET;
20         logger.info("签名前字符串:" + paramStr);
21         String sign = DigestUtils.md5DigestAsHex(paramStr.getBytes()).toUpperCase();
22         if (isRequestFlag) {
23             logger.info("加签为sign:" + sign);
24             requestDataMap.put("sign", sign);
25         } else {
26             logger.info("加签为paySign:" + sign);
27             requestDataMap.put("paySign", sign);
28         }
29     }
30 
31 }
View Code

  2.4、post方式提交xml数据工具(微信支付这样要求的)

 1 public Map<String, String> wxPayRequest(LinkedHashMap<String, String> requestDataMap, String requestUrl) {
 2         Map<String, String> wxResponseMap;
 3         try {
 4             URLConnection con = new URL(requestUrl).openConnection();
 5             con.setDoOutput(true);
 6             con.setRequestProperty("Cache-Control", "no-cache");
 7             con.setRequestProperty("Content-Type", "text/xml");
 8 
 9             OutputStreamWriter out = new OutputStreamWriter(con.getOutputStream());
10             out.write(new String(WXPayUtil.mapToXml(requestDataMap).getBytes("UTF-8")));
11             out.flush();
12             out.close();
13 
14             String line = "";
15             StringBuffer wxResponseStr = new StringBuffer();
16             BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
17             for (line = br.readLine(); line != null; line = br.readLine()) {
18                 wxResponseStr.append(line);
19             }
20 
21             logger.info("wxRespStr::" + wxResponseStr);
22             wxResponseMap = WXPayUtil.xmlToMap(wxResponseStr.toString());
23             logger.info("wxRespMap:" + wxResponseMap);
24             return wxResponseMap;
25         } catch (IOException e) {
26             e.printStackTrace();
27             return null;
28         } catch (Exception e) {
29             e.printStackTrace();
30             return null;
31         }
32     }
View Code

  3、服务器token验证(配合上文的   六、公众号开发参数配置——> 2)  

 1 @GetMapping("weixinVerifyUrl")
 2     @LoggerManage(logDescription = "系统-微信校验URL的合法性")
 3     public void tokenVarify(HttpServletRequest request, HttpServletResponse response) {
 4 
 5         // 微信加密签名
 6         String signature = request.getParameter("signature");
 7         // 时间戳
 8         String timestamp = request.getParameter("timestamp");
 9         // 随机数
10         String nonce = request.getParameter("nonce");
11         // 随机字符串
12         String echostr = request.getParameter("echostr");
13 
14         // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
15         if (signature != null && CheckoutUtil.checkSignature(signature, timestamp, nonce)) {
16             try {
17                 response.getWriter().write(echostr);
18             } catch (IOException e) {
19                 e.printStackTrace();
20             }
21             return;
22         }
23         return;
24     }
View Code

  4、统一下单

 1 public Map<String, Object> weixinPay(DealRecord dealRecord, Byte memberGrade, String userId) {
 2         Map<String, Object> retMap = new HashMap<>(2);
 3 
 4         /**
 5          * 1、用户背景校验
 6          */
 7        
 8 
 9         /**
10          * 2、准备请求参数
11          */
12         LinkedHashMap<String, String> requestDataMap = new LinkedHashMap<>();
13         requestDataMap.put("appid", WeixinConstant.WX_CON_APPID);
14         requestDataMap.put("attach", memberGrade + "");
15         requestDataMap.put("body", dealRecord.getGoodsName() + "-" + memberGrade);
16         requestDataMap.put("device_info", "WEB");
17         requestDataMap.put("fee_type", "CNY");
18         requestDataMap.put("mch_id", WeixinConstant.WX_CON_MCH_ID);
19         requestDataMap.put("nonce_str", UUIDUtil.getUUID());
20         requestDataMap.put("notify_url", weixinPayNotifyUrl);
21         requestDataMap.put("openid", dealRecord.getOpenid());
22         requestDataMap.put("out_trade_no", dealRecord.getId());
23         requestDataMap.put("sign_type", "MD5");
24         requestDataMap.put("spbill_create_ip", dealRecord.getDeviceIp());
25         requestDataMap.put("time_start", DateUtil.formatDate(dealRecord.getCreateTime(), DateUtil.SDF_yyyyMMddHHmmss));
26         // 虽然文档上total_fee为int,但是String也可以
27         requestDataMap.put("total_fee", dealRecord.getAmount() + "");
28         requestDataMap.put("trade_type", "JSAPI");
29 
30         /**
31          * 3、微信请求参数签名+下单
32          */
33         WeixinSignUtil.getWeixinSign(requestDataMap, true);
34         Map<String, String> wxResponseMap = weixinService.wxPayRequest(requestDataMap, WeixinConstant.WX_URL_PAY_UNIFIEDORDER);
35 
36         /**
37          * 4、逻辑处理
38          */
39         String return_code = wxResponseMap.get("return_code");
40         if (WeixinConstant.RESULT_FAIL.equals(return_code)) {
41             retMap.put("result", 0);
42             retMap.put("data", wxResponseMap);
43             return retMap;
44         } else if (WeixinConstant.RESULT_SUCCESS.equals(return_code)) {
45             String prepay_id = wxResponseMap.get("prepay_id");
46 
47             LinkedHashMap<String, String> wxDataMap = new LinkedHashMap<>(6);
48             wxDataMap.put("appId", WeixinConstant.WX_CON_APPID);
49             wxDataMap.put("nonceStr", UUIDUtil.getUUID());
50             wxDataMap.put("package", "prepay_id=" + prepay_id);
51             wxDataMap.put("signType", "MD5");
52             wxDataMap.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
53 
54             // 微信请求参数签名
55             WeixinSignUtil.getWeixinSign(wxDataMap, false);
56             
57             retMap.put("result", 0);
58             retMap.put("data", wxDataMap);
59             return retMap;
60         }
61         retMap.put("result", 9);
62         return retMap;
63     }
View Code

  5、微信支付结果通知及商户服务器响应

 1 /**
 2      * @param request
 3      * @function 微信/支付通知
 4      */
 5     @RequestMapping(value = "/weixin/pay/notify")
 6     @LoggerManage(logDescription = "用户-微信/支付/回调通知")
 7     public void weixinPayNotify(HttpServletRequest request, HttpServletResponse response) {
 8         BufferedReader reader = null;
 9         StringBuilder wxRequestStr = new StringBuilder();
10         Map<String, String> wxRequestMap = null;
11         try {
12             reader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
13             String line = null;
14             while ((line = reader.readLine()) != null) {
15                 wxRequestStr.append(line);
16             }
17 
18             logger.info("wxNotifyReqStr:" + wxRequestStr.toString());
19             wxRequestMap = WXPayUtil.xmlToMap(wxRequestStr.toString());
20             logger.info("wxNotifyReqMap:" + wxRequestMap);
21         } catch (IOException e) {
22             e.printStackTrace();
23         } catch (Exception e) {
24             e.printStackTrace();
25         } finally {
26             try {
27                 if (null != reader) {
28                     reader.close();
29                 }
30             } catch (IOException e) {
31             }
32         }
33 
34         String return_code = wxRequestMap.get("return_code");
35         if (WeixinConstant.RESULT_FAIL.equals(return_code)) {
36             // 支付失败
37             logger.info("交易失败!");
38         } else if (WeixinConstant.RESULT_SUCCESS.equals(return_code)) {
39             
40             try {
41                 BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
42                 out.write(WXPayUtil.mapToXml(WeixinConstant.WX_CON_PAY_RESPMAP).getBytes("UTF-8"));
43                 out.flush();
44                 out.close();
45             } catch (IOException e) {
46                 e.printStackTrace();
47             } catch (Exception e) {
48                 e.printStackTrace();
49             }
50         }
51     }
View Code

九、填坑之路

  1、统一下单接口,请求微信,提示签名失败

1)仔细检查统一下单接口里携带的参数,参数大小写、参数是否必须携带。比如是appid而不是appId;openid文档上写的是“”,但是它在备注里写到trade_type=JSAPI时(即JSAPI支付),此参数必传,这个注意啊;

2)加密方式是否正确,可以把自己生成的sign与微信公众平台支付接口调试工具生成的sign做对比,如果不一致,那证明你的加签方式不正确,请仔细阅读加签安全规范这篇文章,比如里面提到的按照参数名ASCII字典序排序、拼接商户密钥、MD5加密并转化为大写;

3)如果第二步没问题,好吧,不要犹豫,去重置商户秘钥吧,注意是商户秘钥,因为支付加签时和商户秘钥有关,我当时就是卡在这一步了好久,至今记忆深刻;

4)如果第三步没问题,那么就是加签方法错误了,推荐使用微信工具包里面的加签方法。

  2、微信内h5调起支付,提示“支付验证签名失败”

1)仔细检查参数,参数大小写、参数是否必须携带。比如是appId而不是appid(统一下单与这个正好相反);package的value必须是“prepay_id=xxx”形式;

2)统一下单的签名的key是sign,而微信调起h5的签名的key是paySign,这个要注意;

3)加密方式是否正确,可以把自己生成的sign与微信公众平台支付接口调试工具生成的sign做对比,如果不一致,那证明你的加签方式不正确,请仔细阅读加签安全规范这篇文章,比如里面提到的按照参数名ASCII字典序排序、拼接商户密钥、MD5加密并转化为大写;

4)如果第三步没问题,好吧,不要犹豫,去重置商户秘钥吧。

  3、微信内h5调起支付,提示“当前页面未注册 http://xxx”

1)检查微信商户平台–>产品中心–>开发配置–>支付授权目录是否配置;

2)如果第一步没问题,但还是报错,那就得注意配置了。比如你的支付页是http://www.abc.com/pay/wxjspay/id/50.html,那配置为http://www.abc.com/pay/wxjspay/id/;假如是http://www.abc.com/pay/wxjspay.php,那配置为http://www.abc.com/pay/。规律就是去掉最后一个反斜杠的内容。

  

在全栈的道路上,积极向上、成熟稳重、谦虚好学、怀着炽热的心向前方的走得更远。
原文地址:https://www.cnblogs.com/DDgougou/p/10812418.html