在写这篇文章以前,先鄙视下腾讯,对于微信公众号的支付,文档写的乱七八糟,一些出现的错误,都没有进行很好的说明。而且,在升级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字数限制,一些细节的地方可能说明不够,若是还有不理解的,请私信我,在这里,也要感谢 半个朋友 提供的无私帮助!