Kii BLOG

应用内支付(IAP)服务端扩展(Server Extension)Demo

为什么要做这个Demo? 因为市场有潜在的需求:

  1. 在Google商店和Apple应用商店中,他们提供了 IAP(应用内付费)功能。作为一个开发者,你可以上架你的IAP商品,但是你没法上架商品的内容。例如:如果你的IAP商品是一首歌,你没法上传这首歌到Apple的IAP服务器。用我们的话来说就是:他们支持对象(Object),但是不支持实体(Body)。
  2. 很多中国本地的应用商店是不支持IAP的,所以开发者们没有办法在应用商店中使用他们的IAP功能。

我们为第二种情况准备了这个Demo,当然如果做一些微小的变动也可以支持第一个种情况。

 

作为开发者,在编写代码前我们需要定义一些类:商品和收据。商品类包含一些诸如商品名、描述、新上架、推荐、价格、货币单位、商品ID、类型(消耗品、永久商品、时限商品)、缩略图和实体等字段。收据类包含诸如商品名、收据ID、交易号、价格、货币单位、购买日期和商品ID等字段。

流程大致是这样的:

  1. 开发者设置应用级数据仓库(Bucket)的ACL:对所有用户只读,所以需要删除所有用户对该Bucket的写权限。
  2. 开发者创建服务端扩展来接收购买请求并将收据写入用户的Bucket。
  3. 新增商品,通常这一步是在本地的浏览器中完成,但是我不是很了解JS程序,所以我写了一个Java的代码作为演示。
  4. 按需上传商品实体。因为一个商品只能包含唯一的文件实体,我把缩略图进行base64编码后作为键值对放入KiiObject中。
  5. 客户端可以通过 KiiCloud SDK列出应用级Bucket “product”中所有的商品。
  6. 客户端可以通过服务端扩展API购买商品。具体来说:如果使用支付宝(AliPay)进行支付,那么服务端扩展API需要设置支付宝通知的URL。
  7. 客户端从用户的收据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的一些截屏:

5

示例代码 & 使用指导

  1. 示例代码下载:sample
  2. 在KiiCloud中创建一个你自己的应用,并把app id、app key、client id以及client secrect替换成你自己应用中的信息。
  3. 使用Kii Cloud 提供的工具部署服务端扩展到远程服务器,你可以按照开发者平台上指导的步骤完整这一步: http://documentation.kii.com/cn/guides/serverextension/executing_servercode/
  4. 所有含有 main() 函数的 Java 类都可以使用命令行工具或Eclipse来运行。
  5. 将客户端代码导入你的Eclipse,然后在任意的Android设备或模拟器上运行之。

发表评论