在写这篇文章之前,先鄙视下腾讯,对于微信公众号的支付,文档写的乱七八糟,一些出现的错误,都没有进行很好的说明。并且,在升级V3后,文档居然还停留在V2的支付上,他们自己的DEMO都不能进行支付,所以让一些接入微信支付的开发者苦不堪言,个中的滋味,只有我们自己体会得到。接入出了异常,根本就没有客服可以咨询,完全靠自己摸索,跟支付宝不在一个档次。
言归正传,这篇文章记录的是我在做微信公众号网页内支付躺过的坑以及一个能正常运行的支付DEMO的所有代码,主要分为10个部分,当然,接入微信支付,需要几个硬性条件,如果没有达到,恕笔者爱莫能助了。
硬性条件:微信公众号为服务号,并且开通了微信支付,拥有一个经过ICP备案的域名
第一部分:微信公众号的配置
1:需要将微信公众号内-->微信支付-->开发配置 的支付授权目录、测试授权目录、测试白名单配置好。支付授权目录,测试授权目录需要配置到最后一级(这里微信支付的说明文档说是只要配置到二级或者三级目录,是不对的),什么是最后一级?就是你支付页面所在的目录,例如我的支付页面是 http://www.baidu.com/xx/xx1/index.html ,那你需要将目录配置到成http://www.baidu.com/xx/xx1/ (而且层级不要太深,出了莫名其妙的错误,那就可能是这里的原因)
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
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字数限制,一些细节的地方可能说明不够,如果还有不理解的,请私信我,在这里,也要感谢 半个朋友 提供的无私帮助!