版权声明:博客将逐步迁移到 http://cwqqq.com https://blog.csdn.net/cwqcwk1/article/details/71852679
文章已迁移到新博客 http://cwqqq.com/2017/12/ios_in-app_pay_server_side_code
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代码为例):
$verification_uri = 'https://buy.itunes.apple.com/verifyReceipt';
if ($is_sandbox) $verification_uri = 'https://sandbox.itunes.apple.com/verifyReceipt';
$post_data = array('receipt-data'=>$receipt_data);
$ch = curl_init($verification_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 拿来做匹配。
扫描二维码关注公众号,回复:
3168541 查看本文章
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 时,把请求地址换成沙盒测试地址,再次请求验单。
参考:http://blog.csdn.net/mycwq/article/details/71852679