微支付 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的编码集,一个写法上的顺序,一个斜杠,这些小细节直接会引发各种错误,希望大家要非常细节的来对待,这不是演习!最后更新时间:2015年4月26日
正篇:
亚瑞特巨坑,微支付jsapi巨坑
1.在微信网站上的设置

(你必须是服务号并且申请各种认证通过后才能往下走,否则请回炉!)

首先请登录 https://mp.weixin.qq.com
微信公众平台,进行如下配置,谢谢。
1.1微信支付-开发配置
1.2公众号设置-功能设置
1.3开发者中心-配置项-接口权限
1.4 微信支付-商户平台
(坑1:此处为最大的巨坑,要不是大神告诉我要配置这里,我死也想不出这里也需要配置,这里不配置的话,后面的预订单提交会一直报错,并且那个错也许你永远都不知道原因。。。阿西)
请登录: https://pay.weixin.qq.com 微信支付-商户平台
将这个秘钥设置成和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%2F test.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+timestamp
String 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>



微支付 js-api java 坑王之王!!!相关推荐

  1. 微支付js-api,java版的坑。

     微支付的js-api,我是不想要喷你,是全国人民选我来喷你. 尼玛,这是我有屎以来遇到过最深的坑,坑里面还有碎玻璃,各种怪兽,各种机关,暗器,各种折磨,看完你们的文档,看着内牛满面. 乱七八糟完 ...

  2. 【坑王之王】OException: Mkdirs failed to create C:/Users/sunxiaochuan/AppData/Local/Temp/*************错误解决

    ERROR StreamMetadata: Error writing stream metadata StreamMetadata IOException: Mkdirs failed to cre ...

  3. 微支付jsapi巨坑 微支付 jsapi java

    1.在微信网站上的设置 (你必须是服务号并且申请各种认证通过后才能往下走,否则请回炉!) 首先请登录https://mp.weixin.qq.com 微信公众平台,进行如下配置,谢谢. 1.1微信支付 ...

  4. 基于ArcGIS JS API封装dojo微件(以工具条为例)

    1.应用场景: 我们知道ArcGIS JS API自带了一些微件(或者说是控件),比如缩放按钮.定位按钮等等.但是有的时候这些微件的样式不太符合项目实际要求,或者是项目上想要把这些组合起来,这时候我们 ...

  5. 微信公众号页面支付接口java,[Java教程]微信公众号支付(三):页面调用微信支付JS并完成支付...

    [Java教程]微信公众号支付(三):页面调用微信支付JS并完成支付 0 2015-09-15 15:00:30 一.调用微信的JS文件 1.首先要绑定[JS接口安全域名],"公众号设置&q ...

  6. 微信支付开发(1) JS API支付

    From: http://www.cnblogs.com/txw1958/p/wxpayv3-jsapi.html 关键字:微信支付 微信支付v3 jsapi支付 统一支付 Native支付 prep ...

  7. Paypal REST API Java 版 PC端商城支付接口对接。

    引言: 同类文借鉴链接:http://blog.csdn.net/change_on/article/details/73881791(对此博主万分感谢) Paypal账号注册网址:https://w ...

  8. 微信公众平台开发:JS API支付

    本文介绍微信支付下的jsapi实现流程 前言 微信支付现在分为v2版和v3版,2014年9月10号之前申请的为v2版,之后申请的为v3版.V3版的微信支付没有paySignKey参数.v2的相关介绍请 ...

  9. php 微信支付闪了一下,php,_微信公众号JS API支付,安卓没有效果(会闪一下就消失了),php - phpStudy...

    微信公众号JS API支付,安卓没有效果(会闪一下就消失了) 代码如下,ios可以支付,但是到安卓手机上就会出现微信支付读条(那三个点),然后就消失了,没有跳出输入密码支付的界面.... //调用微信 ...

最新文章

  1. 通俗讲java反射机制ioc,结合反射说明SpringIOC的实现原理
  2. 实验二matlab数值,实验二MATLAB数值计算
  3. php遍历一个目录 并重命名
  4. Oracle收购Sun
  5. SIP学习之网络链接
  6. SharePoint 2010的数据库服务器实例默认为sqlserver2
  7. 谈谈持久连接——HTTP权威指南读书心得(五)
  8. Gephi下载百度云加速,舒服了
  9. 格拉布斯离群值检验——理论与 Python 实现
  10. 【通信仿真】基于matlab STAP全自由度空时自适应处理【含Matlab源码 1956期】
  11. 分享5个宝藏文字转语音配音软件,错过太可惜
  12. Linux usbkey自动登陆,Usbkey怎么用?|3分钟让您了解Usbkey使用方法
  13. 落枕了睡觉还枕枕头吗_兄弟,你还枕头吗?
  14. wireshark如何抓取本机包
  15. 中考计算机易错知识点,【中考备考】易错知识点归类
  16. python爬虫selenium-前程无忧
  17. 分枝限界法求解流水线作业调度问题
  18. 使用Charles抓取https请求
  19. 北大计算机科学与技术保研率,北大信科---我的保研路
  20. 智慧营区管理系统软件:提供智慧营区一体化平台解决方案

热门文章

  1. 复杂美入选浙江省2021年科技型中小企业
  2. 一卷河图赋太虚:HMS Core CG kit与移动游戏新可能
  3. 类和对象(中)——构造函数介绍
  4. 使用IDEA调试DBeaver
  5. 无损压缩算法专题——RLE算法实现
  6. imgaug+augmentor
  7. Java快速创建多级目录
  8. html点击页面放大,JS实现点击图片跳转页面放大功能【原创】
  9. linux 怎么对目录设置限额
  10. 2020上半年已过,疫情下互联网迎来红利期,Android技术下半场在哪?