一、背景
最近公司给第三方开发了一个公众号,其中最重要的功能是支付,由于是第一次开发,遇到的坑特别的多,截止我写博客时,支付已经完成,在这里我把遇到的坑记录一下(不涉及退款)。不得不吐槽一下,腾讯这么大的公司,写的代码真的烂,变量命名不规范(后来又开发了公众号分享,发现对于同一个变量,两个地方的变量名不同),文档写的垃圾,而且好多时候,有返回的错误码但是文档上没说明,另外线上线下测试极其不方便。
二、参考资料
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
九、填坑之路
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/。规律就是去掉最后一个反斜杠的内容。