腾讯开放平台的接入是非常麻烦的, open.qq.com,腾讯开放平台的文档很多很杂,社交功能的api接口也很多还有。我现在只接了他的登录跟支付。
一、登录。
登录相对来讲还是比较简单的,首先前端sdk要正确接入获取access_token 跟 openid ,然后需要一个https 方式的get请求来取得进一步的信息。
url :https://graph.qq.com/user/get_simple_userinfo?oauth_consumer_key=%s&access_token=%s&openid=%s&clientip=&oauth_version=2.a&scope=all
填写好自己应用的所有内容。https协议的java实现
import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyhttpService { private int read_time_out = 10000; private Logger logger = LoggerFactory.getLogger(MyhttpService.class); public MyhttpService() { super(); } public MyhttpService(int time_out) { read_time_out = time_out; } public String doPost(String url, Map<String,String> params){ StringBuilder postData = new StringBuilder(); for(Entry<String,String> entry:params.entrySet()){ if(postData.length()!=0){ postData.append("&"); } postData.append(entry.getKey()).append("=").append(entry.getValue()); } return service(false, url, postData.toString(), "POST", null); } public String doPost(String url, Map<String,String> params,Map<String,String> headers){ StringBuilder postData = new StringBuilder(); for(Entry<String,String> entry:params.entrySet()){ if(postData.length()!=0){ postData.append("&"); } postData.append(entry.getKey()).append("=").append(entry.getValue()); } return service(false, url, postData.toString(), "POST", headers); } public String doPost(String url,String body){ return service(false, url, body, "POST", null); } public String doPost(String url, String postData, Map<String,String> headers){ return service(false, url, postData, "POST", headers); } public String doGet(String url, Map<String,String> headers){ return service(false, url, null, "GET", headers); } public String doGet(String url){ return service(false, url, null, "GET", null); } public String doHttpsPost(String url, String postData) { return service(true, url, postData, "POST",null); } public String doHttpsPost(String url, Map<String,String> params){ return doHttpsPost(url,params,null); } public String doHttpsPost(String url, Map<String,String> params,Map<String,String> headers){ StringBuilder postData = new StringBuilder(); for(Entry<String,String> entry:params.entrySet()){ if(postData.length()!=0){ postData.append("&"); } postData.append(entry.getKey()).append("=").append(entry.getValue()); } return service(true, url, postData.toString(), "POST", headers); } public String doHttpsGet(String url) { return service(true, url, null, "GET",null); } private String service(boolean isHttps, String url, String postData, String method, Map<String,String> headers){ HttpURLConnection conn = null; try { boolean doOutput = postData != null && postData.equals(""); conn = isHttps ? createHttpsConn(url, method, doOutput) : createHttpConn(url, method, doOutput); fillProperties(conn, headers); if(doOutput) writeMsg(conn, postData); String msg = readMsg(conn); logger.debug(msg); return msg; } catch (Exception ex) { logger.error(ex.getMessage(), ex); } finally { if (conn != null) { conn.disconnect(); conn = null; } } return null; } private HttpURLConnection createHttpConn(String url, String method, boolean doOutput) throws IOException { URL dataUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) dataUrl.openConnection(); conn.setReadTimeout(read_time_out); conn.setRequestMethod(method); conn.setDoOutput(doOutput); conn.setDoInput(true); return conn; } public static void main(String[] args) { // System.out.println(DigestUtils.md5DigestAsHex("19a98d31-4652-4b94-b7cd-129e8ddaliji11899CNY68appstoreQY7road-16-WAN-0668ddddSHEN-2535-7ROAD-shenqug-lovedede77".getBytes())); } private String readMsg(HttpURLConnection conn) throws IOException { return readMsg(conn, "UTF-8"); } private String readMsg(HttpURLConnection conn, String charSet) throws IOException { BufferedReader reader = null; try{ reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), charSet)); StringBuilder sb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); } return sb.toString(); } finally { if(reader != null){ reader.close(); } } } private void writeMsg(HttpURLConnection conn, String postData) throws IOException { DataOutputStream dos = new DataOutputStream(conn.getOutputStream()); dos.write(postData.getBytes()); dos.flush(); dos.close(); } private void fillProperties(HttpURLConnection conn, Map<String,String> params) { if(params == null||params.isEmpty()){ return; } for (Entry<String,String> entry: params.entrySet()) { conn.addRequestProperty(entry.getKey(), entry.getValue()); } } public String httpsPost(String url, String postData) { HttpURLConnection conn = null; try { boolean doOutput = (postData != null && postData.equals(""));//!Strings.isNullOrEmpty(postData); conn = createHttpsConn(url, "POST", doOutput); if (doOutput) writeMsg(conn, postData); return readMsg(conn); } catch (Exception ex) { // ingore // just print out logger.error(ex.getMessage(), ex); } finally { if (conn != null) { conn.disconnect(); conn = null; } } return null; } private HttpURLConnection createHttpsConn(String url, String method, boolean doOutput) throws Exception { HostnameVerifier hv = new HostnameVerifier() { public boolean verify(String urlHostName, SSLSession session) { return true; } }; HttpsURLConnection.setDefaultHostnameVerifier(hv); trustAllHttpsCertificates(); URL dataUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) dataUrl.openConnection(); conn.setReadTimeout(read_time_out); conn.setRequestMethod(method); conn.setDoOutput(doOutput); conn.setDoInput(true); return conn; } private static void trustAllHttpsCertificates() throws Exception { // Create a trust manager that does not validate certificate chains: javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1]; javax.net.ssl.TrustManager tm = new miTM(); trustAllCerts[0] = tm; javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, null); javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory( sc.getSocketFactory()); } public static class miTM implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public boolean isServerTrusted( java.security.cert.X509Certificate[] certs) { return true; } public boolean isClientTrusted( java.security.cert.X509Certificate[] certs) { return true; } public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException { return; } public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException { return; } } /** * 执行一个HTTP POST请求,返回请求响应的内容 * @param url 请求的URL地址 * @param params 请求的查询参数,可以为null * @return 返回请求响应的内容 */ public static String doPostforUC(String url, String body) { StringBuffer stringBuffer = new StringBuffer(); HttpEntity entity = null; BufferedReader in = null; HttpResponse response = null; try { DefaultHttpClient httpclient = new DefaultHttpClient(); HttpParams params = httpclient.getParams(); HttpConnectionParams.setConnectionTimeout(params, 20000); HttpConnectionParams.setSoTimeout(params, 20000); HttpPost httppost = new HttpPost(url); httppost.setHeader("Content-Type", "application/x-www-form-urlencoded"); httppost.setEntity(new ByteArrayEntity(body.getBytes("UTF-8"))); response = httpclient.execute(httppost); entity = response.getEntity(); in = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8")); String ln; while ((ln = in.readLine()) != null) { stringBuffer.append(ln); stringBuffer.append("\r\n"); } httpclient.getConnectionManager().shutdown(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); } catch (IllegalStateException e2) { e2.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { if (null != in) { try { in.close(); in = null; } catch (IOException e3) { e3.printStackTrace(); } } } return stringBuffer.toString(); } }内容的返回是json格式的,可以从里面找自己需要的内容来解析
JSONParser jsonParser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE); JSONObject obj; obj = (JSONObject) jsonParser.parse(doHttpsGet); String code = String.valueOf(obj.get("ret")); if(code.equals("0")){ String nickName = String.valueOf(obj.get("nickname"));后面就是自己服务器的逻辑了。登录相对来讲还是很简单的。
二、支付
1、腾讯的支付接口不知道是新开发的,还是涉及太多,总之非常乱,他们的开放平台的wiki上面有,四个服务,应用接入,移动接入,网站接入,腾讯云接入。因为我们是移动游戏所以,应该按照移动接入来接,但是实际上还要涉及应用接入这边的文档。只看一边的文档会发现少很多东西。按照文档接入出现问题。正题。
我手中的文档是 移动接入的sdk下载里面的
,腾讯的支付有两种:第一种是由腾讯来管理我们的支付,举例就是玩家充值的元宝在腾讯服务器上面,这个蛋疼的地方是,以后你所有的元宝操作都要跟腾讯交互,增加、扣除、赠送等等都要写协议去腾讯云处理。所以我们用了另外一种模式,道具购买模式。道具购买模式是直接花q点或者q币购买我们的道具,这个道具就是元宝。
2、这里有一个问题是,sdk里面自带的文档跟wiki上面的不一致,调用的接口也不是一个
这个对我们的影响在于后面的发货接口。发货接口的文档又再wiki上面,所以后面我们回到wiki的时候发现两份文档对不上。sdk文档包括腾讯托管跟我们自己管理元宝两种,第一种因为接口多,所以大部分是将第一种方式的。
3、道具购买服务器需要实现两个接口。购买道具下订单接口。购买结束回调接口。
下单接口需要客户端在登录时候取得 paytoken openkey pf pfkey 然后按照文档以 http 方式连接开放api就可以了。
//qq直接购买道具下单界面 public String qq_buy_items(String appid,String sessionId ,String openid,String pay_token,String openkey ,String amount,String pf,String pfkey){ String appkey = PlatformUtil.QQ_APPKEY; String apiaddress = "119.147.19.43";//qq测试地址 // String apiaddress = "openapi.tencentyun.com";//qq正式 //pf = "qq_m_qq-10000144-android-10000144-1111"; //pfkey = "pfkey"; OpenApiV3 openApiV3 = new OpenApiV3(appid, appkey, apiaddress); String zoneid="1"; Map<String,String> params = new HashMap<String, String>(); params.put("openid", openid); params.put("openkey", openkey); params.put("pf", pf); params.put("pfkey",pfkey); params.put("ts", String.valueOf(System.currentTimeMillis()/1000)); params.put("pay_token", pay_token); params.put("zoneid", zoneid); params.put("appmode", "1"); params.put("appid", appid); int iamount = SCUtils.calcScCount(amount+".0"); String payitem = String.format("100*1*%s",String.valueOf(iamount)); String goodsmeta = "元宝*元宝"; String goodsurl = "http://dragon.dl.hoolaigames.com/other/CH.png"; String app_metadata = String.format("%s-%s-",sessionId,String.valueOf(amount)); params.put("payitem", payitem); params.put("goodsmeta", goodsmeta); params.put("goodsurl", goodsurl); params.put("app_metadata",app_metadata); //这个在最终透传时候会增加腾讯的内容,*qdqd*qq 告诉我们是用什么方式支付的 // params.put("qq_m_qq",String.format("%s,%s,%s", appid,openid,openkey) ); try { Map<String,String> cookies = new HashMap<String, String>(); cookies.put("session_id", SnsSigCheck.encodeUrl("openid")); cookies.put("session_type", SnsSigCheck.encodeUrl("kp_actoken")); cookies.put("org_loc ",SnsSigCheck.encodeUrl("/mpay/buy_goods_m")); String api = openApiV3.api("/mpay/buy_goods_m", params,null ,"http"); return api; } catch (OpensnsException e) { log.error("openApiV3.api invoke failed",e); return "error"; } }其中的 OpenApiV3 其实可以从开放平台下载,是 http://wiki.open.qq.com/wiki/SDK%E4%B8%8B%E8%BD%BD 里面其实是一些验证以及http的访问。实际接入时候可以下载一个最新的看看。返回值也是一个json
JSONParser jsonParser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE); JSONObject obj; obj = (JSONObject) jsonParser.parse(payurl); int ret = (Integer) obj.get("ret"); String msg = (String) obj.get("msg"); String token_id = (String) obj.get("token"); String url_params = (String) obj.get("url_params");关于返回值里面参数,文档跟实际的返回有些出入,不一致,token 文档中写的是token_id 但实际返回的是token,这个可以实际debug看下再接收参数。得到的这些值需要发送给前端的sdk,前端的sdk会用这个返回的url 处理剩下的逻辑。
4、支付回调。客户端拿到刚才的url 会回传给腾讯,然后腾讯会调用我们在后台配置的回调接口,来通知支付结果,同时我们也要处理道具发放逻辑。这里有一个非常困难的问题 https协议的证书问题。 腾讯的证书最变态的一点是绑定ip地址,当然也是为了安全考虑。腾讯的后台我没有登录,但是应该是配置回调的ip地址,填写回调url 然后腾讯会生成一个绑定ip地址的证书,你需要安装这个证书在那台服务器上面,
发货URL用来给腾讯计费后台回调。用户付费成功后,腾讯计费后台将回调该URL给用户发货。在9001端口后可以是一个cgi或者php的路径。
hosting应用on CVM(即应用部署在腾讯CVM服务器上):
-发货URL只需HTTP协议即可,不需要使用SSL安全协议。
-必须使用9001端口(内网端口,需开发者主动启用,用apache iis或nginx做一个web监听,端口改成9001)。
hosting应用on CEE_V2(即应用部署在腾讯CEE_V2服务器上):
-发货URL只需HTTP协议即可,不需要使用SSL安全协议。
-必须使用9001端口(内网端口,需开发者主动启用,用apache iis或nginx做一个web监听,端口改成9001)。
-路径必须以ceecloudpay开头,即支付相关代码必须都放到应用根目录下的“ceecloudpay”目录下。
-对于CEE其发货URL的IP只能填写为10.142.11.27或者10.142.52.17(详见:CEE_V2访问云支付)。
non-hosting应用(即应用部署在开发者自己的服务器上):
-发货URL必须使用HTTPS协议。
-必须使用443端口(外网端口)。
-必须填写发货服务器所在运营商(电信/联通)。
5、证书的安装。
证书安装很坑爹的一个没有官方文档,官方有一个window浏览器的导入文档,没有linux的。这太无语了。
证书的安装可以安装在apache 或者 nginx 下面,我没有直接安装在tomcat下面,应该也是可以的吧,用apache或者nginx 可以做转发,转发到本地debug什么的。所以,我们用的是nginx做转发。首先腾讯后台下载一个这样的证书包。
这个里面带钥匙的那个需要密码,密码在readme里面,但是其实linux下面并没有用到,这个我估计是原始的密钥文件,可以和那个key生成 crt 文件,但是这里已经是生成好了的 crt 文件所以还是直接用比较好,先给第一个最长的那个起个别的名字。然后上传到 nginx 服务器的 conf 目录下面 ,nginx 在安装服务的时候应该是默认为支持https的 ssl 的,所以一般是不需要重新编译的,如果需要重新编译,可以去网上找找相关资料。如果你的 nginx 支持,那么就剩下一步,修改配置文件。
同样是conf目录下面的 nginx.conf
server { listen 443; server_name xxxxxxx; ssl on; ssl_certificate oem.crt; ssl_certificate_key oem.key; ssl_verify_client off; ssl_session_timeout 5m; ssl_protocols SSLv2 SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; ssl_client_certificate ca.crt; ssl_verify_depth 1; location ~ ^/xxxxxr/* { proxy_pass http://xxxxxx3; index index.jsp index.html index.htm; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }里面的
ssl_client_certificate
ssl_certificate对应证书里面的名字,修改完成后记得reload ./nginx -s reload 一下应该就生效了,我是做了转发本地处理的,当然也可以转发到任意服务器或者本机。如果下订单成功但是收不到回调,多半是这个证书的问题,可以看 nginx的log日志看看有没有访问到。如果没有80%都是证书的问题,询问下腾讯的支持让他们帮你查下日志吧,不过等他们反馈,估计你已经找到原因了。
6、支付回调的验证,当你终于能收到回调了,恭喜你你就要成功了。
对于支付回调的处理,其实很简单,但是腾讯的就很蛋疼。这就是上面说的蛋疼的问题,没有文档。sdk里面的文档说去看 wiki ,wiki里面的文档貌似不是这一版的,而且sdk文档里面的连接还是去 wiki 的主页,哎。 这里忍不住吐槽太多太乱,大家看上去都差不多,我哪知道是我需要的接口。最终我看到这个貌似像 :
http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3
这个文档上面的参数回调,大部分都是正确的。注意是大部分,因为收到的所有参数都要参与 HmacSHA1 签名,所以一个参数错误就悲剧了,你都不知道去哪里找,贴一下我最终的回调处理。
//qq支付回调接口,根据http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3 编写 protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Map<String, Object> obj = new HashMap<String, Object>(); try { String openid = request.getParameter("openid"); //根据APPID以及QQ号码生成,即不同的appid下,同一个QQ号生成的OpenID是不一样的。 String appid = request.getParameter("appid"); //应用的唯一ID。可以通过appid查找APP基本信息。 String ts = request.getParameter("ts"); //linux时间戳。 注意开发者的机器时间与腾讯计费开放平台的时间相差不能超过15分钟。 String payitem = request.getParameter("payitem"); //接收标准格式为ID*price*num G001*10*1 String token = request.getParameter("token"); //应用调用v3/pay/buy_goods接口成功返回的交易token String billno = request.getParameter("billno"); //支付流水号(64个字符长度。该字段和openid合起来是唯一的)。 String version = request.getParameter("version"); //协议版本 号,由于基于V3版OpenAPI,这里一定返回“v3”。 String zoneid = request.getParameter("zoneid"); //在支付营销分区配置说明页面,配置的分区ID即为这里的“zoneid”。 如果应用不分区,则为0。 String providetype = request.getParameter("providetype");//发货类型 0表示道具购买,1表示营销活动中的道具赠送,2表示交叉营销任务集市中的奖励发放。 //Q点/Q币消耗金额或财付通游戏子账户的扣款金额。可以为空 若传递空值或不传本参数则表示未使用Q点/Q币/财付通游戏子账户。注意,这里以0.1Q点为单位。即如果总金额为18Q点,则这里显示的数字是180。 String amt = request.getParameter("amt"); String payamt_coins = request.getParameter("payamt_coins");//扣取的游戏币总数,单位为Q点。 String pubacct_payamt_coins = request.getParameter("pubacct_payamt_coins");//扣取的抵用券总金额,单位为Q点。 String appmeta = request.getParameter("appmeta"); String clientver = request.getParameter("clientver"); String sig = request.getParameter("sig"); String url = "/xxx/xxxx"; Map<String, String> params = createCallbackParamsMap(openid, appid, ts, payitem, token, billno, version, zoneid, providetype, amt, payamt_coins, pubacct_payamt_coins, appmeta,clientver); if(SnsSigCheck.verifySig(request.getMethod(), url,params, PlatformUtil.QQ_APPKEY+"&", sig)){ if(ok){ }else{ obj.put("ret", 0); obj.put("msg", "ok"); } }else{ log.info("qqPayCallback SnsSigCheck fail."); obj.put("ret", -5); obj.put("msg", "签名错误"); } String resp = JSONObject.toJSONString(obj); out.println(resp); } catch (SQLException | DbException | ProtocolException | NumberFormatException | OpensnsException e) { e.printStackTrace(); } finally { out.close(); }中间标红的地方都是有问题的地方,都是坑。首先
pubacct_payamt_coins是有可能传空的,因为你没有用抵用券对吧,但是记住这个也需要加入签名。 clientver 神坑。我最终也没再文档或者哪里找到这个参数为什么给我传过来,但是你就是传过来了,而且你还必须接收,必须加入签名中去,也许我水平太菜,反正我是没找到这个参数在那个文档上面写了。
createCallbackParamsMap 字面意思就是把参数弄到 map里面
SnsSigCheck.verifySig 这也是上面下载的那个工具项目中自带的功能,其实就是一个 HmacSHA1 的 utf 格式的签名。可以下载,有兴趣的也可以自己写写。
(三)总结
腾讯的支付接口应该做的不难,困难在于没有一个明确的文档。