基於微信公衆號的V3網頁內支付

在寫這篇文章以前,先鄙視下騰訊,對於微信公衆號的支付,文檔寫的亂七八糟,一些出現的錯誤,都沒有進行很好的說明。而且,在升級V3後,文檔竟然還停留在V2的支付上,他們本身的DEMO都不能進行支付,因此讓一些接入微信支付的開發者苦不堪言,箇中的滋味,只有咱們本身體會獲得。接入出了異常,根本就沒有客服能夠諮詢,徹底靠本身摸索,跟支付寶不在一個檔次。php

言歸正傳,這篇文章記錄的是我在作微信公衆號網頁內支付躺過的坑以及一個能正常運行的支付DEMO的全部代碼,主要分爲10個部分,固然,接入微信支付,須要幾個硬性條件,若是沒有達到,恕筆者心有餘而力不足了。html

硬性條件:微信公衆號爲服務號,而且開通了微信支付,擁有一個通過ICP備案的域名java


第一部分:微信公衆號的配置ajax

1:須要將微信公衆號內-->微信支付-->開發配置 的支付受權目錄、測試受權目錄、測試白名單配置好。支付受權目錄,測試受權目錄須要配置到最後一級(這裏微信支付的說明文檔說是隻要配置到二級或者三級目錄,是不對的),什麼是最後一級?就是你支付頁面所在的目錄,例如個人支付頁面是 http://www.baidu.com/xx/xx1/index.html ,那你須要將目錄配置到成http://www.baidu.com/xx/xx1/ (並且層級不要太深,出了莫名其妙的錯誤,那就多是這裏的緣由)api

2:配置JS接口安全域名,具體目錄 設置-->公衆號設置-->功能設置-->JS接口安全域名 在裏面題寫上你那通過ICP備案的域名就能夠了。緩存

3:配置OAuth2.0網頁受權的回調域名,在開發者中心-->接口權限表-->網頁受權獲取用戶基本信息  後面的修改連接,在裏面題寫上你那通過ICP備案的域名就能夠了。安全

4:商戶平臺的配置,https://pay.weixin.qq.com/index.php/home/login?return_url=%2F (只能在IE登陸)登陸進這個系統,用戶名和密碼是在你申請微信支付的時候設定的。在帳戶設置-->API安全-->API密鑰欄目裏設定你的32位的API密鑰,記住這個密鑰,後續不少地方會有做用服務器


第二部分:在支付頁面獲取用戶的OpenID微信

   1:在頁面內引入 http://res.wx.qq.com/open/js/jweixin-1.0.0.js app


   2:配置通過處理的URL

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

   參數說明

   appid : 你的公衆號appId

   redirect_uri:你的支付頁面的外網地址,就是服務器地址,本機地址不行,並且地址要通過 encodeURIComponent 處理

   response_type: code 固定值

   scope:snsapi_base 靜默受權,用戶沒法感知你的程序正在獲取他的信息

   state:你須要傳遞的參數值

   後面的那個wechat_redirect必須帶上

   通過上面的處理,用戶訪問的實際支付地址不要直接寫你服務器的支付頁面了,而是寫上面的地址,訪問上面通過處理的地址,會自動跳轉到你的支付頁面的,跳轉後的地址裏面帶了一個名爲 code 的參數,而咱們,僅僅只須要它


  3:經過 code 拿用戶的OpenID 

  在頁面上拿到 code 再調用下面這個連接

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

   參數說明

   appid : 你的公衆號appId

   secret: 你的公衆號密鑰

   code: 第二步用戶訪問處理得連接獲得的code

   grant_type:固定值

正確時返回的JSON數據包以下,而咱們只須要OpenID:

{
   "access_token":"ACCESS_TOKEN",
   "expires_in":7200,
   "refresh_token":"REFRESH_TOKEN",
   "openid":"OPENID",
   "scope":"SCOPE",
   "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}


第三部分:wx.conf的配置(頁面初始化時要完成)

   在第二部分拿到的OpenID暫時在這部分是用不到的,這部分主要是 微信支付的 初始化

   1:wx.conf

$.ajax({
    type : 'POST',
    //url參數是你的支付目錄url,對應後面Servlet裏面的取值
    url : "/CMCC/GetSignature?url=http://www.colinktek-server.com/CMCC/pub/default.html",
    success : function(response) {
        wx.config({
            appId : params.appid, // 必填,公衆號的惟一標識
            timestamp : params.timestamp, // 必填,生成簽名的時間戳
            nonceStr : params.noncestr,  // 必填,生成簽名的隨機串
            signature : params.signature,// 必填,簽名,見附錄1
            jsApiList : [ 'chooseWXPay' ]
        });
    }
})

  上面的params 是我在後臺返回的一個參數,後臺Servlet的具體代碼以下:

response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();

//你的支付目錄的Url,不須要轉義,不包含#及其後面部分
String url = request.getParameter("url");
ServletContext application = this.getServletContext();

//ticket後面會有說明
JSONObject obj = getSignature(url, String.valueOf(application.getAttribute("ticket")));
out.print(obj.toString());
out.flush();
out.close();

  getSignature方法代碼:

public static JSONObject getSignature(String url,String ticket){
    StringBuffer buff = new StringBuffer();
    
    //生成一個32位的隨機數,文章後面會提供工具類的下載
    String noncestr = NonceString.generate();
    long time = System.currentTimeMillis() / 1000;
    
    //字符串拼接的順序是有約束的,因此不要換位置,換了就不行了
    buff.append("jsapi_ticket="+ticket+"&");
    buff.append("noncestr="+noncestr+"&");
    buff.append("timestamp="+time+"&");
    buff.append("url="+url);
 
    //SHA-1加密,文章後面會提供工具類的下載
    String s  = SHA1Util.getDigestOfString(buff.toString().getBytes());
    JSONObject obj = new JSONObject();
    obj.put("noncestr", noncestr);
    obj.put("timestamp", time);
    obj.put("appid", appid);
    obj.put("signature", s.toLowerCase());
    return obj;
}

 

  2:關於ticket

   上面servlet裏面的 ticket 須要說明下,我是經過在application裏面拿的,那它究竟是哪的呢?

   先了解一下jsapi_ticket,jsapi_ticket是公衆號用於調用微信JS接口的臨時票據。正常狀況下,

   jsapi_ticket的有效期爲7200秒,經過access_token來獲取。

   因爲獲取jsapi_ticket的api調用次數很是有限,頻繁刷新jsapi_ticket會致使api調用受限,影響自身業務,

   開發者必須在本身的服務全局緩存jsapi_ticket 。

   獲取access_token的方法,我這裏就不作說明了,由於只要你作過微信公衆號相關的開發,就應該知道

   假如在已經獲取access_token 的狀況下,

   採用http GET方式請求得到jsapi_ticket(有效期7200秒,開發者必須在本身的服務全局緩存jsapi_ticket)

   地址:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

   成功返回以下JSON:

{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}

  獲取到ticket後,將它緩存在你的服務器就能夠了,而個人是直接存在Application裏面,1個半小時去刷新一次

  

第四部分:wx.chooseWXPay(支付請求)配置 -- 發起支付的事件,在點擊某些付款按鈕時調用

   這部分開始以前說明,有另外的一種方式來進行發起支付請求,但我沒去試過,看網上說,那是老版本了,不知道可否成功

   1:wx.chooseWXPay 

$.ajax({
    type : 'POST',
    //用到了第二部分獲取到的OpenID,後面的money是金額數,單位爲 分
    url : "/CMCC/Unifiedorder?openId="+openId+"&money="+n,
    success : function(response) {
        wx.chooseWXPay({
            timestamp :parseInt(params.timeStamp), 
            nonceStr : params.nonceStr, 
            package : params["package"], // 統一支付接口返回的prepay_id參數值,提交格式如:prepay_id=***)
            signType : params.signType, 
            paySign : params.paySign, 
            complete : function(res) {
                //alert('成功');
            }
        });
    }
})

  上面params是我在點擊支付按鈕訪問後臺返回的參數,具體的後臺代碼:

   servlet代碼:

response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
String openId = request.getParameter("openId");
String money = request.getParameter("money");
int m = Integer.parseInt(money);
JSONObject str = unifiedorder(m*100, openId);
out.print(str.toString());
out.flush();
out.close();

  對應的unifiedorder方法(統一下單):

   具體的一些參數說明,請查閱:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

public static JSONObject unifiedorder(int totalMoney, String openId) {
    Map<String, String> map = new TreeMap<String, String>();
    map.put("appid", appid);
    String attach = String.valueOf(totalMoney);
    map.put("attach", attach);
    String body = "流量卡充值";
    map.put("body", body);
    String detail = "充值金額:" + totalMoney;
    map.put("detail", detail);
    map.put("device_info", device_info);
    map.put("fee_type", "CNY");
    map.put("goods_tag", "WXG");
    map.put("limit_pay", "no_credit");
    map.put("mch_id", mch_id);
    String nonce_str = NonceString.generate();
    map.put("nonce_str", nonce_str);
    map.put("notify_url", "http://www.colinktek-server.com/CMCC/pub/success.html");
    map.put("openid", openId);
    String out_trade_no = NonceString.generate();
    map.put("out_trade_no", out_trade_no);
    map.put("product_id", NonceString.generate());
    
    //獲取客戶端的IP,此方法不會提供,請自行上網查找
    String ip = getIpAddress();
    map.put("spbill_create_ip", ip);
    String time_expire = getEndTime();
    map.put("time_expire", time_expire);
    String starttime = getStartTime();
    map.put("time_start", starttime);
    map.put("total_fee", String.valueOf(totalMoney));
    map.put("trade_type", "JSAPI");
    String sign = getSign(map);
    String str = MapToXML(map,sign);
    JSONObject p = doPost("https://api.mch.weixin.qq.com/pay/unifiedorder", str);
    JSONObject xml = p.getJSONObject("xml");
    long timeStamp = System.currentTimeMillis() / 1000;
    String nt = NonceString.generate();
    if(xml.getString("result_code").equalsIgnoreCase("SUCCESS") 
        && xml.getString("return_code").equalsIgnoreCase("SUCCESS")){
	Map<String ,String> siganMap = paySign(nt,timeStamp,xml.getString("prepay_id"));
	p = JSONObject.fromObject(siganMap);
    }
    return p;
}

  getStartTime方法:

private static String getStartTime() {
    Date date = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    String str = sdf.format(date);
    return str;
}

  getEndTime方法:

private static String getEndTime() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    Date date1 = new Date();
    long Time = (date1.getTime() / 1000) + 60 * 10;
    date1.setTime(Time * 1000);
    String mydate1 = sdf.format(date1);
    return mydate1;
}

  getSign方法:

private static String getSign(Map<String, String> map) {
    StringBuffer sb = new StringBuffer();
    Set es = map.entrySet();// 全部參與傳參的參數按照accsii排序(升序)
    Iterator it = es.iterator();
    while (it.hasNext()) {
	Map.Entry entry = (Map.Entry) it.next();
	String k = (String) entry.getKey();
	Object v = entry.getValue();
	if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
	    sb.append(k + "=" + v + "&");
	}
    }
    
    //你在公衆號內設置的密鑰
    sb.append("key=" + "19VFQC5BC000A703A8C00000E4F4D266");
    
    //MD5加密方法,文章後面會提供工具類下載
    String sign = MD5Util.getMD5String(sb.toString()).toUpperCase();
    return sign;
}

 MapToXML方法:

//此方法只能使用於這裏,只能解析單層目錄
private static String MapToXML(Map<String, String> map,String sign) {
    StringBuffer buff = new StringBuffer();
    buff.append("<xml>");
    Set<String> objSet = map.keySet();
    for (String key : objSet) {
        if (key == null) {
	    continue;
        }
        buff.append("\n");
        buff.append("<").append(key.toString()).append(">");
        String value = map.get(key);
        buff.append(value);
        buff.append("</").append(key.toString()).append(">");
   }
   buff.append("\n<sign>" + sign + "</sign>");
   buff.append("\n");
   buff.append("</xml>");
   return buff.toString();
}

  doPost方法:

private static JSONObject doPost(String urlstr, String data) {
    try {
	URL url = new URL(urlstr);
	URLConnection con = url.openConnection();
	con.setDoOutput(true);
	con.setRequestProperty("Pragma:", "no-cache");
	con.setRequestProperty("Cache-Control", "no-cache");
	con.setRequestProperty("Content-Type", "text/xml");
	OutputStreamWriter out = new OutputStreamWriter(con.getOutputStream());
	out.write(new String(data.getBytes("UTF-8")));
	out.flush();
	out.close();
	JSONObject obj = xml2JSON(con.getInputStream());
	return obj;
    } catch (MalformedURLException e) {
	e.printStackTrace();
    } catch (IOException e) {
	e.printStackTrace();
    }
    return null;
}

  paySign方法:

public static Map<String,String> paySign(String nt,long timeStamp,String prepay_id){
    SortedMap<String, String> map = new TreeMap<String, String>();
    map.put("appId", appid);
    map.put("timeStamp",String.valueOf(timeStamp));
    map.put("nonceStr",nt );
    map.put("signType", "MD5");
    map.put("package","prepay_id=" + prepay_id);
    String paySign = getSign(map);
    map.put("paySign", paySign);
    map.put("result_code", "SUCCESS");
    map.put("return_code", "SUCCESS");
    return map;
}

至此,已所有完成支付功能。因爲oschina字數限制,一些細節的地方可能說明不夠,若是還有不理解的,請私信我,在這裏,也要感謝 半個朋友 提供的無私幫助!

相關文章
相關標籤/搜索