編輯:關於Android編程
騰訊開放平台的接入是非常麻煩的, 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內容的返回是json格式的,可以從裡面找自己需要的內容來解析params){ StringBuilder postData = new StringBuilder(); for(Entry 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 params,Map headers){ StringBuilder postData = new StringBuilder(); for(Entry 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 headers){ return service(false, url, postData, "POST", headers); } public String doGet(String url, Map 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 params){ return doHttpsPost(url,params,null); } public String doHttpsPost(String url, Map params,Map headers){ StringBuilder postData = new StringBuilder(); for(Entry 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 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 params) { if(params == null||params.isEmpty()){ return; } for (Entry 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(); } }
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下載裡面的
vcrHo6zS1LrzxOPL+dPQtcTUqrGmstnX97a80qq4+sza0ba9u7ulo6zU9rzToaK/27P9oaLU+cvNtci1yLa80qrQtNCt0unIpcza0bbUxrSmwO2ho8v50tTO0sPH08PBy8HtzeLSu9bWxKPKvaOstcC+37m6wvLEo8q9oaO1wL7fubrC8sSjyr3Kx9axvdO7qHG147vy1d9xsdK5usLyztLDx7XEtcC+36Os1eK49rXAvt++zcrH1KqxpqGjPC9wPgo8cD4yoaLV4sDv09DSu7j2zsrM4srHo6xzZGvA78Pm19S0+LXEzsS1tbj6d2lracnPw+a1xLK70rvWwqOstffTw7XEvdO/2tKysrvKx9K7uPY8L3A+CjxwPjxpbWcgc3JjPQ=="" alt="\">這個對我們的影響在於後面的發貨接口。發貨接口的文檔又再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其中的 OpenApiV3 其實可以從開放平台下載,是 http://wiki.open.qq.com/wiki/SDK%E4%B8%8B%E8%BD%BD 裡面其實是一些驗證以及http的訪問。實際接入時候可以下載一個最新的看看。返回值也是一個jsonparams = new HashMap (); 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 cookies = new HashMap (); 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"; } }
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中間標紅的地方都是有問題的地方,都是坑。首先obj = new HashMap (); 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 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 格式的簽名。可以下載,有興趣的也可以自己寫寫。
(三)總結
騰訊的支付接口應該做的不難,困難在於沒有一個明確的文檔。
最近研究HyBrid的兩種方式:一、直接原生WebView1)初始化WebView: //啟動javascript webView = (WebView)
紅米手機快捷鍵使用技巧匯總。紅米手機,在市場的位置也慢慢變重要了,價格低,又實惠,又好用。那朋友們,你們知道它有那些快捷鍵的嗎?那就讓小編來跟大家詳細的介紹
EasyLikeArea Easy like area in the circle of friends or QQ qzone
本文實例講述了Android編程之SurfaceView用法。分享給大家供大家參考,具體如下:關於surfaceView相關知識:View和SurfaceView主要區別