企业微信-企业支付开发
一、开发前准备
1)企业微信
2)商户号(微信支付商户平台账号)
3)wx-pay sdk, jssdk
二、开发前了解开发文档,以及相关概念。
官方文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90280。
先来了解企业支付需要的一些相关概念:
1、corpid:每个企业都拥有唯一的corpid,获取此信息可在管理后台"我的企业" - "企业信息"下查看"企业id"(需要有管理员权限)。
2、userid:每个成员都有唯一的userid,即所谓的"账号"。在管理后台->"通讯录"->点进某个成员的详情页,可以看到。
3、agentid:每个应用都有唯一的agentid。在管理后台->"应用与小程序"->"应用",点进某个应用,即可以看到agentid。
4、secret:secret是企业应用里面用于报障数据安全的"钥匙",每个应用都有一个独立的访问密钥,为了保证数据的安全,secret不能泄露。
5、access_token:access_token是企业后台去企业微信的后台获取信息时的重要票据,由corpid和secret产生。所有接口在通信时都要携带此信息用于验证接口的访问权限。
企业支付分为:企业红包、向员工付款、向员工收款三类。如下图所示,此次主要详说向员工收款。
按文档介绍,第一步,开通企业微信专区。第二步,获取用户openid。第三步,添加jsapi的权限验证。第四步,发起向员工收款。第五步,调用支付jsapi完成支付。
第一步就不讲了,因为我没有企业微信管理员账号,都是让人给配置好了,我再用的。
将上面的步骤文字转换为图来看 ,过程就比较清晰明了了。(如下图)
1)首先获取access_token。
请求方式: get(https)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=id&corpsecret=secret
需要两个参数,corpid和corpsecret,就是前面的概念介绍的企业id和应用id。获取到的access_token有效时间为7200秒即两小时,因为获取access_token的接口有访问次数限制,所以我们需要把获
取到的access_token缓存起来。这个access_token是我们访问其他接口要用到的。(注意access_token可能会提前失效,逻辑中要判断失效重新调接口获取)。
2)获取用户openid
在h5页面上发起向员工收款,需要获取到用户的openid信息,但在企业微信上我们没有直接获取openid的api,需要先获取到userid,再通过userid转换为openid。获取useid需要构造网页授权链接,
通过链接获取code参数,再通过code参数获取用户userid信息。
2.1)构造网页授权地址:https://open.weixin.qq.com/connect/oauth2/authorize?appid=corpid&redirect_uri=redirect_uri&response_type=code&scope=snsapi_base&state=state#wechat_redirect
这里的参数如下图说明:
这里state是选填的,但是一般还是带上,企业微信会原样返回你给的参数,可以拿这个区分自己是通过哪个方法去请求网页授权的。redirect_uri需要用urlencode处理。构造好网页授权地址后,访问后,页面会跳转到redirect_uri给的地址,并带上code和原样返回的state参数。拿到code后我们就可以用code获取userid了。(注意:code只有5分钟的有效期,并且只能使用一次)
2.2)获取访问用户身份
请求方式:get(https)
请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=access_token&code=code
这里两个参数access_token和code就是前两步我们获取到的。
权限说明:跳转的域名须完全匹配access_token对应应用的可信域名,否则会返回50001错误。(这个是在第一步,开通企业微信专区那儿设置的)
请求之后,就会返回用户身份信息了,里面包含userid。返回数据格式:{
"errcode": 0,
"errmsg": "ok",
"userid":"userid",
"deviceid":"deviceid"
}
2.3)userid转换为openid
请求方式:post(https)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=access_token
这里需要两个参数一个是access_token放在url地址里,另一个是userid。userid不能拼接在地址后面,需要放到body里面并且是json格式。提供一个方法来进行post请求。
1 public static jsonobject dojsonpost(string url, jsonobject jsonobject) { 2 httpclient client = httpclientbuilder.create().build(); 3 httppost post = new httppost(url); 4 jsonobject response = null; 5 6 try { 7 stringentity s = new stringentity(jsonobject.tostring()); 8 s.setcontentencoding("utf-8"); 9 s.setcontenttype("application/json"); 10 post.setentity(s); 11 httpresponse res = client.execute(post); 12 if (res.getstatusline().getstatuscode() == httpstatus.sc_ok) { 13 httpentity entity = res.getentity(); 14 string result = entityutils.tostring(entity); 15 response = jsonobject.parseobject(result); 16 } 17 } catch (exception e) { 18 throw new runtimeexception(e); 19 } 20 return response; 21 }
请求成功返回一个jsonobject{
"errcode": 0,
"errmsg": "ok",
"openid": "odogms-6ycngrrovbj2yhij5jl6e"
},里面包含 openid信息。
3)添加jsapi权限验证
拿到openid后,就差不多完成了发起支付的前期准备条件,不过调用jsapi支付之前,需要对请求的jsapi进行权限验证。在调用jsapi的页面引入jssdk,地址为https://res2.wx.qq.com/open/js/jweixin-1.4.0.js 然后,在页面添加如下脚本。脚本所需的参数建议通过java后台代码一次性获取后传到页面赋值。
wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来(开发时设为true,发布时记得调为false) appid: 'appid', // 必填,企业微信的corpid timestamp: 'timestamp', // 必填,生成签名的时间戳 noncestr: 'noncestr', // 必填,生成签名的随机串 signature: 'signature',// 必填,签名 jsapilist: ['getbrandwcpayrequest'] });
appid就是企业微信的corpid,timestamp可以去当前时间的时间戳,noncestr可以用微信sdk生成一个随机字符串,signature签名,需要通过签名算法来生成了,其中又需要额外获取一个jsapi_ticket票据。
3.1)js-sdk签名算法
生成签名之前必须拿到一个调用企业微信的临时票据jsapi_ticket,jsapi_ticket有效期为7200,且一小时内只能获取400次,单个应用获取不能超过100次,所以需要缓存起来备用。
请求方式:get(https)
请求url:https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=access_token
就一个参数access_token。请求后返回的数据格式:
{
"errcode":0,
"errmsg":"ok",
"ticket":"bxldikrxvbtpdhsm05e5u5suoxnkd8-41zo3mhkoyn5ofkwitdggnr2fwj0m9e8nyzwkvzvdvtaugwvsdshfka",
"expires_in":7200
}
3.1.1)签名
参与签名的参数有四个: noncestr(随机字符串), jsapi_ticket, timestamp(时间戳), url(当前网页的url, 不包含#及其后面部分)。有两个注意点:1. 字段值采用原始值,不要进行url转义;2. 必须严格按照如下格式拼接,不可变动字段顺序。
jsapi_ticket=jsapiticket&noncestr=noncestr×tamp=timestamp&url=url
然后对拼接出来的字符串作sha1加密即可得到signature。注意:1、url是你调用jsapi的页面的url,包括参数,但不包括#及其后面部分。2、noncestr和timestamp必须和之前的wx.config中的noncestr和timestamp一致。下面贴出sha1加密方法。
1 public static string sha1(string des){ 2 char[] hexdigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 3 'a', 'b', 'c', 'd', 'e', 'f'}; 4 messagedigest mdtemp = null; 5 try { 6 mdtemp = messagedigest.getinstance("sha1"); 7 try { 8 mdtemp.update(des.getbytes("utf-8")); 9 } catch (unsupportedencodingexception e) { 10 e.printstacktrace(); 11 } 12 } catch (nosuchalgorithmexception e) { 13 e.printstacktrace(); 14 } 15 16 17 byte[] md = mdtemp.digest(); 18 int j = md.length; 19 char[] buf = new char[j * 2]; 20 int k = 0; 21 for (int i = 0; i < j; i++) { 22 byte byte0 = md[i]; 23 buf[k++] = hexdigits[byte0 >>> 4 & 0xf]; 24 buf[k++] = hexdigits[byte0 & 0xf]; 25 } 26 string sign= new string(buf); 27 return sign; 28 }
这些参数在java后台代码获取之后,输出到页面去,就完成了jsapi权限验证。接下来就是进行统一下单了。
4)统一下单
统一下单代码:
1 public map<string, string> getunifiedorderresult(string body, string tradeno,string totalfee,string openid){ 2 3 try { 4 map<string , string > data = new hashmap<>(); 5 data.put("body", body);//商品描述 6 data.put("attach", body);//附加数据 7 data.put("out_trade_no",tradeno );//自己生成的订单号,必须唯一 8 data.put("fee_type", "cny");//货币种类 9 data.put("total_fee", totalfee);//付款金额 10 data.put("trade_type", "jsapi");//交易类型 11 data.put("notify_url", "你的回调地址,不能带参数,外网可访问"); 12 data.put("openid", openid);//付款用户openid 13 data.put("sign_type","md5");//加密方式 14 unifiedorderresult = wxpay.unifiedorder(data); 15 return unifiedorderresult; 16 } catch (exception e) { 17 //throw new exception(string.format("unifiedorder response error!") ); 18 } 19 return null ; 20 }
统一下单就是调用微信接口,预下单。请求地址为:https://api.mch.weixin.qq.com/pay/unifiedorder。这个在wx-pay sdk中封装好了,我们只需要调用就行了。
返回为xml格式的数据如下:
<xml>
<return_code><![cdata[success]]></return_code>
<return_msg><![cdata[ok]]></return_msg>
<appid><![cdata[wx2421b1c4370ec43b]]></appid>
<mch_id><![cdata[10000100]]></mch_id>
<nonce_str><![cdata[iitri8iabbblz1jc]]></nonce_str>
<openid><![cdata[oupf8umuajo_m2pxb1q9znjwes6o]]></openid>
<sign><![cdata[7921e432f65eb8ed0ce9755f0e86d72f]]></sign>
<result_code><![cdata[success]]></result_code>
<prepay_id><![cdata[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
<trade_type><![cdata[jsapi]]></trade_type>
</xml>
prepay_id为预支付交易会话标识,有效期为2小时,我们需要这个数据去请求jsapi完成支付。建议这个数据保存下来,以便不小心没有支付成功,可以再次使用(须在两小时内支付)。
(统一下单的微信官方文档链接为:)
5)前端页面调用jsapi完成支付
在页面中调用weixinjsbridge内置对象发起支付,注意1、这个页面和前面进行jsapi权限验证用于前面的url是同一个页面。2、weixinjsbridge内置对象只在微信浏览器有用,在其他浏览器中无效。
//调用微信js api 支付 function onbridgeready() { weixinjsbridge.invoke( 'getbrandwcpayrequest', ${jsapiprarmeters}, function (res) { if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判断前端返回,微信团队郑重提示: //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 $.toast("支付成功"); location.href ='支付成功后返回的页面' } else{ $.toast("${ordercode}"); $.toast("支付失败", "forbidden"); history.go(-1); } }); } if (typeof weixinjsbridge == "undefined"){ if( document.addeventlistener ){ document.addeventlistener('weixinjsbridgeready', onbridgeready, false); } else if (document.attachevent){ document.attachevent('weixinjsbridgeready', onbridgeready); document.attachevent('onweixinjsbridgeready', onbridgeready); } } else{ onbridgeready(); } //这里${jsapiparameters} 我是在java服务端代码获取到,在传到页面来的,其格式如下: { "appid":"wx2421b1c4370ec43b", //企业corpid "timestamp":"1395712654", //时间戳,自1970年以来的秒数 "noncestr":"e61463f8efa94090b1f366cccfbbb444", //随机串 "package":"prepay_id=u802345jgfjsdfgsdg888", //这个就是统一下单时生成的预支付交易会话标识 "signtype":"md5", //加密方式 注意此处需与统一下单的签名类型一致 "paysign":"70ea570631e4bb79628fbca90534c63ff7fadd89" //微信签名 },
这里paysign必须按照微信要求的算法来生成。我生成jsapiparameters的方法如下
1 public string getjsapiparameters() 2 { 3 try { 4 long time =system.currenttimemillis()/1000; 5 string timestamp=time.tostring(); 6 map<string,string> jsapiparam = new hashmap<>() ; 7 jsapiparam.put("appid", unifiedorderresult.get("appid")); 8 jsapiparam.put("timestamp", timestamp); 9 jsapiparam.put("noncestr", unifiedorderresult.get("nonce_str")); 10 jsapiparam.put("package", "prepay_id=" + unifiedorderresult.get("prepay_id"));//统一下单返回的预支付交易会话标识 11 jsapiparam.put("signtype", "md5"); 12 jsapiparam.put("paysign", wxpayutil.generatesignature(jsapiparam, "key"));//kee是微信商户平台的密钥( 微信商户平台(pay.weixin.qq.com)-->账户设置-->api安全-->密钥设置),调用微信sdk封装好的方法进行签名 13 14 15 object parameters = json.tojson(jsapiparam); 16 return parameters.tostring(); 17 } 18 catch (exception e){ 19 20 } 21 22 return null; 23 }
这个方法其实就是按这个顺序组装好字符串,"appid=你的企业corpid×tamp=12312312&noncestr=noncestr&package=prepay_id=统一下单预付交易会话标识&signtype=md5&key=商户密钥";然后调用微信sdk的generatesignature方法进行签名。
至此企业微信支付的主体流程就完成了。下面再说说支付完成后回调的处理,在这里遇到一个坑。
1 @requestmapping(value = "/wxpaynotifyurl",method = {requestmethod.get,requestmethod.post}) 2 @responsebody 3 public string wxpaynotifyurl(httpservletrequest request, httpservletresponse response) { 4 5 string resxml = getwxnotifyxml(request,response); 6 return payback(resxml); 7 } 8 9 10 public string payback(string notifydata) { 11 string xmlback = ""; 12 map<string, string> notifymap = null; 13 try { 14 15 notifymap = wxpayutil.xmltomap(notifydata); 16 log.info("回调数据转map结束"+notifymap);// 转换成map 17 //坑就在这里,微信文档前面涉及到的签名一般默认都是md5加密,并且微信sdk验证签名有效性的方法默认也是用md5加密,所以因为这个回调失败了,通过日志才知道是因为验证签名有效性不通过,后来改成hmac-sha256签名方式才回调成功 18 if (wxpayutil.issignaturevalid(notifymap, wxconfig.getinstance().getkey(), wxpayconstants.signtype.hmacsha256)) { 19 string return_code = notifymap.get("return_code");//状态 20 if(return_code.equals("success")){ 21 if(notifymap.get("result_code").equals("success")){ 22 string out_trade_no = notifymap.get("out_trade_no");//订单号 23 if (out_trade_no == null) { 24 log.info("微信支付回调失败订单号:", notifymap); 25 xmlback = "<xml>" + "<return_code><![cdata[fail]]></return_code>" + "<return_msg><![cdata[报文为空]]></return_msg>" + "</xml>"; 26 return xmlback; 27 } 28 log.info("回调成功返回的订单号"+out_trade_no); 29 //回调成功后对订单状态做改变 30 string ordercode = out_trade_no; 31 order order = ordermapper.findbyordercode(ordercode); 32 33 if (order.getorderstatusname().equals("未付款")) { 34 order.setorderstatus(1); 35 order.setorderstatusname("已付款"); 36 ordermapper.updateorder(order); 37 } 38 xmlback = "<xml>" + "<return_code><![cdata[success]]></return_code>" + "<return_msg><![cdata[success]]></return_msg>" + "</xml> "; 39 return xmlback; 40 } 41 } 42 } else { 43 xmlback = "<xml>;" + "<return_code><![cdata[fail]]></return_code>" + "<return_msg><![cdata[报文为空]]></return_msg>" + "</xml> "; 44 log.info("微信支付回调签名验证失败:"+ notifymap); 45 return xmlback; 46 } 47 } catch (exception e) { 48 log.info("处理回调数据时异常", e.getstacktrace()); 49 xmlback = "<xml>" + "<return_code><![cdata[fail]]></return_code>" + "<return_msg><![cdata[报文为空]]></return_msg>" + "</xml> "; 50 } 51 log.info("处理回调数据结束", xmlback); 52 return xmlback; 53 } 54 55 56 public string getwxnotifyxml(httpservletrequest request, httpservletresponse response){ 57 string resxml = ""; 58 try { 59 inputstream inputstream; 60 stringbuffer sb = new stringbuffer(); 61 inputstream = request.getinputstream(); 62 63 bufferedreader reader = new bufferedreader(new inputstreamreader(inputstream, "utf-8")); 64 string line = null; 65 try { 66 while ((line = reader.readline()) != null) { 67 sb.append(line + "\n"); 68 } 69 } catch (ioexception e) { 70 log.info("流转xml失败"+e.getstacktrace()); 71 e.printstacktrace(); 72 } finally { 73 try { 74 reader.close(); 75 inputstream.close(); 76 } catch (ioexception e) { 77 e.printstacktrace(); 78 } 79 } 80 resxml = sb.tostring(); 81 82 } catch (exception e) { 83 log.info("流转xml失败"+ e.getstacktrace()); 84 } 85 return resxml; 86 }
为什么要回调呢,因为调用jsapi只是把付款操作给到了微信,微信返回ok也只是表示接收到这个操作指令了,并不一定真正处理成功,所以需要回调告知处理结果。在回调方法里,一定要再次进行一次签名,和微信返回的签名数据进行对比,以防被串改过。
回调成功后,返回给微信的数据格式如下,如果没有返回这个数据给微信,微信会回调多次,我们在代码中要能识别同一个订单的回调。
<xml>
<return_code><![cdata[success]]></return_code>
<return_msg><![cdata[ok]]></return_msg>
</xml>
上一篇: 给IConfiguration写一个GetAppSetting扩展方法
下一篇: 今天出门运气不太好