微信各種支付(完整開發流程)


最近做了幾個關於微信支付的項目,現在剛好有點時間,來總結一下容易踩的坑。

首先微信支付主要有這幾種方式(微信公眾號支付、微信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支付、微信小程序支付

結合前面幾種支付,都是套娃式的

六、注意

①、測試支付時,別老用同一訂單編號。

②、微信提供了簽名校驗工具,https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1

③、掃碼支付需要在商戶號里面設置回調地址,其它都是在請求支付的時候把回調地址帶過去。

④、微信的回調會執行多次,注意不要重復操作訂單

⑤、微信支付時,金額單位必須是分

⑥、H5支付時,支付請求傳入的spbill_create_ip,必須是用戶的真實IP

⑦、遇到問題不要急,先檢查參數有沒有問題,再看微信返回的內容,有錯誤碼就去微信開發文檔里面查看具體問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM