1、背景 php
最近公司給第三方開發了一個公衆號,其中最重要的功能是支付,因爲是第一次開發,遇到的坑特別的多,截止我寫博客時,支付已經完成,在這裏我把遇到的坑記錄一下(不涉及退款)。不得不吐槽一下,騰訊這麼大的公司,寫的代碼真的爛,變量命名不規範(後來又開發了公衆號分享,發現對於同一個變量,兩個地方的變量名不一樣),文檔寫的垃圾,並且好多時候,有返回的錯誤碼可是文檔上沒說明,另外線上線下測試極其不方便。html
2、參考資料java
一、JSAPI支付開發文檔:主要是支付開發過程當中接口、流程、注意事項、常見問題等; node
二、微信公衆平臺:裏面有公衆號的運維配置,固然也有開發配置,我們主要是用到了開發配置; git
三、微信商戶平臺:要支付必須有微信商戶,裏面需配置開發設置; 數據庫
四、花生殼:主要是用到花生殼的內網穿透,因爲微信支付開發過程當中,須要公網的微信服務器訪問本地開發的電腦,包括重定向之類的,因此須要內網穿透,這樣微信才能訪問到局域網電腦;花生殼能夠免費註冊使用,可是呢,免費的帶寬爲1M,因爲帶寬過小,根本穿透不進來,因此咱們後來購買了帶寬,發現帶寬爲3M時,足以應付日場使用。固然Ngrok也能夠內網穿透,可是帶寬過低了; 後端
五、微信公衆平臺支付接口調試工具:這個平臺能夠校驗本身生成的sign與微信服務器生成的sign是否一致,頗有用,尤爲是提示簽名失敗時。api
3、說明 數組
一、在支付接入過程當中,若是不順利,不要懷疑微信服務器有問題,多想一想本身哪兒可能出問題了,由於接入微信支付的商戶已經達好幾億了,微信不可能出問題的;瀏覽器
二、微信的開發文檔中說到,爲保證商戶接入質量,提高交易安全及用戶體驗,微信作了一個支付仿真沙箱環境,還說什麼「微信支付的合做服務商在正式上線交易前,必須先根據本文指引完成驗收」。這個別聽微信的,他們的這個沙箱環境根本不成熟,用這個是瞎折騰,我們能夠直接用真實環境測試;
三、微信支付開發文檔中提到「協議規則」,首先傳輸方式是HTTPS,是指微信服務器和商戶服務器通信協議是HTTPS,可是不要求微信客戶端與商戶服務器之間的通信協議是HTTPS,用HTTP也行,也就是說商戶服務器不用本身準備SSL證書。其次全部微信支付有關的接口都用POST。最後,提交和返回的數據都是XML格式,不是目前普標接受的JSON格式,這個要注意。
4、微信支付流程
第3步:微信客戶端請求商戶後臺系統,主要是攜帶商品的價格,其次若有必要攜帶商品的描述,或者傳遞一個參數可讓服務器惟一肯定商品價格與商品描述;
第4步:這一步是在咱們商戶後臺的數據庫的"訂單表"裏面插入一條數據,數據裏包含的基本信息有:用戶id,訂單金額,訂單商品及描述,支付狀態;
第5步:整個微信公衆號支付流程圖中,最重要的一步,這一步的目的是返回預支付訂單號prepay_id,這一步起到承上啓下的做用;這一步的詳細代碼見後文 「統一下單」;開發文檔 ;
第6步:根據第5步造成的prepay_id附帶其餘參數一塊兒發送給微信客戶端,開發文檔;
第7步:攜帶第6步的參數請求微信,這一步也是次重要的一步,開發文檔,常見問題,
第10步、第11步:支付完成後,微信服務器通知商戶後臺系統,後臺系統驗證完畢後,須要改變商戶訂單表裏訂單的狀態,即第4步的訂單狀態。以後須要後臺系統給微信服務器返回特定的信息。可是開發文檔中也說了,絕大部分狀況下,咱們支付成功,能接收到微信的通知,可是有時收不到,這時咱們須要主動調用微信支付【查詢訂單API】確認訂單狀態 。這一步的詳細代碼見後文 「微信支付結果通知及商戶服務器響應」,開發文檔 。
5、微信公衆號開通微信支付
見參考文檔:微信公衆號怎麼申請微信支付。開通以後,從公衆號內能夠看到已經關聯的商戶(圖1),同時從微信商戶裏能夠看到關聯的微信公衆號(圖2)。
6、公衆號開發參數配置
一、設置——公衆號設置——功能設置:設置網頁受權域名
微信是這麼說的「用戶在網頁受權頁贊成受權給公衆號後,微信會將受權數據傳給一個回調頁面,回調頁面需在此域名下,以確保安全可靠」,我是這裏理解的:網頁須要重定向時才能夠跳轉到該域名下的地址。因爲是在微信裏面嵌套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。
二、開發——基本配置。
1)這裏須要配置開發者密碼(特別重要)、ip白名單(經過開發者ID及密碼調用獲取access_token接口時,須要設置訪問來源IP爲白名單)。
2)服務器配置,驗證token。這一步也是微信防止沒有通過鑑權的服務器訪問本身的服務器作的一個驗證吧,能夠看作是爲了安全考慮。這個須要服務端代碼支持,代碼見後面。這個配置有時候須要多試幾回,有時提示「系統繁忙」。
7、微信商戶開發參數配置
這裏的支付受權目錄配置的就是微信公衆號調起支付頁面的URL,和後端服務器無關。這裏有坑,詳見下面的填坑之路。
8、後端代碼
一、微信常量類
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.一、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("\n|\r", ""); 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.二、服務器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.三、支付加簽工具
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.四、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 }
三、服務器token驗證(配合上文的 6、公衆號開發參數配置——> 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 }
四、統一下單
1 public Map<String, Object> weixinPay(DealRecord dealRecord, Byte memberGrade, String userId) { 2 Map<String, Object> retMap = new HashMap<>(2); 3 4 /** 5 * 一、用戶背景校驗 6 */ 7 8 9 /** 10 * 二、準備請求參數 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 * 三、微信請求參數簽名+下單 32 */ 33 WeixinSignUtil.getWeixinSign(requestDataMap, true); 34 Map<String, String> wxResponseMap = weixinService.wxPayRequest(requestDataMap, WeixinConstant.WX_URL_PAY_UNIFIEDORDER); 35 36 /** 37 * 四、邏輯處理 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 }
五、微信支付結果通知及商戶服務器響應
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 }
9、填坑之路
一、統一下單接口,請求微信,提示簽名失敗;
1)仔細檢查統一下單接口裏攜帶的參數,參數大小寫、參數是否必須攜帶。好比是appid而不是appId;openid文檔上寫的是「否」,可是它在備註裏寫到trade_type=JSAPI時(即JSAPI支付),此參數必傳,這個注意啊;
2)加密方式是否正確,能夠把本身生成的sign與微信公衆平臺支付接口調試工具生成的sign作對比,若是不一致,那證實你的加簽方式不正確,請仔細閱讀加簽安全規範這篇文章,好比裏面提到的按照參數名ASCII字典序排序、拼接商戶密鑰、MD5加密並轉化爲大寫;
3)若是第二步沒問題,好吧,不要猶豫,去重置商戶祕鑰吧,注意是商戶祕鑰,由於支付加簽時和商戶祕鑰有關,我當時就是卡在這一步了很久,至今記憶深入。
二、微信內h5調起支付,提示「支付驗證簽名失敗」
1)仔細檢查參數,參數大小寫、參數是否必須攜帶。好比是appId而不是appid(統一下單與這個正好相反);package的value必須是「prepay_id=xxx」形式;
2)統一下單的簽名的key是sign,而微信調起h5的簽名的key是paySign,這個要注意;
3)加密方式是否正確,能夠把本身生成的sign與微信公衆平臺支付接口調試工具生成的sign作對比,若是不一致,那證實你的加簽方式不正確,請仔細閱讀加簽安全規範這篇文章,好比裏面提到的按照參數名ASCII字典序排序、拼接商戶密鑰、MD5加密並轉化爲大寫;
4)若是第三步沒問題,好吧,不要猶豫,去重置商戶祕鑰吧。
三、微信內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/。規律就是去掉最後一個反斜槓的內容。