iOS内购充值 服务器端处理

iOS内购充值,是通过客户端接入iOS的IAP模块(In-App Purchase)后,由客户端发起充值,然后再把充值数据(receipt)发给服务端,最后由服务端远程调用AppStore服务器验证。最近研究了下iOS充值,着实遇到不少麻烦,就利用点时间总结下自己的经验,给大家做个分享。

服务端连接AppStore验单

验单的过程是,服务端发起HTTP Post请求,将以下两个字段的数据以json格式请求 AppStore 服务器,解析返回数据来验证。
receipt-data The base64 encoded receipt data.
password Only used for receipts that contain auto-renewable subscriptions.
Your app’s shared secret (a hexadecimal string).
AppStore 服务器有两个,对应测试环境(沙盒测试)和正式环境:
测试环境: https://sandbox.itunes.apple.com/verifyReceipt
正式环境: https://buy.itunes.apple.com/verifyReceipt
充值验证的请求如下(以PHP代码为例):
$uri = 'https://buy.itunes.apple.com/verifyReceipt';
if ($is_sandbox) $uri = 'https://sandbox.itunes.apple.com/verifyReceipt';
$post_data = array('receipt-data' => $receipt_data);
$ch = curl_init($uri);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));

$response = curl_exec($ch);
$errno = curl_errno($ch);
$errmsg = curl_error($ch);
curl_close($ch);

if ($errno != 0) {
throw new Exception($errmsg, $errno);
}

$data = json_decode($response, 1);

服务端验证返回数据

iOS发起票据验证请求后,通过处理AppStore返回数据来验单。下面举两个示例,同时说明不同iOS版本的返回数据不同,服务端要做好区别。
1、iOS7及以上获取的票据返回数据:
{
    receipt =  {
        "adam_id" = 0,
        "app_item_id" = 0,
        "application_version" = 1,
        "bundle_id" = "com.test",
        "download_id" = 0,
        "in_app" = {
            {
                "is_trial_period" = false,
                "original_purchase_date" = "2017-01-01 01:01:01 Etc/GMT",
                "original_purchase_date_ms" = 1483203661000,
                "original_purchase_date_pst" = "2017-01-01 01:01:01 America/Los_Angeles",
                "original_transaction_id" = 1000000000000001,
                "product_id" = "com.test.10",
                "purchase_date" = "2017-01-01 01:01:01 Etc/GMT",
                "purchase_date_ms" = 1483203661000,
                "purchase_date_pst" = "2017-01-01 01:01:01 America/Los_Angeles",
                "transaction_id" = 1000000000000001
            },
            //......
        },
        "receipt_type" = "ProductionSandbox",
        "request_date" = "2017-01-01 01:01:01 Etc/GMT",
        "request_date_ms" = 1483203661000,
        "request_date_pst" = "2017-01-01 01:01:01 America/Los_Angeles",
        "version_external_identifier" = 0,
    },
    status = 0
}
 2、iOS7以下获取的票据返回数据(不包括iOS7):
{
    receipt = {
        "bid" = "com.test",
        "bvrs" = 1,
        "item_id" = 573837050,
        "original_purchase_date" = "2017-01-01 01:01:01 Etc/GMT",
        "original_purchase_date_ms" = 1483203661000,
        "original_purchase_date_pst" = "2017-01-01 01:01:01 America/Los_Angeles",
        "original_transaction_id" = 1000000000000001,
        "product_id" = "com.test.10",
        "purchase_date" = "2017-01-01 01:01:01 Etc/GMT",
        "purchase_date_ms" = 1483203661000,
        "purchase_date_pst" = "2017-01-01 01:01:01 America/Los_Angeles",
        "transaction_id" = 1000000000000001
    },
    status = 0
}
验证订单是否成功,关键看这几个数据
1、status为 0 表示成功;其他都为失败,表示失败原因
2、根据 receipt.in_app 字段判断iOS版本,验证方法也不同
iOS7及以上:有in_app字段,验证 receipt.bundle_id 是否为你 App 的 bundle id,根据 in_app 处理充值的每一笔订单, 根据 product_id 判断用户充值了哪个档位,同时取出 transaction_id
iOS7以下:没有in_app字段,验证 receipt.bid 是否为你 App 的 bundle id,根据 product_id 判断用户充值了哪个档位,同时取出 transaction_id
3、根据 transaction_id 对比数据库历史订单判断是否已处理过,没有则认为本次充值是有效的。

iOS充值验单防坑指南

iOS充值坑点: in_app 究竟是什么

receipt.in_app 是请求AppStore验单后返回的数据,前面有提及,为用户的充值订单数据。
有两个问题要注意:
1、iOS内购充值时,客户端充值后从iOS得到的票据 receipt_data 不是针对本次充值的,而是相当于给一个授权 token, 获取用户 appleid 账号在本 App 中所有未关闭的充值记录,包括刚刚发起的充值。
2、根据这个票据查到的充值数据(receipt.in_app) ,除了最近发起的充值,还包括了非消耗品型,订阅型的充值数据。其中,最近发起的充值,不只是刚刚发起的充值,还可能是最近的几笔充值。特别是沙盒测试,还可能拿到已经确认关闭了的充值订单
所以,取到充值数据,不是取 receipt.in_app 中的第一个数据、或最后一个,而是在客户端完成充值后,将AppStore回调给到的 transaction_id 拿来做匹配。

 

iOS充值坑点: applicationUsername 为空

SKPayment.applicationUsername 字段是客户端发起充值时传给AppStore,在AppStore回传数据时会携带的参数。这是iOS7引进的参数,但就算是iOS版本没问题,iOS实际上没法保证 applicationUsername 传递准确。所以,当你用这个参数传递数据,可能会收到空数据,目前的情况是要么正确,要么为空。说白了,这是apple的bug,早在一两年前,就有人遇到这个问题,但直到现在,apple还是没有解决。(帖子链接猛击这里
这个字段的作用是,可以让App区分是哪个账号充值的。用户在App下注册了两个账号,他登录了账号A充值,在完成充值时他换了账号B登录。可能结果就是,他本来给账号A充值,变成了给账号B充值。
那么,这个字段失效后,该怎么处理?
用户发起订单时,记录他充值的 product_id,等完成订单时,看他当前账号是否有这个 product_id(有必要的话再对比 purchase_date_ms),对得上的话就给他充值,没有就延迟处理。当然,还有极端的情况,他两个账号都同时发起了同样的一笔订单,但实际上这是无法出现的,一个 appleid不能同时发起同样的一笔订单,除非是apple 出bug了,真有这个时候,认了吧 ╮(╯▽╰)╭

 

iOS充值坑点: Deferred 状态是什么

SKPaymentTransactionStateDeferred 即等待确认,主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。

对各种交易状态的处理如下(这是客户端的逻辑,既然写到这里,也科普下 ^_^):

for (SKPaymentTransaction *transaction in transactions) {
   switch (transaction.transactionState) {
       case SKPaymentTransactionStatePurchasing:
            NSLog(@"正在支付");
            break;
       case SKPaymentTransactionStateDeferred:
            NSLog(@"延迟处理");
            break;
       case SKPaymentTransactionStateFailed:
            NSLog(@"交易失败");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
       case SKPaymentTransactionStatePurchased:
            NSLog(@"交易完成");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
       case SKPaymentTransactionStateRestored:
            NSLog(@"购买过了");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
       default:
            NSLog(@"其他状态 %@", @(transaction.transactionState));
            break;
   }
}

主动完成交易的目的是,如果没有主动完成交易,下次启动App时(添加Observer),AppStore会再次通知你交易信息,直到你完成交易。当然,如果没有完成交易,是不能再发起同样的一笔充值订单。

iOS充值坑点:App审核不通过

苹果审核App时,是在沙盒环境下测试。所以,当App提交苹果审核时,服务端需换成沙盒环境,否则就无法通过苹果审核。通常游戏开发商都会搞一个审核服来给苹果审核,这样,审核服用沙盒环境,正式服用正式环境。
但对于很多App应用开发商来说,专门搞一个服务器显然增加了不少成本。其实还是有办法处理的,方法如下:
根据验单返回的 status 字段:

当 status = 21007 时,把请求地址换成沙盒测试地址,再次请求验单。

2 thoughts on “iOS内购充值 服务器端处理”

发表评论

电子邮件地址不会被公开。 必填项已用*标注