最近做了幾個關於微信支付的項目,現在剛好有點時間,來總結一下容易踩的坑。
首先微信支付主要有這幾種方式(微信公眾號支付、微信H5支付、微信掃碼支付、微信APP支付)
所有支付的第一步都是請求統一下單,統一下單,統一下單,請求URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder。統一下單的目的是拿到預支付交易會話標識prepay_id,這個是必須的。所有的支付調用都是通過prepay_id來識別。
還有一個重要的就是,微信支付接口簽名校驗工具(網址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1),此工具旨在幫助開發者檢測調用【微信支付接口API】時發送的請求參數中生成的簽名是否正確,提交相關信息后可獲得簽名校驗結果。簽名正確基本就沒什么問題了。
一、微信資料准備
1、注冊一個微信公眾號,並年審(年審需要交300費用,然后等待微信那邊審核通過),這個跟着微信步驟弄就行了
微信公眾平台(https://mp.weixin.qq.com/)
2、注冊微信商戶號,成為微信商家
微信支付(https://pay.weixin.qq.com/)
3、把公眾號與商戶號綁定
4、需要用到的信息位置
①、公眾號(AppID)
②、商戶號(MCH_ID,API_KEY)
③、商戶授權目錄、掃碼回調地址
④、商戶號關聯的APPID
⑤、商戶號支付功能開通
二、微信公眾號支付
微信公眾號支付需要的參數有:APP_ID(微信公眾號開發者ID)、APP_SECRET(微信公眾號開發者密碼)、MCH_ID(商戶ID)、API_KEY(商戶密鑰)。
微信公眾號支付應用的場景是在微信內部的H5環境中是用的支付方式。因為要通過網頁授權獲取用戶的openId,所以必須要配置網頁授權域名。同時要配置JS接口安全域名。
1、工具類
1 /** 2 * 公眾號支付配置信息 3 */ 4 public class PayConfigUtil { 5 public static String APP_ID="公眾號APPID"; 6 public static String MCH_ID="商戶號"; 7 public static String API_KEY="API密鑰"; 8 public static String CREATE_IP="127.0.0.1"; 9 public static String NOTIFY_URL="微信回調地址";//公眾號支付回調-與商戶里配置的支付回調無關 10 //微信統一下單地址 11 public static String UFDODER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder"; 12 }
1 /** 2 * 微信支付常用方法 3 */ 4 public class PayCommonUtil { 5 6 /** 7 * 是否簽名正確,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。 8 * @return boolean 9 */ 10 public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { 11 StringBuffer sb = new StringBuffer(); 12 Set es = packageParams.entrySet(); 13 Iterator it = es.iterator(); 14 while(it.hasNext()) { 15 Map.Entry entry = (Map.Entry)it.next(); 16 String k = (String)entry.getKey(); 17 String v = (String)entry.getValue(); 18 if(!"sign".equals(k) && null != v && !"".equals(v)) { 19 sb.append(k + "=" + v + "&"); 20 } 21 } 22 23 sb.append("key=" + API_KEY); 24 25 //算出摘要 26 String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase(); 27 String tenpaySign = ((String)packageParams.get("sign")).toLowerCase(); 28 29 //System.out.println(tenpaySign + " " + mysign); 30 return tenpaySign.equals(mysign); 31 } 32 33 /** 34 * @Description:sign簽名 35 * @param characterEncoding 編碼格式 36 * @param packageParams 請求參數 37 * @param API_KEY 38 * @return 39 */ 40 public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { 41 StringBuffer sb = new StringBuffer(); 42 Set es = packageParams.entrySet(); 43 Iterator it = es.iterator(); 44 while (it.hasNext()) { 45 Map.Entry entry = (Map.Entry) it.next(); 46 String k = (String) entry.getKey(); 47 String v = (String) entry.getValue(); 48 if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { 49 sb.append(k + "=" + v + "&"); 50 } 51 } 52 sb.append("key=" + API_KEY); 53 String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); 54 return sign; 55 } 56 57 /** 58 * @Description:將請求參數轉換為xml格式的string 59 * @param parameters 請求參數 60 * @return 61 */ 62 public static String getRequestXml(SortedMap<Object, Object> parameters) { 63 StringBuffer sb = new StringBuffer(); 64 sb.append("<xml>"); 65 Set es = parameters.entrySet(); 66 Iterator it = es.iterator(); 67 while (it.hasNext()) { 68 Map.Entry entry = (Map.Entry) it.next(); 69 String k = (String) entry.getKey(); 70 String v = (String) entry.getValue(); 71 if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) { 72 sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">"); 73 } else { 74 sb.append("<" + k + ">" + v + "</" + k + ">"); 75 } 76 } 77 sb.append("</xml>"); 78 return sb.toString(); 79 } 80 81 /** 82 * 取出一個指定長度大小的隨機正整數 83 * @param length int 設定所取出隨機數的長度。length小於11 84 * @return int 返回生成的隨機數。 85 */ 86 public static int buildRandom(int length) { 87 int num = 1; 88 double random = Math.random(); 89 if (random < 0.1) { 90 random = random + 0.1; 91 } 92 for (int i = 0; i < length; i++) { 93 num = num * 10; 94 } 95 return (int) ((random * num)); 96 } 97 98 /** 99 * 獲取當前時間 yyyyMMddHHmmss 100 * @return String 101 */ 102 public static String getCurrTime() { 103 Date now = new Date(); 104 SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 105 String s = outFormat.format(now); 106 return s; 107 } 108 }
1 /** 2 * 發送支付請求 3 */ 4 public class HttpUtil { 5 private final static int CONNECT_TIMEOUT = 5000; // in milliseconds 6 private final static String DEFAULT_ENCODING = "UTF-8"; 7 8 public static String postData(String urlStr, String data){ 9 return postData(urlStr, data, null); 10 } 11 12 public static String postData(String urlStr, String data, String contentType){ 13 BufferedReader reader = null; 14 try { 15 URL url = new URL(urlStr); 16 URLConnection conn = url.openConnection(); 17 conn.setDoOutput(true); 18 conn.setConnectTimeout(CONNECT_TIMEOUT); 19 conn.setReadTimeout(CONNECT_TIMEOUT); 20 if(contentType != null) 21 conn.setRequestProperty("content-type", contentType); 22 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); 23 if(data == null) 24 data = ""; 25 writer.write(data); 26 writer.flush(); 27 writer.close(); 28 29 reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); 30 StringBuilder sb = new StringBuilder(); 31 String line = null; 32 while ((line = reader.readLine()) != null) { 33 sb.append(line); 34 sb.append("\r\n"); 35 } 36 return sb.toString(); 37 } catch (IOException e) { 38 System.out.println("Error connecting to " + urlStr + ": " + e.getMessage()); 39 } finally { 40 try { 41 if (reader != null) 42 reader.close(); 43 } catch (IOException e) { 44 } 45 } 46 return null; 47 } 48 }
1 /** 2 * 微信返回結果解析 3 */ 4 public class XMLUtil { 5 /** 6 * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml數據。 7 * 8 * @param strxml 9 * @return 10 * @throws JDOMException 11 * @throws IOException 12 */ 13 public static Map<String, String> doXMLParse(String strxml) throws JDOMException, IOException { 14 strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); 15 16 if (null == strxml || "".equals(strxml)) { 17 return null; 18 } 19 20 Map<String, String> m = new HashMap<>(); 21 22 InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); 23 SAXBuilder builder = new SAXBuilder(); 24 Document doc = builder.build(in); 25 Element root = doc.getRootElement(); 26 List list = root.getChildren(); 27 Iterator it = list.iterator(); 28 while (it.hasNext()) { 29 Element e = (Element) it.next(); 30 String k = e.getName(); 31 String v = ""; 32 List children = e.getChildren(); 33 if (children.isEmpty()) { 34 v = e.getTextNormalize(); 35 } else { 36 v = XMLUtil.getChildrenText(children); 37 } 38 m.put(k, v); 39 } 40 // 關閉流 41 in.close(); 42 return m; 43 } 44 45 /** 46 * 獲取子結點的xml 47 * @param children 48 * @return String 49 */ 50 public static String getChildrenText(List children) { 51 StringBuffer sb = new StringBuffer(); 52 if (!children.isEmpty()) { 53 Iterator it = children.iterator(); 54 while (it.hasNext()) { 55 Element e = (Element) it.next(); 56 String name = e.getName(); 57 String value = e.getTextNormalize(); 58 List list = e.getChildren(); 59 sb.append("<" + name + ">"); 60 if (!list.isEmpty()) { 61 sb.append(XMLUtil.getChildrenText(list)); 62 } 63 sb.append(value); 64 sb.append("</" + name + ">"); 65 } 66 } 67 return sb.toString(); 68 } 69 }
2、微信支付
①、頁面請求
1 $.ajax({ 2 url:"weChatPay", 3 type:"post", 4 data: { 5 "用於生成訂單的商品信息" 6 }, 7 success:function(res){ 8 var data = res.data; 9 if(data.msg != null && data.msg != ""){ 10 $.toast(data.msg, "cancel"); 11 }else { 12 WeixinJSBridge.invoke( 13 'getBrandWCPayRequest', { 14 "appId":data.appId, //公眾號名稱,由商戶傳入 15 "timeStamp":data.timeStamp, //時間戳,自1970年以來的秒數 16 "nonceStr":data.nonceStr, //隨機串 17 "package":data.package, 18 "signType":"MD5", //微信簽名方式: 19 "paySign":data.paySign //微信簽名 20 }, 21 function(re){ 22 if(re.err_msg == "get_brand_wcpay_request:fail" ) 23 { 24 // 使用以上方式判斷前端返回,微信團隊鄭重提示: 25 //res.err_msg將在用戶支付成功后返回ok,但並不保證它絕對可靠。 26 $.toast("支付失敗,請稍后再試!", "cancel"); 27 } 28 else if(re.err_msg == "get_brand_wcpay_request:cancel") 29 { 30 $.toast("已取消支付!", "cancel"); 31 } 32 else if(re.err_msg == "get_brand_wcpay_request:ok") 33 { 34 $.toast("支付成功,請手動刷新訂單查看!"); 35 } 36 }); 37 } 38 }, 39 error:function(e){ 40 $.toast("系統錯誤,支付失敗!", "cancel"); 41 } 42 });
②、controller層
1 /** 2 * 微信公眾號支付 3 * @param session 4 * @return 5 */ 6 @PostMapping("weChatPay") 7 public Response weChatPay(Object 訂單需要信息,HttpSession session){ 8 //獲取微信用戶的openId---這個是用戶訪問頁面的時候已經從微信獲取保存在session里面了,可以通過微信授權的時候取到 9 String openId = session.getAttribute("openId").toString(); 10 //生成待支付訂單 11 String orderCode = "訂單唯一值"; 12 。。。此處省略了插入數據庫模塊 13 Map<String,Object> map = this.weChatService.weChatPay(openId,orderCode); 14 return new Response().success().data(map); 15 }
③、service層
1 public Map<String, Object> weChatPay(String openId,String orderCode) { 2 Map<String,Object> wechatmap = new HashMap<>(); 3 String msg= ""; 4 try{ 5 //通過訂單取到支付金額即商品名稱 6 。。。通過訂單編號取訂單信息 7 //訂單金額 8 BigDecimal ordersAmount = null; 9 //商品名稱 10 String body = ""; 11 //微信支付金額單位統一轉換成分 12 BigDecimal fen = ordersAmount.multiply(new BigDecimal(100)); 13 fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP); 14 String amount = fen.toString(); 15 16 //微信支付配置 17 String appid = PayConfigUtil.APP_ID; // appid 18 String mch_id = PayConfigUtil.MCH_ID; // 商業號 19 String key = PayConfigUtil.API_KEY; // key 20 21 String currTime = PayCommonUtil.getCurrTime(); 22 String strTime = currTime.substring(8, currTime.length()); 23 String strRandom = PayCommonUtil.buildRandom(4) + ""; 24 String nonce_str = strTime + strRandom; 25 26 String spbill_create_ip = PayConfigUtil.CREATE_IP;// 獲取發起電腦 ip 27 String notify_url = PayConfigUtil.NOTIFY_URL;// 回調接口 28 String trade_type = "JSAPI";//公眾號支付方式 29 30 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 31 packageParams.put("appid", appid); 32 packageParams.put("openid",openId);//用戶標識 33 packageParams.put("mch_id", mch_id); 34 packageParams.put("nonce_str", nonce_str); 35 packageParams.put("body", body); 36 packageParams.put("out_trade_no", orderCode); 37 packageParams.put("total_fee", order_price); 38 packageParams.put("spbill_create_ip", spbill_create_ip); 39 packageParams.put("notify_url", notify_url); 40 packageParams.put("trade_type", trade_type); 41 //附加類型-這里可以附加自己需要的特殊屬性 42 packageParams.put("attach", "公眾號一支付"); 43 44 //計算簽名 45 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 46 packageParams.put("sign", sign); 47 //把支付參數轉換xml格式 48 String requestXML = PayCommonUtil.getRequestXml(packageParams); 49 //發起微信支付請求 50 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 51 52 //解析返回結果 53 Map<String, String> map = XMLUtil.doXMLParse(resXml); 54 55 if(map.get("return_code").equals("FAIL")){ 56 msg = "下單錯誤"; 57 wechatmap.put("msg",msg); 58 wechatmap.put("error_code", map.get("return_msg")); 59 return wechatmap; 60 }else if(map.get("return_code").equals("SUCCESS")){ 61 if(map.get("result_code").equals("FAIL") 62 || (map.get("err_code") != null && !map.get("err_code").equals("")) 63 || (map.get("err_code_des") != null && !map.get("err_code_des").equals(""))){ 64 msg = "其他的錯誤參數值"; 65 wechatmap.put("msg",msg); 66 wechatmap.put("error_code", map.get("err_code_des")); 67 return wechatmap; 68 } 69 } 70 71 //預支付交易會話標識---這個值很關鍵 72 String prepay_id = (String) map.get("prepay_id"); 73 74 SortedMap<Object, Object> mapp = new TreeMap<Object, Object>(); 75 76 currTime = PayCommonUtil.getCurrTime(); 77 strTime = currTime.substring(8, currTime.length()); 78 strRandom = PayCommonUtil.buildRandom(4) + ""; 79 nonce_str = strTime + strRandom; 80 81 mapp.put("appId", appid); 82 mapp.put("timeStamp", System.currentTimeMillis());//重新計算時間戳 83 mapp.put("nonceStr", nonce_str);//重新計算隨機字符串,長度要求在32位以內。 84 85 if(!prepay_id.equals("")) { 86 mapp.put("package", "prepay_id="+prepay_id); 87 } 88 89 mapp.put("signType", "MD5"); 90 wechatmap = createSign("UTF-8", mapp, key); 91 }catch (IOException e){ 92 msg = "系統異常,暫時無法使用微信支付,請聯系客服!!"; 93 } 94 wechatmap.put("msg",msg); 95 return wechatmap; 96 }
④、util(公眾號用戶授權)
1 /** 2 * 微信授權重定向后進入的方法獲取用戶在本公眾號的 唯一標示 3 * 能否授權成功並取到用戶的openId是公眾號各類操作成功的前提 4 */ 5 public static String[] obtainUnionid(HttpServletRequest request) 6 { 7 String CODE = request.getParameter("code"); 8 9 String[] openid = {"",""}; 10 11 if(CODE == null || CODE.equals("")) 12 { 13 openid[0] = "未授權!"; 14 } 15 else if(CODE.equals("10009"))//操作頻繁 16 { 17 openid[0] = "操作頻繁!"; 18 } 19 else if(CODE.equals("10004"))//此公眾號被封禁 20 { 21 openid[0] = "此公眾號被封禁!"; 22 } 23 else if(CODE.equals("10006"))//必須關注此測試號 24 { 25 openid[0] = "必須先關注!"; 26 } 27 else if(CODE.equals("10015"))//公眾號未授權第三方平台,請檢查授權狀態 28 { 29 openid[0] = "此公眾不支持!"; 30 } 31 else if (CODE.equals("10003") || CODE.equals("10005") || CODE.equals("10007") || CODE.equals("10008") 32 || CODE.equals("10010") || CODE.equals("10011") || CODE.equals("10012") || CODE.equals("10013") 33 || CODE.equals("10014") || CODE.equals("10016")) 34 { 35 openid[0] = "系統錯誤,請稍后再試!"; 36 } 37 else 38 { 39 String APPID = WeChatConfig.getInstance().getAppId(); 40 String SECRET = WeChatConfig.getInstance().getAppSecret(); 41 42 String URL = WeChatConsts.URL_OAUTH2_GET_ACCESSTOKEN.replace("APPID", APPID).replace("SECRET", SECRET).replace("CODE", CODE); 43 44 JSONObject jsonStr = WeChatRealization.httpRequest(URL,"GET",null); 45 46 if(jsonStr.get("errcode") != null && !jsonStr.get("errcode").equals("")) 47 { 48 //失敗 49 openid[0] = "此公眾不支持!!!"; 50 } 51 else 52 { 53 openid[1] = jsonStr.get("openid").toString();//只有在用戶將公眾號綁定到微信開放平台帳號后,才會出現該字段。 54 } 55 } 56 return openid; 57 }
3、微信回調
①、controller層
1 /** 2 * 微信支付回調 3 * @param request 4 * @param response 5 */ 6 @RequestMapping(value = "payBackWx", method = {RequestMethod.GET,RequestMethod.POST}) 7 public void payBackWx(HttpServletRequest request, HttpServletResponse response){ 8 weChatService.payBackWx(request,response); 9 }
②、service層
1 public void payBackWx(HttpServletRequest request, HttpServletResponse response) { 2 response.setContentType("text/xml"); 3 InputStream inputStream;// 讀取參數 4 StringBuffer sb = new StringBuffer(); 5 try{ 6 inputStream = request.getInputStream(); 7 String s; 8 BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 9 while ((s = in.readLine()) != null){ 10 sb.append(s); 11 } 12 in.close(); 13 inputStream.close(); 14 // 解析xml成map 15 Map<String, String> m = new HashMap<String, String>(); 16 m = XMLUtil.doXMLParse(sb.toString()); 17 18 // 過濾空 設置 TreeMap 19 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 20 Iterator it = m.keySet().iterator(); 21 while (it.hasNext()){ 22 String parameter = (String) it.next(); 23 String parameterValue = m.get(parameter); 24 String v = ""; 25 if (null != parameterValue){ 26 v = parameterValue.trim(); 27 } 28 packageParams.put(parameter, v); 29 } 30 31 String key = PayConfigUtil.API_KEY; // key 32 String resXml = ""; 33 34 // 判斷簽名是否正確 35 if (PayCommonUtil.isTenpaySign("UTF-8", packageParams, key)){ 36 // 處理業務開始 37 if ("SUCCESS".equals((String) packageParams.get("result_code"))){ 38 // 這里是支付成功 39 ////////// 自己的業務邏輯//////////////// 40 String out_trade_no = packageParams.get("out_trade_no").toString();//商戶訂單號 41 String mch_id = packageParams.get("mch_id").toString();//商戶號 42 String openid = packageParams.get("openid").toString();//用戶標識 43 String total_fee = packageParams.get("total_fee").toString();//金額 為 分 44 String attach = packageParams.get("attach").toString();//自己的附加屬性 45 // 分 換成元 46 Double total_fee2 = Double.valueOf(total_fee) / 100; 47 //微信支付訂單號 48 String transaction_id = packageParams.get("transaction_id").toString(); 49 。。。修改訂單狀態為支付成功 50 // 通知微信.異步確認成功.必寫.不然會一直通知后台.八次之后就認為交易失敗了. 51 resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "" + "</xml> "; 52 }else{ 53 resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" 54 + "<return_msg><![CDATA[失敗]]></return_msg>" + "</xml> "; 55 } 56 response.getWriter().write(resXml); 57 response.getWriter().flush(); 58 }else{ 59 //log通知簽名驗證失敗 60 } 61 }catch (IOException e){ 62 //log獲取微信充值返回參數錯誤 63 }catch(JDOMException e){ 64 //log微信充值返回數據xml轉換為map錯誤 65 } 66 }
4、常見問題
①、此公眾號並沒有這些scope的權限,錯誤碼:10005
建議檢查一下公眾號的功能。比如是不是在訂閱號/未認證的公眾號里面嘗試調用認證服務號的功能。
微信支付認證過期或者APPID填寫錯誤。
請使用snsapi_userinfo的授權登錄方式即可解決。
②、商家暫時沒有此類交易權限,請聯系商家客服
請檢查你的下單接口是否指定了支付用戶的身份,需單獨開通指定身份支付權限方可使用。
請確認你使用的商戶號是否有jsapi支付的權限,可登錄商戶平台-產品中心查看。
③、當前頁面的URL未注冊:http://www.weixin.qq.com/pay.do
請檢查下單接口中使用的商戶號是否在商戶平台(http://pay.weixin.qq.com)配置了對應的支付目錄。
④、redirect_url域名與后台配置不一致,錯誤碼:10003
本錯誤是公眾號獲取openid接口報的錯誤,接口文檔:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
檢查下單接口傳的appid與獲取openid接口的appid是否同一個(需一致)。
檢查appid對應的公眾號后台(mp.weixin.qq.com),是否配置的授權域名和獲取openid的域名一致。授權域名配置路徑:公眾平台--設置--公眾號設置--功能設置--網頁授權域名。
⑤、該商戶暫不支持通過外部拉起微信完成支付
Jsapi支付只能從微信瀏覽器內發起支付請求。
⑥、使用公眾號時需要在公眾號內設置IP白名單,需要把自己的公網IP/服務器IP加到白名單內。
⑦、安全域名、業務域名
都有每月設置次數限制,測試的時候不要經常去換域名信息,要不然上線的時候就沒機會換域名了。(設置域名時注意,公眾號內有個用戶驗證的MP_verify_xxxxxxxxx.txt文件,微信提示說要放在根目錄下面,其實只要把文件內容返回出來,訪問名稱設置成文件名稱加后綴就能通過驗證了)。
三、微信掃碼支付
微信掃碼支付需要的參數有:APP_ID(微信公眾號開發者ID)、MCH_ID(商戶ID)、API_KEY(商戶密鑰)。
微信掃碼支付一般應用的場景是PC端電腦支付。微信掃碼支付可分為兩種模式,根據支付場景選擇相應模式。一般情況下的PC端掃碼支付選擇的是模式二,需要注意的是模式二無回調函數。
【模式一】商戶后台系統根據微信支付規則鏈接生成二維碼,鏈接中帶固定參數productid(可定義為產品標識或訂單號)。用戶掃碼后,微信支付系統將productid和用戶唯一標識(openid)回調商戶后台系統(需要設置支付回調URL),商戶后台系統根據productid生成支付交易,最后微信支付系統發起用戶支付流程。
【模式二】商戶后台系統調用微信支付【統一下單API】生成預付交易,將接口返回的鏈接生成二維碼,用戶掃碼后輸入密碼完成支付交易。注意:該模式的預付單有效期為2小時,過期后無法支付。
注意:需要回調時需要在商戶里面配置掃碼回調地址(可以在前面的資料准備-信息位置里面看圖)
1、工具類
需要的工具類與上面微信公眾號支付的一致
2、微信支付
這里我們使用的是微信掃碼支付的【模式二】,使用鏈接生成二維碼方式
①、頁面請求
1 //引用二維碼生成JS---直接網上下載一個就行了 2 <script src="qrcode.js"></script> 3 4 $.ajax({ 5 url : 'weChatPayment', 6 type : 'post', 7 async: true, 8 data : formData, 9 success : function(data) { 10 if(data!=null && data!="" && data.length > 0){ 11 if(data[0] != ""){ 12 if(data[0] == 1){ 13 //頁面中間顯示圖片 14 $("#qrcode").html(""); 15 var qrcode = new QRCode('qrcode'); 16 qrcode.makeCode(data[1]); 17 }else{ 18 layer.msg(data[0], {icon: 2}); 19 } 20 }else{ 21 layer.msg("錯誤!!!", {icon: 2}); 22 } 23 }else{ 24 layer.msg("錯誤!!!", {icon: 2}); 25 } 26 }, 27 error:function(e){ 28 layer.msg("網絡錯誤!!!", {icon: 2}); 29 } 30 });
②、controller層
1 /** 2 * 微信充值前 3 * @param 訂單需要信息 4 * @return 5 */ 6 @PostMapping("weChatPayment") 7 public Object weChatPayment(Object 訂單需要信息){ 8 //生成待支付訂單 9 String orderCode = "訂單唯一值"; 10 。。。此處省略了插入數據庫模塊 11 return paymentService.weChatPayment(orderCode); 12 }
③、service層
1 @Transactional 2 public String[] weChatPayment(String orderCode) { 3 String[] str = new String[2]; 4 try{ 5 //通過訂單取到支付金額即商品名稱 6 。。。通過訂單編號取訂單信息 7 //訂單金額 8 BigDecimal ordersAmount = null; 9 BigDecimal fen = new BigDecimal(ordersAmount).multiply(new BigDecimal(100)); 10 fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP); 11 String amount = fen.toString(); 12 13 //微信支付配置 14 String appid = PayConfigUtil.APP_ID; // appid 15 String mch_id = PayConfigUtil.MCH_ID; // 商業號 16 String key = PayConfigUtil.API_KEY; // key 17 18 String currTime = PayCommonUtil.getCurrTime(); 19 String strTime = currTime.substring(8, currTime.length()); 20 String strRandom = PayCommonUtil.buildRandom(4) + ""; 21 String nonce_str = strTime + strRandom; 22 23 String spbill_create_ip = PayConfigUtil.CREATE_IP;// 獲取發起電腦 ip 24 String notify_url = PayConfigUtil.NOTIFY_URL;// 回調接口 25 String trade_type = "NATIVE";//掃碼支付方式 26 27 String order_price = amount; // 價格 注意:價格的單位是分 28 String body = "賬戶充值"; // 商品名稱 29 30 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 31 packageParams.put("appid", appid); 32 packageParams.put("mch_id", mch_id); 33 packageParams.put("nonce_str", nonce_str); 34 packageParams.put("body", body); 35 packageParams.put("out_trade_no", orderCode); 36 packageParams.put("total_fee", order_price); 37 packageParams.put("spbill_create_ip", spbill_create_ip); 38 packageParams.put("notify_url", notify_url); 39 packageParams.put("trade_type", trade_type); 40 41 //計算簽名 42 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 43 packageParams.put("sign", sign); 44 //把支付參數轉換xml格式 45 String requestXML = PayCommonUtil.getRequestXml(packageParams); 46 //發起微信支付請求 47 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 48 Map<String, String> map = XMLUtil.doXMLParse(resXml); 49 50 //獲取得到充值url-前台生成二維碼使用 51 String urlCode = (String) map.get("code_url"); 52 if(urlCode != null && !urlCode.equals("")){ 53 str[0] = "1"; 54 str[1] = urlCode; 55 return str; 56 }else{ 57 //系統異常,暫時無法使用微信支付,請聯系客服 58 str[0] = "系統異常,暫時無法使用微信支付,請聯系客服"; 59 } 60 }catch (IOException e){ 61 str[0] = "系統異常,暫時無法使用微信支付,請聯系客服!!"; 62 } 63 return str; 64 }
3、微信回調
回調和解析方式都與微信公眾號支付回調一致
四、微信H5支付
微信H5支付需要的參數有:APP_ID(微信公眾號開發者ID)、MCH_ID(商戶ID)、API_KEY(商戶密鑰)。
微信H5支付是微信官方2017年上半年剛剛對外開放的支付模式,它主要應用於在手機網站在移動瀏覽器(非微信環境)調用微信支付的場景。底層的技術以及支付鏈接本質上是財付通。
微信H5支付的流程比較簡單,就是拼接請求的xml數據,進行統一下單,獲取到支付的mweb_url,然后請求這個url網址就行。請求使用curl函數,使用的時候需要注意設置header參數。
注意:微信H5支付需要在微信支付商戶平台單獨申請開通,否則無法使用。(可以在前面的資料准備-信息位置里面看圖)
1、工具類
需要的工具類與上面微信公眾號支付的一致
2、微信支付
①、頁面請求
1 $.ajax({ 2 url: 'pay', 3 type: 'POST', 4 dataType: "json", 5 data:{}, 6 success: function (res) { 7 $.hideLoading(); 8 var data = res.data; 9 if(data!=null && data!="" && data.length > 0){ 10 if(data[0] != ""){ 11 if(data[0] == 1){ 12 $("#submit_z").text("正在支付"); 13 //原頁面顯示一個需手動確認的支付提示信息 14 layer.open({ 15 type: 1 16 ,title: false //不顯示標題欄 17 ,closeBtn: false 18 ,area:['240px','150px'] 19 ,shade: 0.3 20 ,offset: 'auto' 21 ,id: 'LAY_layuipro' //設定一個id,防止重復彈出 22 ,content: '<div style="width: 100%;text-align: center;font-size: 14px;border-radius: 20px;">\n' + 23 ' <div style="width: 100%;height: 59px;line-height: 59px;border-bottom: 1px #ccc solid;">\n' + 24 ' 請確認微信支付是否已完成\n' + 25 ' </div>\n' + 26 ' <div onclick="pay_true();" style="width: 100%;font-weight: 500;border-bottom: 1px #ccc solid;line-height: 44px;height: 44px;color: red;">\n' + 27 ' 已完成支付\n' + 28 ' </div>\n' + 29 ' <div onclick="pay_false();" style="color: #9c9c9c;width: 100%;line-height: 45px;height: 45px;">\n' + 30 ' 支付遇到問題,重新支付\n' + 31 ' </div>\n' + 32 '</div>' 33 }); 34 }else{ 35 layer.msg(data[0], {icon: 2}); 36 } 37 } 38 else{ 39 layer.msg("錯誤!!!", {icon: 2}); 40 } 41 } 42 else{ 43 layer.msg("錯誤!!!", {icon: 2}); 44 } 45 }, 46 error:function(e){ 47 layer.msg("網絡錯誤!!!", {icon: 2}); 48 } 49 }); 50 51 //跳轉到訂單詳情 52 function pay_true() { 53 //跳轉到訂單頁 54 window.location.href = [[${url}]]; 55 } 56 //回到支付前狀態 57 function pay_false() { 58 layer.closeAll(); 59 $("#submit_z").text("提交訂單"); 60 }
②、controller層
1 @PostMapping("pay") 2 public Response pay(Object obj,HttpServletRequest request) { 3 String request_url = request.getRequestURL().toString().split("indent/pay")[0]; 4 // 獲取請求主機IP地址,如果通過代理進來,則透過防火牆獲取真實IP地址 5 // 一定要取到真實IP,不然后面支付會報錯 6 String ip = null; 7 try { 8 ip = request.getHeader("x-forwarded-for"); 9 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 10 ip = request.getHeader("Proxy-Client-IP"); 11 } 12 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 13 ip = request.getHeader("WL-Proxy-Client-IP"); 14 } 15 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 16 ip = request.getHeader("HTTP_CLIENT_IP"); 17 } 18 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 19 ip = request.getHeader("HTTP_X_FORWARDED_FOR"); 20 } 21 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 22 ip = request.getRemoteAddr(); 23 if (ip.equals("127.0.0.1")) { 24 // 根據網卡取本機配置的IP 25 InetAddress inet = null; 26 try { 27 inet = InetAddress.getLocalHost(); 28 } catch (UnknownHostException e) { 29 //LOGGER_ERROR.error("拋出異常 # ",e); 30 } 31 ip = inet.getHostAddress(); 32 } 33 } 34 35 // 對於通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割 36 if (ip != null && ip.length() > 15) { // "***.***.***.***".length() = 15 37 if (ip.indexOf(",") > 0) { 38 ip = ip.substring(0, ip.indexOf(",")); 39 } 40 } 41 } catch (Exception ex) { 42 // LOGGER_ERROR.error("NetworkUtil # getIpAddress # 拋出異常 # ", ex); 43 } 44 //生成待支付訂單 45 String orderCode = "訂單唯一值"; 46 。。。此處省略了插入數據庫模塊 47 return new Response().success().data(this.payService.pay(orderCode,request_url,ip)); 48 }
③、service層
1 @Transactional 2 public String[] pay(String orderCode, String url,String ip) { 3 String[] str = new String[2]; 4 String message = ""; 5 try { 6 7 //通過訂單取到支付金額即商品名稱 8 。。。通過訂單編號取訂單信息 9 //訂單金額 10 BigDecimal ordersAmount = null; 11 // 商品名稱 12 String body = ""; 13 14 //獲取支付配置 15 String appid = PayConfigUtil.APP_ID;// appid 16 String mch_id = PayConfigUtil.MCH_ID;// 商業號 17 String key = PayConfigUtil.API_KEY;// 商戶API-key 18 19 String currTime = PayCommonUtil.getCurrTime(); 20 String strTime = currTime.substring(8, currTime.length()); 21 String strRandom = PayCommonUtil.buildRandom(4) + ""; 22 String nonce_str = strTime + strRandom; 23 24 // 價格 注意:價格的單位是分 25 String order_price = new BigDecimal(String.valueOf(ordersAmount)).multiply(new BigDecimal("100")).setScale(0,BigDecimal.ROUND_HALF_UP).toString(); 26 27 // 獲取發起電腦 ip----這里的IP限制跟別的支付不一樣,這里需要真實IP 28 String spbill_create_ip = ip; 29 30 // 回調接口 31 String notify_url = PayConfigUtil.NOTIFY_URL; 32 33 //JSAPI NATIVE 代表公眾號支付 MWEB 代表H5 34 String trade_type = "MWEB"; 35 36 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 37 38 packageParams.put("appid", appid);//公眾賬號ID 39 40 packageParams.put("mch_id", mch_id);//商戶號 41 42 //packageParams.put("device_info", "WEB"); 43 //自定義參數,可以為終端設備號(門店號或收銀設備ID),PC網頁或公眾號內支付可以傳"WEB" 44 45 packageParams.put("nonce_str", nonce_str); 46 //隨機字符串,長度要求在32位以內。 47 packageParams.put("body", body); 48 //商品簡單描述 49 packageParams.put("out_trade_no", orderCode); 50 //商戶訂單號 51 packageParams.put("total_fee", order_price); 52 //標價金額 單位為分 53 packageParams.put("spbill_create_ip", spbill_create_ip); 54 //APP和網頁支付提交用戶端ip 55 packageParams.put("notify_url", notify_url); 56 //異步接收微信支付結果通知的回調地址,通知url必須為外網可訪問的url,不能攜帶參數。 57 packageParams.put("trade_type", trade_type); 58 packageParams.put("scene_info","'h5_info':{'type':'Wap','wap_url':'"+url+"/shop','wap_name': "+body+""); 59 60 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 61 packageParams.put("sign", sign); 62 //通過簽名算法計算得出的簽名值 63 64 String requestXML = PayCommonUtil.getRequestXml(packageParams); 65 66 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 67 68 69 Map<String, String> map = XMLUtil.doXMLParse(resXml); 70 71 String urlCode = (String) map.get("code_url");//獲取得到充值url 72 73 // 確認支付過后跳的地址,需要經過urlencode處理 74 String mweb_url = map.get("mweb_url") + "&redirect_url=" + PayConfigUtil.NOTIFY_URL; 75 76 if(mweb_url != null && !mweb_url.equals("")){ 77 str[0] = "1"; 78 str[1] = mweb_url; 79 return str; 80 }else{ 81 //系統異常,暫時無法使用微信支付,請聯系客服 82 str[0] = "系統異常,暫時無法使用微信支付,請聯系客服"; 83 } 84 }catch (Exception e){ 85 str[0] = "系統異常,暫時無法使用微信支付,請聯系客服!!"; 86 } 87 return str; 88 }
3、微信回調
回調和解析方式都與微信公眾號支付回調一致
4、常見問題
①、網絡環境未能通過安全驗證,請稍后再試
商戶側統一下單傳的終端IP(spbill_create_ip)與用戶實際調起支付時微信側檢測到的終端IP不一致導致的,這個問題一般是商戶在統一下單時沒有傳遞正確的終端IP到spbill_create_ip導致,詳細可參見客戶端ip獲取指引(本文已經提供了一個獲取用戶真實IP的方法)。
統一下單與調起支付時的網絡有變動,如統一下單時是WIFI網絡,下單成功后切換成4G網絡再調起支付,這樣可能會引發我們的正常攔截,請保持網絡環境一致的情況下重新發起支付流程。
②、商家參數格式有誤,請聯系商家解決
當前調起H5支付的referer為空導致,一般是因為直接訪問頁面調起H5支付,請按正常流程進行頁面跳轉后發起支付,或自行抓包確認referer值是否為空。
如果是APP里調起H5支付,需要在webview中手動設置referer,如(Map extraHeaders = new HashMap();extraHeaders.put("Referer", "商戶申請H5時提交的授權域名");//例如 http://www.baidu.com ))。
③、商家存在未配置的參數,請聯系商家解決
當前調起H5支付的域名(微信側從referer中獲取)與申請H5支付時提交的授權域名不一致,如需添加或修改授權域名,請登陸商戶號對應的商戶平台--"產品中心"--"開發配置"自行配置。
如果設置了回跳地址redirect_url,請確認設置的回跳地址的域名與申請H5支付時提交的授權域名是否一致。
④、支付請求已失效,請重新發起支付
統一下單返回的MWEB_URL生成后,有效期為5分鍾,如超時請重新生成MWEB_URL后再發起支付。
⑤、請在微信外打開訂單,進行支付
H5支付不能直接在微信客戶端內調起,請在外部瀏覽器調起
⑥、IOS:簽名驗證失敗、安卓:系統繁忙,請稍后再試
請確認同一個MWEB_URL只被一個微信號調起,如果不同微信號調起請重新下單生成新的MWEB_URL。
如MWEB_URL有添加redirect_url,請確認參數拼接格式是否有誤,是否有對redirect_url的值做urlencode,可對比以下例子格式:https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn
五、微信APP支付、微信小程序支付
結合前面幾種支付,都是套娃式的