微支付 js-api java 坑王之王!!!

时间:2022-02-26 16:21:38
微支付 js-api java 坑
转载请指明出处,版权必究。此文章是我们在经历了各种坑之后的结晶,请爱惜她。最后更细时间:2015年5月4日
前言:
微支付的js-api,我是不想要喷你,是全国人民选我来喷你。
尼玛,这是我有屎以来遇到过最深的坑,各种精英怪,各种机关,暗器,各种折磨,看完你们的文档,写的是一坨什么?

在缺少demo的情况下,文档又烂的情况下,你只有去试,碰壁,试,碰壁,抓狂,冷静,再试,再抓狂,再冷静,再试,崩溃,冷静,再试。。。。
乱七八糟的文档完全不知道在搞神马东西。
你这哪是写开发文档?哪里是做开放接口?
你丫当所有的开发人员都是奥特曼,都可以攻克各种小怪兽呢是吧?
你丫当我们是60年代的团队40人副本,清一色全是战士和盗贼,木有奶打奥妮克希亚呢是吧?
 
你丫当我们是强大的奈非天,玩diablo3,去跟大天使一起攻打地狱的恶魔呢是吧?你丫要通过这个文档来考验我们的理解力、忍耐力、吐槽力呢是吧?
你丫当你们是探索与发现栏目组呢是吧?

我严重怀疑微支付的开发团队是马云手下派来的卧底,故意玩死腾讯的特种作战部队。
好,那我今天就当一把不吐槽会死星人。

当初定plan的时候,微支付调研只需要2天,最后花了尼玛两个礼拜才搞定,要不是有大神各种指点,还不知道要调研到什么时候去。 

他喵的写的文档驴唇不对马嘴不说,写的东西还特别隐晦,让开发人员各种猜是吧?
一个变量,有N种叫法是吧?一会叫key,一会叫什么商户ID,一会又叫什么秘钥,结果猜来猜去发现是一个东西。我勒个擦。
他喵的网站版本的文档说全大写,pdf里又说不能都大写。
他喵的文档很明显就是两波人写出来的,一个功能有两种实现方式,得挨个去试。
他喵的一会用json传参数,一会用xml传参数。
他喵的一会url需要转义,一会不需要转义,一会要带http://,一会又不带。
他喵的这么大个公司,连个demo都写不好,让玩家几乎是在0沟通,0攻略,100愤怒值的情况下,在重重困难之下,把你们玩通关,通关后,我们发现,其实我们没有赢,我们其实是被玩的。 颤抖吧,凡人!

文档里什么都不写,编码出错也不提示。
他喵的出错误提示,除了“签名错误”就没有其他的了是吧?
他喵的想调用你们的接口一顿山路十八弯是吧,通过这个拿那个,通过那个拿这个,签名,各种签名,校验,验你妹啊验。

最让我无语的是,你丫给开发者提供了一个签名校验工具,我满心欢喜的拿各种参数去工具里RUN,发现全都没问题,结果真正调用你们接口就出“签名错误”。
尼玛文档里不写清楚,各种来回配置,不是这里要配置就是那里要配置,商户平台的那个配置尤其恶心,要不是大神指点,可能现在还在坑里呆着。

呃。我的菊花。。。。又开始隐隐作痛了。。。

从头到尾,跟客服完全是0沟通是吧?给客服打电话,客服说:
“对不起官人,您提出的是技术问题,请联系技术支持。”
“那么好,咋联系呀?”
“请您给技术部门的支持发邮件,他们会尽快回复您的。”
抱着百分之一的希望给微支付的技术支持发了一封mail,尼玛秒回了居然,我擦,大公司就是大公司,这执行力太高了吧。
待洒家定睛观瞧。。。。

“微信JSSDK发布之后,对行业造成了巨大的正面影响,由于反馈邮件非常多,已经没法做到及时回复邮件来帮助你解决问题,我们感到很抱歉。
目前我们已经对文档进行了错误修订,并把目前反馈的共性问题进行了整理放到开放文档的“常见错误及解决方案”中。。。。”
WHAT THE FUCK? U MOTHER FU**KER PIECES OF SHIT!
STOP FU**KING AROUND OK?
就这样,这封邮件犹如我早上拉出去的翔一样,石沉大海。。。
(此时我的菊花又开始隐隐作痛起来。。。)

你知道你们对整个java行业造成了多大负面影响吗?邮件非常多,对啊,上百万封吐槽的邮件吧?
抱歉你妹啊抱歉,你们的开发人员的脑子里装的都是翔吧? 

百般无奈下,开始硬着头皮去搜关于java 微支付 api 等关键字
90%都是在喷微支付的坑,有多么多么深,微支付的坑里有多么多么多的荆棘。
尼玛写错一点都不行啊,举步维艰,万丈深渊。
微支付的js-api不是一坨翔,翔至少还有可塑性,而微支付的js-api,却没有。。。
finally,the god damn js-api is finished ,im finished too...(js-api调研完了,我也被玩完了。)
我已经无力吐槽,你呢?
为了避免更多的孩子们惨遭毒手,我决定把我调研的点点滴滴写出来,希望可以给大家起到抛砖引玉的作用。

注意,请忘掉所有的你看过的网上的攻略以及各种官方上的开发文档,也许您看完我写的文章后依然会呆在坑里无法自拔(但至少我是从坑里出来了),请您做好心理准备,用包容的,理解的,被坑的心态来面对微支付jsapi,一个表单符号,一个utf-8的编码集,一个写法上的顺序,一个斜杠,这些小细节直接会引发各种错误,希望大家要非常细节的来对待,这不是演习!有问题请加我的QQ:171757607。最后更新时间:2015年4月26日
正篇:亚瑞特巨坑,微支付jsapi巨坑
1.在微信网站上的设置(你必须是服务号并且申请各种认证通过后才能往下走,否则请回炉!)
首先请登录https://mp.weixin.qq.com微信公众平台,进行如下配置,谢谢。

1.1微信支付-开发配置微支付 js-api java 坑王之王!!!
1.2公众号设置-功能设置微支付 js-api java 坑王之王!!!
1.3开发者中心-配置项-接口权限微支付 js-api java 坑王之王!!!微支付 js-api java 坑王之王!!!
1.4 微信支付-商户平台(坑1:此处为最大的巨坑,要不是大神告诉我要配置这里,我死也想不出这里也需要配置,这里不配置的话,后面的预订单提交会一直报错,并且那个错也许你永远都不知道原因。。。阿西)请登录:https://pay.weixin.qq.com 微信支付-商户平台微支付 js-api java 坑王之王!!!将这个秘钥设置成和AppSecret(应用密钥)设置成一样的,just相信我。
2.调用网页微支付各位童鞋,经过上面的配置,大家可以进入第二阶段,its time to coding.因为我们是要从网页上调用微支付么,对吧,那么我们就要通过一个http get请求来告诉微信服务器,我们要请求啦。肿么写?


2.1 第一个http请求,我们要支付!!!https://open.weixin.qq.com/connect/oauth2/authorize?appid=11111111&redirect_uri=http%3A%2F%2Ftest.aaa.com/wxtest%2Fwxtest&response_type=code&scope=snsapi_base&state=12345#wechat_redirect
您第一眼看到这坨,您会想,嗯。。。这尼玛是什么?简单的说,这坨就是你要告诉微信服务器你要进行支付的一个http get请求的url,当然了这只是第一步,你需要给他问号传参,下面请允许我来解释一下这几个参数:appid:这就不用说了,你应该把它弄成全局变量,很多地方要用。redirect_uri:跳转地址,写成你自己url,切记这里的写法要与上面的配置在一个域名里,否则这步就够你卡一阵子的。你可以配置成/xxx.jsp,xxx.do,xxx.action,啥都可以,我这里是用了spring 的restful的url格式。(坑2:你要把你的url进行encode转码,切记你的url是带http://的,例如我的是http://test.aaaa.com/wxtest,转码之后是http%3A%2F%2Ftest.aaa.com/wxtest%2Fwxtest。这不是演习!)response_type:code,固定写法,不要问为什么。scope:snsapi_base固定写法,不要问为什么。state:state是你自己的业务id,随便写啥都行,在后面会接到你自己传的参数,名字就叫state,别改。后面的照抄(#wechat_redirect),别乱写。
这坨url只能在微信浏览器中使用,因此我们的测试方法是把这个url用微信随便发给一个人,然后你自己在聊天框里点击这个url。如果上面的配置都木有错误的话,那么按照套路出牌,微信服务器会回调你的url,说白了就是请求你的服务器,然后给你传递2个参数:code和state。如果没有拿到这2个参数或者没有被请求到,反省吧,凡人,前面的配置你肯定是配错了。好的,休息一下,我们进入下一小节。
(什么?你说你不会写如何提供web服务让微信访问你的代码?就是controller,action里的方法呗,或者一个jsp,servlet也行呀。什么?你还是不懂?凡人,你需要立即马上放下手头的工作,去买一本java web速成。。。)

2.2 第一次微信来请求你,还不错。好的,欢迎回来,前文书说到我们会拿到2个参数,下面是2个参数的含义:state:你自定义的业务id,前面提到过。code:code是微信返回的用户auth校验后的code码,后面要用到。你接到参数的时候需要做判断:如果code是空,则不能往下继续进行,因为可能用户鉴权失败了。

2.3 第一个签名(config配置签名),坑王之王。注意!一大波大坑正在靠近。你的脑海里要有一个印象,在亚瑞特巨坑中,呃,不是,在微信jsapi巨坑中,所有的签名,都是手机中的战斗机。坑王之王。只要是带签名的地方,我们基本都被卡住很长时间,各种试啊,你懂我的意思。
那么好,你拿到的那个code和state,是要干什么用?答案是你要做第一个config签名(配置签名),什么是配置签名?简单的说,微信服务器为了防止第三方程序or个人进行抓包篡改请求参数,就是做坏事,的一种防范措施。签名里放的是你要给微信服务器请求的参数以及加密,再使用ticket,把他们揉在一起,捏出来一个签名,发给微信服务器。
不太懂对吗?别怕,跟着杨老师一步一步带你升仙。
这样,我先告诉你,这个config签名里都需要什么吧,然后我在下面一步一步带你升仙,肿么样?这个签名里还需要nonce_str32位随机码,timestamp生成时间,url微支付页面url,jsapiTicket门票。就这四样任务物品,得到后就可以交任务,拿经验了。
其中jsapiTicket是比较坑的,获取jsapiTicket需要accessToken,而获取accessToken又需要appid和AppSecret(应用密钥)。
WTF?我们继续。。。
2.3.1 获取accessToken简单的说你要给微信的一个url发http get请求,参数是appid和AppSecret(应用秘钥),https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+WxConfig.getAppID()+"&secret="+WxConfig.getSecret(); 这是我的代码,当然了我把appid和AppSecret(应用密钥)弄成了全局变量。(坑3:这个请求不要试图在页面上写,不要用jquery或者ajax,会有跨域的问题,即使你解决了跨域的问题,也会有拿不到返回值的问题。乖乖的拿到java代码里去写,在java代码里写一个get请求,并且用json来接返回值,不会写的话,QQ里管我要。)请求后,微信服务器会返回给你一个json,没错,你没听错,的确是json。json里取"access_token",并且要把这个值全局缓存起来,7200秒吧。频繁的请求微信服务器会把你的账号封掉。
什么?你说你不会取json里的值?凡人。。。。。
2.3.2 获取jsapiTicket好的,我们现在已经有了accessToken,下一步就是去取jsapiTicket了哟。同上,你需要给微信的另一个url发http get请求,参数是我们刚才拿到的access_token,https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+accessToken+"&type=jsapi 返回值依然是json,你需要在返回的json中取"ticket",并缓存7200秒。
2.3.3 获取url这里的url是微支付页面url,也是你之前在2.1里配置的那个url,好了,坑来了。(坑4:这个url必须是带"http://",并且不用encode转码,记住,是不能转码!并且必须带上code和state参数,否则出错!当时就因为没有传code和state试了多少遍,阿西!例如:http://test.aaa.com/wxtest?code="+code+"&state="+state;code和state是微信给你传的,还记得吗?
2.3.4 获取nonce_str+timestampString nonce_str = UUID.randomUUID().toString().replaceAll("-", "");//32位随机码
String timestamp = Long.toString(System.currentTimeMillis() / 1000);//生成时间
能看懂吧?
2.3.5 生成第一个config配置签名有了上面的4样任务物品,咱们就可以交任务,拿经验了。(坑5:下面的写法顺序也有严格的要求,照我的抄吧,至少我的写法是最后支付成功了。)下面我们就来把这4个东西揉在一块,捏出一个签名出来,我直接放java代码了,有疑问请加我QQ。
/**
* @category 第一个签名,前台的config配置签名
* @param code
* @param state
* @return 返回签名map,前台要用
*/
public static Map<String, Object> configSign(String code,String state) {
String nonce_str = UUID.randomUUID().toString().replaceAll("-", "");//32位随机码
String timestamp = Long.toString(System.currentTimeMillis() / 1000);//生成时间
String url = WxConfig.getUrl()+"/wxtest?code="+code+"&state="+state;//微支付页面url
String jsapiTicket;
try {
jsapiTicket = WxpayUtil.getJsapiTicket();
//注意这里参数名必须全部小写,且必须有序
String string1 = "jsapi_ticket=" + jsapiTicket +
"&noncestr=" + nonce_str +
"&timestamp=" + timestamp +
"&url=" + url;

//加密算法
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
String signature = byteToHex(crypt.digest());//生成签名

Map<String, Object> returnMap = new HashMap<String, Object>();
returnMap.put("url", url);
returnMap.put("jsapi_ticket", jsapiTicket);
returnMap.put("nonceStr", nonce_str);
returnMap.put("timestamp", timestamp);
returnMap.put("signature", signature); //这是万恶的config配置签名,就是他!!!
return returnMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
我的方法返回了一个MAP,MAP里放的是config签名信息,我们姑且叫他config签名MAP,我在前台页面中需要拿这个MAP进行下一步操作。
我的页面中是这么写的,当然你可以有你更好的写法,我并没有对代码进行优化和封装:<%
String code = request.getParameter("code");//这个code码在下面会用到,到时不要问我code码从哪里来,他的故乡在远方。。。
String state = request.getParameter("state");//报名ID

//第一个签名,用于第一个JS配置
Map<String,Object> signatureMap = WxpaySign.configSign(code,state);
%>

我,尼玛,这都是神马跟神马?这只是一个开始,休息一会,抽根烟,放松放松,我们接着来。


2.4 页面中配置config对象以及ready+error监听方法前文书说到,我们拿到了万恶的config配置签名,以及从后台返回了一个config签名MAP,下面,我们要把这个MAP里的东西用起来,来配置天杀的config对象。

2.4.1 创建一个jsp页面,并引入微信的js包+jquery包<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript" src="${ctx}/js/jquery/jquery-1.9.1.min.js"></script>

什么?你不用jquery?厉害!
2.4.2 配置config对象<script> wx.config({    debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来
    appId: '<%=WxConfig.getAppID()%>', // 必填,公众号的唯一标识
    timestamp: '<%=signatureMap.get("timestamp")%>', // 必填,生成签名的时间戳
    nonceStr: '<%=signatureMap.get("nonceStr")%>', // 必填,生成签名的随机串
    signature: '<%=signatureMap.get("signature")%>',// 必填,签名,见附录1
    sApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
  });
(坑6,记住全部要带单引号!!!)
signatureMap就是我上面从后台返回的config签名MAP,不用客气,从里面拿东西往config里放吧。

2.4.3 配置error监听<script>
    wx.error(function(res){
        for(i in res){
            alert(i+":"+res[i]);
        }
        alert("微支付调用失败,请截图给管理人员,谢谢");
        window.close();
    });
你在这里可以做你想要做的任何逻辑。
2.4.4 配置ready监听<script>
    wx.ready(function(){
        alert("hello world...");
    });
(坑7:如果上述的配置or写法有一点错误,那么页面上就会触发error监听,坑的是触发完error监听,依然会进入ready监听,大家要写好自己的逻辑。触发error监听后,你能看到微信服务器给你返回的错误提示信息,OK,但凡触发了错误监听,您就别往下走了,老老实实的回去反思吧。)呃。。。如果上面的配置or写法完全100%没问你的话,会打出hello world,前提是没有进error监听方法的情况下,好!!!第一场战役胜利了。HOLY SHIT!!!庆祝一下!!!你可以试想当时没有人指点的情况下,要摸清这么多坑,是多么恶心的事情。你下面几乎全部的写法都要在ready监听方法里实现,OK,我们进入下一章,万恶的预订单提交。


2.5 万恶的预订单提交该配置的配置完啦,我们该准备提交订单啦,等等!先要有一个预订单,就是告诉微信服务器你准备要提交订单了,我,尼玛。。。
预订单说白了就是一个表单,微信官方叫统一下单接口,你要给微信服务器一个http post请求,参数是xml(尼玛刚才不还是json呢么,肿么现在是xml了?),这里我是在java里写的,在ready里用ajax调用的。您随意。
我先大概说一下这个预订单提交,你需要拿到通过code码(之前有)拿到openid。然后提交一坨参数给微信,是xml格式,还包括万恶的预订单签名,在一切木有问题的情况下,微信会给你返回一个xml,你通过解析后能拿到prepay_id,传说中的预订单ID,有了这个ID我们才好去打下一关的boss对不对?
2.5.1 取得openid简单的说你要给微信的一个url再发http get请求(恩?我为什么要说一个再字),参数是appid,AppSecret(应用秘钥),code码。https://api.weixin.qq.com/sns/oauth2/access_token?appid="+WxConfig.getAppID()+"&secret="+WxConfig.getSecret()+"&code="+code+"&grant_type=authorization_code 这个url给你返回的是一个json对象,木有错,就是一会xml一会json玩死你。在返回的json对象中取"openid"
什么?你问我openid是什么?对不起,我也不知道,用,就对了。
2.5.2 构造预订单参数像我刚才说的,预订单需要一个xml格式的参数,那么好,我们现在来搞这个xml。我这里是用一个MAP里放一坨参数,然后再用代码转成xml的,当然,你也可以有你自己的实现方式。直接粘代码了啊:
(坑7:Map的顺序一定不能错,不然会报签名错误,GOD!)//此map用于生成签名和XML
Map<String,Object> paramSignMap = new LinkedHashMap();//按顺序来的
paramSignMap.put("appid", WxConfig.getAppID());
paramSignMap.put("attach", "aaa");//
附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
paramSignMap.put("body", "逐风者祝福之剑"); //支付的时候给用户看的商品描述,直接写中文,不用转义。
paramSignMap.put("mch_id", WxConfig.getMchID());//商户号
paramSignMap.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", ""));//随机码
paramSignMap.put("notify_url", WxConfig.getUrl()+"/wxnotify");//监听地址,支付成功或失败微信会回调这个url通知你。
paramSignMap.put("openid",openId); //之前拿到的oepnid
paramSignMap.put("out_trade_no", "1231231231231");//自己的订单号,这里跟你的业务逻辑要挂钩了。
paramSignMap.put("spbill_create_ip", request.getRemoteAddr());//请求者IP
paramSignMap.put("total_fee", signMap.get("money")+"00");//钱数
paramSignMap.put("trade_type", "JSAPI");//固定写法
paramSignMap.put("sign", WxpaySign.preFormSign(paramSignMap));//生成预订单签名

(坑8:paramSignMap的notify_url,必须是带http://的,例如:http://test.aaa.com/wxnotify(坑9:paramSignMap的total_fee,后面要加00,因为你传过去1,默认是1分钱(坑10:paramSignMap的sign,详见2.5.3,一点都不能错,不然就会报天杀的"签名错误"
2.5.3 生成预订单签名好的,大家看到了刚才的这段代码:paramSignMap.put("sign", WxpaySign.preFormSign(paramSignMap));//生成预表单签名preFormSign是我自己封的构造预订单签名的方法,大概的逻辑说一下。paramSignMap里除了sign以外的所有参数都累加,并且使用MD5加密后再加上一个秘钥的大写,就形成了预订单签名。(坑11:这个秘钥就是用户必须要在商户平台去设置的那个,同坑1,我们为了方便,把这个秘钥跟AppSecret(应用密钥)设置成一个了。这个地方是坑王之王,坑断肠。
话不多说,粘代码:/**
* @category 预提交表单签名
* @param url
* @return
*/
public static String preFormSign(Map<String,Object> paramSignMap) {
StringBuffer tempStr = new StringBuffer(1024);

for (String key : paramSignMap.keySet()) {
tempStr.append(key+"="+paramSignMap.get(key)+"&");
}
System.out.println(tempStr.toString());
String returnStr = MD5.GetMD5Code(tempStr.substring(0,tempStr.length()-1)+"&key="+WxConfig.getSecret()).toUpperCase();
logger.info("preFormSign==========================================>"+returnStr);
return returnStr;
}

(坑12,此处的MD5校验,完全复制我的代码吧,之前我们自己的MD5交易,微信服务器不认!代码如下:public class MD5 {
// 全局数组
private final static String[] strDigits = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };

public MD5() {
}

// 返回形式为数字跟字符串
private static String byteToArrayString(byte bByte) {
int iRet = bByte;
// System.out.println("iRet="+iRet);
if (iRet < 0) {
iRet += 256;
}
int iD1 = iRet / 16;
int iD2 = iRet % 16;
return strDigits[iD1] + strDigits[iD2];
}

// 返回形式只为数字
private static String byteToNum(byte bByte) {
int iRet = bByte;
System.out.println("iRet1=" + iRet);
if (iRet < 0) {
iRet += 256;
}
return String.valueOf(iRet);
}

// 转换字节数组为16进制字串
private static String byteToString(byte[] bByte) {
StringBuffer sBuffer = new StringBuffer();
for (int i = 0; i < bByte.length; i++) {
sBuffer.append(byteToArrayString(bByte[i]));
}
return sBuffer.toString();
}

public static String GetMD5Code(String strObj) {
String resultString = null;
try {
resultString = new String(strObj);
MessageDigest md = MessageDigest.getInstance("MD5");
// md.digest() 该函数返回值为存放哈希值结果的byte数组
resultString = byteToString(md.digest(strObj.getBytes()));
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return resultString;
}

public static void main(String[] args) {
System.out.println(MD5.GetMD5Code("qqqqqq"));
}
}


2.5.4 用一坨参数构造xml书接上文2.5.2,我们已经有了paramSignMap,下面该干什么啦?对!我们应该把paramSignMap转成xml,然后一脚踢给微信服务器,抽出她裤衩里的猴皮筋做个弹弓打他们家玻璃。
粘代码:String xmlInfo = XMLUtil.getRequestXml(paramSignMap);//生成xml,这个xml要post请求到微信的服务端
/**
* @category 生成请求xml,用于微支付
* @param parameters
* @return
*/
public static String getRequestXml(Map<String,Object> parameters){
    StringBuffer sb = new StringBuffer(1024);
    sb.append("<xml>");

    for (String key : parameters.keySet()) {
        System.out.println("key= "+ key + " and value= " + parameters.get(key));
        String k = key;
        String v = parameters.get(key)+"";
        sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
    }
    sb.append("</xml>");
    return sb.toString();
}
(坑13:我们当时测试的时候,有的加了CDATA,有的没加,各种问题,后来都加上CDATA就好了。不管了,这么用吧。什么?你不知道什么是CDATA?恩,做得好!)
2.5.5 提交xml给微信服务器,拿prepay_id准备工作都做好了,下面可以砸他们家玻璃了,下面的写法也是我从网上找的,至少他是好用的对吧?(坑14:这里的提交方式你也可以自己写,不过切记编码集是UTF-8,且是POST提交)提交成功后,需要拿到返回的prepay_id,他给你返回的也是xml,需要解析。另外此处你可以跟你的业务挂钩了,进行你的表操作。
粘代码://提交XML给微信服务器,微信服务器返回PREID
URL url = new URL("https://api.mch.weixin.qq.com/pay/unifiedorder");
URLConnection con = url.openConnection();
con.setDoOutput(true); // POST方式
out = new OutputStreamWriter(con.getOutputStream(), "UTF-8");
out.write(xmlInfo);
if(out!=null){
out.flush();
out.close();
}

StringBuffer returnStr = new StringBuffer(1024);
String sCurrentLine = "";
InputStream l_urlStream = con.getInputStream();
BufferedReader l_reader = new BufferedReader(new InputStreamReader(l_urlStream));
while ((sCurrentLine = l_reader.readLine()) != null) {
returnStr.append(sCurrentLine);
}

// System.out.println("xmlInfo=" + xmlInfo);
// System.out.println("-------------预订单返回--------------"+returnStr.toString());

Map<String, String> returnMap = XMLUtil.doXMLParse(returnStr.toString());//解析微信返回的信息,以Map形式存储便于取值
// System.out.println("最后的结果111----->"+returnMap.get("return_code"));
// System.out.println("最后的结果222----->"+returnMap.get("return_msg"));
if("SUCCESS".equals(returnMap.get("return_code"))){

其中doXMLParse的代码:/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
    strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

    if(null == strxml || "".equals(strxml)) {
        return null;
    }

    Map m = new HashMap();

    InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
    SAXBuilder builder = new SAXBuilder();
    Document doc = builder.build(in);
    Element root = doc.getRootElement();
    List list = root.getChildren();
    Iterator it = list.iterator();
    while(it.hasNext()) {
        Element e = (Element) it.next();
        String k = e.getName();
        String v = "";
        List children = e.getChildren();
        if(children.isEmpty()) {
            v = e.getTextNormalize();
        } else {
            v = XMLUtil.getChildrenText(children);
        }
        m.put(k, v);
    }
    //关闭流
    in.close();
    return m;
}
(坑15:关于xml解析等等部分最好是按照我的代码写,因为不保证别的代码是否有问题。)
需要判断返回值:if("SUCCESS".equals(returnMap.get("return_code"))){
    //自己的业务
    return 返回给前台prepay_id以及你自己的业务id;
}else{
    return 错误信息;
}
艾玛,这里你只要能拿到prepay_id,恭喜,一个里程碑似的胜利,然后我把这个prepay_id返回到前台(我们依然是在ready函数里,还记得吗?)。

2.6 快要胜利了,支付订单提交我知道你现在的感受,我很同情你,但是,坚持,坚持。。。知道你现在也很痛苦,但是你想想我们淌水的时候那种艰辛,你换位思考一下,就释然了。书接上文2.5.5,我们已经拿到了prepay_id了对吧,好,我们要用这个prepay_id来干嘛?对!我们要来拿prepay_id 来做订单提交的签名呀。
我从前台给传过来了timestamp,prepay_id,nonceStr,其中timestamp和nonceStr是从之前的signatureMap里get出来的,我真的懒得再生成了,代码如下:

/**
* @category 真实表单签名生成
* @param request
* @param response
* @return String
*/
@ResponseBody
@RequestMapping("getRealFormSignature")
public JsonMessage getRealFormSignature(HttpServletRequest request, String prepay_id,String timestamp,String nonceStr) throws Exception{
Map<String,Object> readFormParamMap = new HashMap();
readFormParamMap.put("appId",WxConfig.getAppID());
readFormParamMap.put("timeStamp",timestamp);
readFormParamMap.put("nonceStr",nonceStr);
readFormParamMap.put("package","prepay_id="+prepay_id);
readFormParamMap.put("signType","MD5");

String realFormSign = WxpaySign.getSign(readFormParamMap);
return 把签名返回给前台
}

WxpaySign.getSign代码://官方的代码
public static String getSign(Map<String,Object> map){
ArrayList<String> list = new ArrayList<String>();
for(Map.Entry<String,Object> entry:map.entrySet()){
if(entry.getValue()!=""){
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String [] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < size; i ++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + WxConfig.getKey();
//Util.log("Sign Before MD5:" + result);
result = MD5Util.MD5Encode(result).toUpperCase();
//Util.log("Sign Result:" + result);
return result;
}
(坑15:这个getSign是官方的通用签名写法,必须这么写,你自己写的一定会出错。)
拿到支付订单签名了吧,下面开始真正的提交支付订单:需要几个参数:写法如下:timestamp,nonceStr,package,signType。就照着我的写法就可以。
//提交支付订单
wx.chooseWXPay({
timestamp: '<%=signatureMap.get("timestamp")%>',
nonceStr: '<%=signatureMap.get("nonceStr")%>',
package: 'prepay_id='+prepay_id,
signType: 'MD5',//SHA1
paySign: realFormSignature,
success:function(res1) {
//alert(res1);

//修改交易状态,这是我自己的逻辑,你换成你的。就是需要更新你的逻辑,告诉自己的数据库,交易成功了。
$.ajax({
type:"POST", //post提交方式默认是get
dataType:'json',
url:'${ctx}/wxPaySaveSuccess',
data : {
key_id : payLogUUID, //这是我自己的业务ID
trade_status :'TRADE_SUCCESS'//我自己的业务参数
},
error:function(data) {// 设置表单提交出错
alert('系统出现异常,请联系管理员');
},
success:function(result) {//提交成功
if(result.success){
alert("哇!成功啦,快去领取您的号码牌吧~");
 window.location.href="${ctx}/sign/phoneSignSuccess/<%=state%>"; //跳转到成功页面
}else{
alert(result.msg);
alert("更新交易记录失败!");
}
}
});
},fail:function(res) {
//alert(res);
alert("本次支付失败,请联系客服人员,谢谢您的支持与理解。");
}
});

2.7 支付成功后的闭环回调微支付会有一个异步的通知给你,告诉你支付成功了,你还需要给他返一个成功标识,不然他还会不听的报警。这个异步通知的url是在2.5.2配置的notify_url。
(坑16:返回给微支付的必须是一个xml)写法如下,我的是springmvc的controller,不过都大同小异了:/**
* @category 监听通知,微信服务器调用闭环
* @param request
* @param response
* @return String
*/
@RequestMapping("wxnotify")
public void wxnotify(HttpServletRequest request, HttpServletResponse response) throws Exception{
System.out.println("微支付回调.1111111111111111111111111111111...");
InputStream inputStream = null;
try {
// 解析结果存储在HashMap
Map<String, String> map = new HashMap<String, String>();
inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();

// 遍历所有子节点
for (Element e : elementList){
map.put(e.getName(), e.getText());
}

//遍历MAP
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

if(map!=null&&"SUCCESS".equals(map.get("return_code"))){
Map logMap = tPayLogService.findOneByUUID(map.get("out_trade_no"));
if(logMap!=null&&"".equals(logMap.get("trade_status"))){//如果已经状态是成功则无视,否则UPDATE一下状态
Map paramMap = new HashMap();
logMap.put("trade_status", "TRADE_SUCCESS");
Map signMap = signService.findOneByKeyId(logMap.get("sign_id"));
Map userMap = tClientService.findOneByKeyId(signMap.get("client_id"));
Map activityMap = tActivityService.findOneByKeyId(signMap.get("activity_id"));

paramMap.put("social_type", userMap.get("social_type"));
paramMap.put("price_model", signMap.get("price_model"));

//支付成功
signMap.put("s_status", 2);//状态(0:免费;1:未付费;2:已付费)
signMap.put("sign_num", signService.findMaxSignNum(paramMap, activityMap));
signService.saveLogAndSignTransaction(logMap,signMap);

Map<String,Object> paramSignMap = new LinkedHashMap();//按顺序来的
paramSignMap.put("return_code","SUCCESS");
// paramSignMap.put("return_msg",);

String xmlInfo = XMLUtil.getRequestXml(paramSignMap);//生成xml,这个xml要post请求到微信的服务端
PrintWriter out = null;
try {
// 转码
// response.setContentType("text/html;charset=UTF-8");
out = response.getWriter();
out.write(xmlInfo);
out.flush();
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}finally{
if(inputStream!=null){
// 释放资源
inputStream.close();
inputStream = null;
}

}
}

最后,我只能以一句“GOOD LUCK”来做结束。有技术交流请加我的QQ。
下面附上我全部的页面代码:wxtest.jsp:

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ include file="/jsp/common/taglib.jsp"%>
<%@ page import="com.mingyisoft.bean.wxpay.WxpayUtil"%>
<%@ page import="com.mingyisoft.bean.wxpay.WxConfig"%>
<%@ page import="com.mingyisoft.bean.wxpay.WxpaySign"%>

<%@ page import="java.util.Map"%>
<html>
<head>
<meta charset="utf-8">

<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript" src="${ctx}/js/jquery/jquery-1.9.1.min.js"></script>

<script>
<%
String code = request.getParameter("code");//页面获取的code码
String state = request.getParameter("state");//报名ID

//第一个签名,用于第一个JS配置
Map<String,Object> signatureMap = WxpaySign.configSign(code,state);
%>

wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '<%=WxConfig.getAppID()%>', // 必填,公众号的唯一标识
timestamp: '<%=signatureMap.get("timestamp")%>', // 必填,生成签名的时间戳
nonceStr: '<%=signatureMap.get("nonceStr")%>', // 必填,生成签名的随机串
signature: '<%=signatureMap.get("signature")%>',// 必填,签名,见附录1
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});

wx.error(function(res){
for(i in res){
alert(i+":"+res[i]);
}
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
alert("微支付调用失败,请截图给管理人员,谢谢");
window.close();
});

// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
wx.ready(function(){
//调用统一下单接口
$.ajax({
type:"POST", //post提交方式默认是get
dataType:'json',
url:basePath+'/wxpostpreform',
data : {
openId :'<%=WxpayUtil.getOpenid(code)%>',
state : '<%=state%>'
},error:function(data) {// 设置表单提交出错
alert('系统出现异常,请联系管理员');
},success:function(result) {//提交成功
if(result.success){
var prepay_id = result.msg;//这个就是预订单ID
var payLogUUID = result.data;//自己的业务逻辑ID

//alert("预订单编号===>"+prepay_id);

$.ajax({
type:"POST", //post提交方式默认是get
dataType:'json',
url:basePath+'/getRealFormSignature',
data : {
prepay_id :prepay_id,
timestamp :'<%=signatureMap.get("timestamp")%>',
nonceStr :'<%=signatureMap.get("nonceStr")%>'
},error:function(data) {// 设置表单提交出错
alert('系统出现异常,请联系管理员');
},success:function(result3) {//提交成功
if(result3.success){
var realFormSignature = result3.msg;
//alert("最后订单的签名===>"+result3.msg);

//提交支付订单
wx.chooseWXPay({
timestamp: '<%=signatureMap.get("timestamp")%>',
nonceStr: '<%=signatureMap.get("nonceStr")%>',
package: 'prepay_id='+prepay_id,
signType: 'MD5',//SHA1
paySign: realFormSignature,
success:function(res1) {
//alert(res1);

//修改交易状态
$.ajax({
type:"POST", //post提交方式默认是get
dataType:'json',
url:'${ctx}/wxPaySaveSuccess',
data : {
key_id : payLogUUID,
trade_status :'TRADE_SUCCESS'//参数
},
error:function(data) {// 设置表单提交出错
alert('系统出现异常,请联系管理员');
},
success:function(result) {//提交成功
if(result.success){
alert("哇!成功啦,快去领取您的号码牌吧~");
window.location.href="${ctx}/sign/phoneSignSuccess/<%=state%>";
}else{
alert(result.msg);
alert("更新交易记录失败!");
}
}
});
},fail:function(res) {
//alert(res);
alert("本次支付失败,请联系客服人员,谢谢您的支持与理解。");
}
});

}else{
alert(result3.msg);
}
}
});
}else{
alert(result.msg);
}
}
});
});
</script>
</head>
<body>
<h1>系统正在提交订单,请勿跳转页面。</h1>
</body>
</html>