Kii BLOG
应用内支付(IAP)服务端扩展(Server Extension)Demo
为什么要做这个Demo? 因为市场有潜在的需求:
- 在Google商店和Apple应用商店中,他们提供了 IAP(应用内付费)功能。作为一个开发者,你可以上架你的IAP商品,但是你没法上架商品的内容。例如:如果你的IAP商品是一首歌,你没法上传这首歌到Apple的IAP服务器。用我们的话来说就是:他们支持对象(Object),但是不支持实体(Body)。
- 很多中国本地的应用商店是不支持IAP的,所以开发者们没有办法在应用商店中使用他们的IAP功能。
我们为第二种情况准备了这个Demo,当然如果做一些微小的变动也可以支持第一个种情况。
作为开发者,在编写代码前我们需要定义一些类:商品和收据。商品类包含一些诸如商品名、描述、新上架、推荐、价格、货币单位、商品ID、类型(消耗品、永久商品、时限商品)、缩略图和实体等字段。收据类包含诸如商品名、收据ID、交易号、价格、货币单位、购买日期和商品ID等字段。
流程大致是这样的:
- 开发者设置应用级数据仓库(Bucket)的ACL:对所有用户只读,所以需要删除所有用户对该Bucket的写权限。
- 开发者创建服务端扩展来接收购买请求并将收据写入用户的Bucket。
- 新增商品,通常这一步是在本地的浏览器中完成,但是我不是很了解JS程序,所以我写了一个Java的代码作为演示。
- 按需上传商品实体。因为一个商品只能包含唯一的文件实体,我把缩略图进行base64编码后作为键值对放入KiiObject中。
- 客户端可以通过 KiiCloud SDK列出应用级Bucket “product”中所有的商品。
- 客户端可以通过服务端扩展API购买商品。具体来说:如果使用支付宝(AliPay)进行支付,那么服务端扩展API需要设置支付宝通知的URL。
- 客户端从用户的收据Bucket中读取收据信息,如果存在这个收据就可以从服务端直接下载商品实体。
部分示例代码如下:
设置应用级Bucket的ACL
public static void SetBucketACL(String token) { try { HttpDelete delete = new HttpDelete( String.format(PRODUCT_BUCKET_URL, APP_ID) + "/acl/CREATE_OBJECTS_IN_BUCKET/UserID:ANY_AUTHENTICATED_USER"); addCommonHeader(delete, token); doDelete(delete); delete = new HttpDelete( String.format(PRODUCT_BUCKET_URL, APP_ID) + "/acl/WRITE_EXISTING_OBJECT/UserID:ANY_AUTHENTICATED_USER"); addCommonHeader(delete, token); doDelete(delete); delete = new HttpDelete( String.format(PRODUCT_BUCKET_URL, APP_ID) + "/acl/DROP_BUCKET_WITH_ALL_CONTENT/UserID:ANY_AUTHENTICATED_USER"); addCommonHeader(delete, token); doDelete(delete); } catch (Exception e) { e.printStackTrace(); } } }
服务端扩展
function buyProduct(params, context, done) { Kii.initializeWithSite(context.getAppID(), context.getAppKey(), KiiSite.CN); Kii.authenticateAsAppAdmin("XXXX", "XXXX", { success: function(adminContext) { var bucket = adminContext.bucketWithName("products"); var clause = KiiClause.equals("product_id", params.product_id); var product_query = KiiQuery.queryWithClause(clause); var queryCallbacks = { success: function(queryPerformed, resultSet, nextQuery) { //done("query success " + resultSet.length); if (resultSet.length > 0) { var product = resultSet[0]; KiiUser.authenticateWithToken(params.token, { success: function(theAuthedUser) { var bucket = theAuthedUser.bucketWithName("receipt"); var object = bucket.createObject(); object.set("name", params.product_name); object.set("receipt_id", params.receipt_id); object.set("trade_no", params.trade_no); object.set("price", params.price); object.set("price_locale", params.price_locale); object.set("product_id", params.product_id); //set the date to object.set("purchase_date", Date.parse(new Date())/1000); object.save({ success: function(theObj) { done(theObj); }, failure: function(error) { done(error); } }); }, failure: function(theUser, anError) { done(anError); } }); } else { done("no such product id: " + params.product_id); }; }, failure: function(queryPerformed, error) { done(error); } }; bucket.executeQuery(product_query, queryCallbacks); }, failure: function(error, statusCode) { done(error); } }); }
新增商品
public static String AddProduct(String token, Product product) { if (product.product_id == null || product.product_id.length() == 0) { throw new RuntimeException("Product must have a valid product id!"); } List<Product> products = GetProductsList(); for (Product p : products) { if (p.product_id.contentEquals(product.product_id)) { throw new RuntimeException("Product with " + product.product_id + " already exists on KiiCloud, create a new one!"); } } HttpPost request = new HttpPost(String.format(PRODUCT_BUCKET_URL, APP_ID) + "/objects"); addCommonHeader(request, token); request.addHeader(CONTENT_TYPE, CONTENT_ADD_OBJECT); try { request.setEntity(new StringEntity(new Gson().toJson(product))); HttpResponse response = new DefaultHttpClient().execute(request); String ret = logResponse(response); ObjectResult result = new Gson().fromJson(ret, ObjectResult.class); return result.objectID; } catch (Exception e) { e.printStackTrace(); } return null; }
上传商品实体
public static boolean UploadFileBody(String productId, String filePath, String token) { Utils.log(TAG, "upload product, product id is " + productId + ", file path is " + filePath); String url = String.format(BODY_URL, APP_ID, productId); Utils.log(TAG, "upload url is " + url); HttpPut request = new HttpPut(url); try { addCommonHeader(request, token); request.addHeader(CONTENT_TYPE, CONTENT_UPLOAD_TEXT); File f = new File(filePath); if(!f.exists()) { return false; } FileInputStream fis = new FileInputStream(f); byte[] data = new byte[fis.available()]; fis.read(data); fis.close(); request.setEntity(new ByteArrayEntity(data)); HttpResponse response = new DefaultHttpClient().execute(request); logResponse(response); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode == HttpStatus.SC_OK) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; }
通过 KiiCloud SDK 列出商品
private void loadObjects() { KiiQuery query = new KiiQuery(null); query.sortByDesc(Product.LAST_MODIFY_TIME); KiiBucket bucket = Kii.bucket(Product.BUCKET_NAME); bucket.query(new KiiQueryCallBack<KiiObject>() { public void onQueryCompleted(int token, KiiQueryResult<KiiObject> result, Exception e) { if (e == null) { List<KiiObject> objLists = result.getResult(); for (KiiObject obj : objLists) { mListAdapter.add(obj); } } else { Log.v(Utils.TAG, getString(R.string.error_load) + e.getLocalizedMessage()); } } }, query); }
购买商品
private void doBuy(String productId, String token) { KiiQuery query = new KiiQuery(KiiClause.equals(Receipt.PRODUCT_ID, productId)); query.sortByDesc(Receipt.LAST_MODIFY_TIME); KiiBucket bucket = KiiUser.getCurrentUser().bucket(Receipt.BUCKET_NAME); try { //if already bought, return and handle it in somewhere else KiiQueryResult<KiiObject> result = bucket.query(query); List<KiiObject> receiptList = result.getResult(); if (receiptList.size() > 0) { handler.sendEmptyMessage(1); return; } } catch (IOException e) { e.printStackTrace(); } catch (AppException e) { e.printStackTrace(); } HttpPost request = new HttpPost(String.format(Utils.SERVER_EXTENSION_BUY_URL, Utils.APP_ID)); Utils.addCommonHeader(request, null); request.addHeader(Utils.CONTENT_TYPE, Utils.CONTENT_JSON); try { request.setEntity(new StringEntity(String .format(Utils.RECEIPT_FORMAT, token, name, "0001", "0001", price, price_locale, productId))); HttpResponse response = new DefaultHttpClient().execute(request); if (response.getStatusLine().getStatusCode() == 200) { ret = getString(R.string.hint_purchase_succeed); } else { ret = getString(R.string.hint_purchase_failed); } } catch (Exception e) { ret = getString(R.string.hint_register_error); e.printStackTrace(); } finally { handler.sendEmptyMessage(0); } }
获取收据
private void loadReceipts() { KiiQuery query = new KiiQuery(null); query.sortByDesc(Receipt.LAST_MODIFY_TIME); KiiBucket bucket = KiiUser.getCurrentUser().bucket(Receipt.BUCKET_NAME); bucket.query(new KiiQueryCallBack<KiiObject>() { public void onQueryCompleted(int token, KiiQueryResult<KiiObject> result, Exception e) { if (e == null) { final List<KiiObject> objLists = result.getResult(); new Thread() { @Override public void run() { LoadMyBooks(objLists); } }.start(); } else { Log.v(Utils.TAG, getString(R.string.error_load) + e.getLocalizedMessage()); } } }, query); }
如下是客户端Demo的一些截屏:
示例代码 & 使用指导
- 示例代码下载:sample
- 在KiiCloud中创建一个你自己的应用,并把app id、app key、client id以及client secrect替换成你自己应用中的信息。
- 使用Kii Cloud 提供的工具部署服务端扩展到远程服务器,你可以按照开发者平台上指导的步骤完整这一步: http://documentation.kii.com/cn/guides/serverextension/executing_servercode/
- 所有含有 main() 函数的 Java 类都可以使用命令行工具或Eclipse来运行。
- 将客户端代码导入你的Eclipse,然后在任意的Android设备或模拟器上运行之。