苹果IAP V1

准备工作

https://www.freesion.com/article/34711455087/

IOS 内购支付两种模式

  • 内置模式
  • 服务器模式

内置模式的流程

  • app从app store 获取产品信息
  • 用户选择需要购买的产品
  • app发送支付请求到app store
  • app store 处理支付请求,并返回transaction信息
  • app将购买的内容展示给用户

服务器模式的流程1

  • app从服务器获取产品标识列表
  • app从app store 获取产品信息
  • 用户选择需要购买的产品
  • app 发送 支付请求到app store
  • app store 处理支付请求,返回transaction信息
  • app 将transaction receipt 发送到服务器
  • 服务器收到收据后发送到app stroe验证收据的有效性
  • app store 返回收据的验证结果
  • 根据app store 返回的结果决定用户是否购买成功
  • 购买成功,前端发送成功信息给apple server,关闭当前订单(如果没有关闭当前订单,后服务器收到收据就会越来越大)

服务器模式的流程2

  • app从服务器获取产品标识列表
  • app从app store 获取产品信息
  • 用户选择需要购买的产品
  • app 发送 支付请求到app store
  • app store 处理支付请求,返回transaction信息
  • apple 服务器 将transaction receipt 回调到服务器
  • 服务器收到收据后发送到app stroe验证收据的有效性
  • app store 返回收据的验证结果
  • 根据app store 返回的结果决定用户是否购买成功
  • 购买成功,前端发送成功信息给apple server,关闭当前订单(如果没有关闭当前订单,后服务器收到收据就会越来越大)

**上述两种模式的不同之处主要在于:**交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。内建模式简单快捷,但容易被破解。服务器模式流程相对复杂,但相对安全。

开发之初,苹果方就很负责的告知:我们的服务器不稳定。真正开发之后,发现苹果方果然是很负责的,不仅是不稳定,而且足够慢。app store server验证一个收据需要3-6s时间。

  • 用户能否忍受3-6s的等待时间
  • 如果app store server 宕机,如何确保成功付费的用户能够得到正常服务。

对于第一个问题,我们有理由相信用户完全无法忍受,所以采用异步验证的方式,服务器收到客户端的请求后,就将请求放到MCQ中去处理。

对于第二个问题,由于苹果人员很负责人的告知:我们的服务器不稳定,所以不排除收据验证超时的情况。对于验证超时的收据,保存到数据库中并标记为验证超时,定时任务每隔一定的时间去app store验证,确保能够获取收据的验证结果。

在开发过程中,需要测试应用是否能够正常的进行支付,但是又不能进行实际的支付,因此需要使用苹果提供的sandbox Store测试。Store Kit不能在iOS模拟器中使用,测试Store必须在真机上进行。

在sandbox中验证receipt:
https://sandbox.itunes.apple.com/verifyReceipt

在生产环境中验证receipt:
https://buy.itunes.apple.com/verifyReceipt

在实际开发过程中,服务器端通过issandbox字段标识客户端传递的收据是沙盒环境中的收据还是生产环境中的收据。在提交苹果审核前,沙盒测试均无问题。提交苹果审核后,被告知购买失败,审核未通过。通过查询日志发现,客户端发送的交易收据为沙盒收据,但是issandbox字段却标识为生产环境。

苹果审核app时,仍然在沙盒环境下测试。但是客户端同事在app提交苹果审核时,将issandbox字段写死,设置为生产环境。这样就导致沙盒收据发送到https://buy.itunes.apple.com/verifyReceipt去验证。

那么如何自动的识别收据是否是sandbox receipt呢?

识别沙盒环境下收据的方法有两种:

  • 根据收据字段 environment = sandbox。
  • 根据收据验证接口返回的状态码,如果status=21007,则表示当前的收据为沙盒环境下收据, 进行验证。

JAVA实现代码

调用
/**
     * 苹果服务器验证
     *
     * @param receipt
     *            账单
     * @url 要验证的地址
     * @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
     *
     */
    public static String buyAppVerify(String receipt,int type){
        //环境判断 线上/开发环境用不同的请求链接
        String url =  type == 0 ? url_sandbox : url_verify ;
        JSONObject params = new JSONObject();
        params.put("receipt-data", receipt);
        return HttpRequest.post(url)
                .body(params.toJSONString())
                .timeout(600000)
                .execute().body();
    }
处理结果
public R<String> iosPay(String transactionId, String payload, Long userId) {
        log.info("苹果内购校验开始,交易ID:{}", transactionId);
        //线上环境验证
        String verifyResult = IosVerifyUtil.buyAppVerify(payload, 1);
        //插入流水
        insertReceiptLog(userId, payload, JSON.toJSONString(verifyResult));
        if (verifyResult == null) {
            throw new ServiceException("苹果验证失败,返回数据为空!");
        } else {
            log.info("线上,苹果平台返回JSON:" + verifyResult);
            JSONObject appleReturn = JSON.parseObject(verifyResult);
            String states = appleReturn.getString("status");
            //无数据则沙箱环境验证
            if ("21007".equals(states)) {
                verifyResult = IosVerifyUtil.buyAppVerify(payload, 0);
                log.info("沙盒环境,苹果平台返回JSON:" + verifyResult);
                appleReturn = JSON.parseObject(verifyResult);
                states = appleReturn.getString("status");
            }
            log.info("苹果平台返回值:appleReturn" + appleReturn);

            // 前端所提供的收据是有效的 验证成功 获取transaction_id map
            if ("0".equals(states)) {
                String receipt = appleReturn.getString("receipt");
                JSONObject receiptJson = JSON.parseObject(receipt);

                String inApp = receiptJson.getString("in_app");
                List<HashMap> inApps = JSON.parseArray(inApp, HashMap.class);
                if (CollectionUtils.isEmpty(inApps)) {
                    return R.fail("未能获取获取到交易列表");
                }
                //获取到当前transactionId map
                HashMap transactionIdMap = inApps.stream().
                        filter(t -> transactionId.equals(t.get("transaction_id"))).findAny().orElse(null);
                //交易列表包含当前交易,则认为交易成功
                if (MapUtils.isEmpty(transactionIdMap)) {
                    return R.fail("当前交易不在交易列表中");
                }
                // 处理业务逻辑
                insertOrder(userId, transactionIdMap);
                return R.ok("充值成功");
            } else {
                return R.fail("支付失败,错误码:" + states);
            }
        }
    }
响应体
public R<String> iosPay(String transactionId, String payload, Long userId) {
        log.info("苹果内购校验开始,交易ID:{}", transactionId);
        //线上环境验证
        String verifyResult = IosVerifyUtil.buyAppVerify(payload, 1);
        //插入流水
        insertReceiptLog(userId, payload, JSON.toJSONString(verifyResult));
        if (verifyResult == null) {
            throw new ServiceException("苹果验证失败,返回数据为空!");
        } else {
            log.info("线上,苹果平台返回JSON:" + verifyResult);
            JSONObject appleReturn = JSON.parseObject(verifyResult);
            String states = appleReturn.getString("status");
            //无数据则沙箱环境验证
            if ("21007".equals(states)) {
                verifyResult = IosVerifyUtil.buyAppVerify(payload, 0);
                log.info("沙盒环境,苹果平台返回JSON:" + verifyResult);
                appleReturn = JSON.parseObject(verifyResult);
                states = appleReturn.getString("status");
            }
            log.info("苹果平台返回值:appleReturn" + appleReturn);

            // 前端所提供的收据是有效的 验证成功 获取transaction_id map
            if ("0".equals(states)) {
                String receipt = appleReturn.getString("receipt");
                JSONObject receiptJson = JSON.parseObject(receipt);
                JSONArray inApps = receiptJson.getJSONArray("in_app");
                if (CollectionUtils.isEmpty(inApps)) {
                    return R.fail("未能获取获取到交易列表");
                }
                JSONObject transactionIdMap = (JSONObject) inApps.stream().
                        filter(item -> "2000000060230910".equals(((JSONObject) item).getString("transaction_id")))
                        .findAny().orElse(null);
                //交易列表包含当前交易,则认为交易成功
                if (MapUtils.isEmpty(transactionIdMap)) {
                    return R.fail("当前交易不在交易列表中");
                }
                // 处理业务逻辑
                insertOrder(userId, transactionIdMap, receiptJson);
                return R.ok("充值成功");
            } else {
                return R.fail("支付失败,错误码:" + states);
            }
        }
    }

问题总结

  • 先生产验证后测试验证,可以避免来回切换接口的麻烦。测试验证只要用你自己申请的测试appid的时候才会用到,用户不会拥有测试appid,所以不会走到测试验证这一步。即使生产验证出错,应该也不回返回21007状态吗。测试验证通过的用户名,和充值金额最好用数据库记录下来,方便公司资金核对。
  • 对于服务器和前端返回的transaction_id可能是重复的,所以transaction_id的逻辑要设置为可重入
  • 如果开通了Apple通知回调,前端和apple服务端可能同时访问服务器,需要对transaction_id加锁
  • 开启自动订阅是必须要开启Apple 服务端回调的,只是非自动订阅可以不设置回调

苹果反馈的状态码

状态码

描述

21000

App Store无法读取你提供的JSON数据

21002

收据数据不符合格式

21003

收据无法被验证

21004

你提供的共享密钥和账户的共享密钥不一致

21005

收据服务器当前不可用

21006

收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中

21007

收据信息是测试用(sandbox),但却被发送到产品环境中验证

21008

收据信息是产品环境中使用,但却被发送到测试环境中验证

参考:

https://www.freesion.com/article/34711455087/