iOS内购买IAP和服务器交互问题

早期同时兼顾 iOS和PHP任务功能记录

warning 此处不讲解IA如何P配置了,网上挺全面的而且还写的都不错的。但是前提是你有沙盒测试账号和itunesconnect设置过了内购买所需虚拟物品和银行协议的一堆配置
此处只讲解两点

  • 我碰见的问题
  • 和服务器的交互问题(此处以PHP为例子,本人PHP刚入门也是copy来的,根据自己的需求做出小小改变)

问题

配置消耗型物品重复购买却显示此项目免费恢复

  • 需要在

    1
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
  • 的每个状态后面都需要添加

    1
    [[SKPaymentQueue defaultQueue] finishTransaction:tran];

    交互

    先解释一下OC代码和PHP代码

    第一步拾掇拾掇需要的东西

  • #import <StoreKit/StoreKit.h>
  • <SKPaymentTransactionObserver, SKProductsRequestDelegate> 两个协议方法你也得实现吧

    第二步注册观察者,并且判断该用户能否使用内购买

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    if([SKPaymentQueue canMakePayments]){
    weakSelf.currentProId = productID;
    [weakSelf requestProductData:productID];
    }else{
    UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示"
    message:@"您的手机没有打开程序内付费购买"
    delegate:self cancelButtonTitle:NSLocalizedString(@"关闭",nil) otherButtonTitles:NSLocalizedString(@"提示",nil), nil];
    [alerView show];
    }

    第三步查询 => 你传一个你在itunesconnect 中App中定义的内购买Product_ID
    我是图

    1
    2
    3
    4
    5
    6
    NSArray *product = [[NSArray alloc] initWithObjects:type, nil];
    NSSet *nsset = [NSSet setWithArray:product];
    // 请求动作
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];

    第四步 收到你在itunesconnect 中定义的Product_ID的详细数据,顺便把购买请求发送了()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSLog(@"--------------收到产品反馈消息---------------------");
    NSArray *product = response.products;
    if([product count] == 0){
    //[SVProgressHUD dismiss];
    NSLog(@"--------------没有商品------------------");
    return;
    }

    NSLog(@"productID:%@", response.invalidProductIdentifiers);
    NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);

    SKProduct *p = nil;
    for (SKProduct *pro in product) {
    NSLog(@"%@", [pro description]);
    NSLog(@"%@", [pro localizedTitle]);
    NSLog(@"%@", [pro localizedDescription]);
    NSLog(@"%@", [pro price]);
    NSLog(@"%@", [pro productIdentifier]);

    if([pro.productIdentifier isEqualToString:_currentProId]){
    p = pro;
    }
    }

    SKPayment *payment = [SKPayment paymentWithProduct:p];
    NSLog(@"发送购买请求");
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    }

    第五步收到信息给你返回数据了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    for(SKPaymentTransaction *tran in transactions){

    switch (tran.transactionState) {
    case SKPaymentTransactionStatePurchased:{
    NSLog(@"交易完成");
    //方法在下面
    [self completeTransaction:tran];
    }
    break;
    case SKPaymentTransactionStatePurchasing:
    NSLog(@"商品添加进列表");

    break;
    case SKPaymentTransactionStateRestored:{
    NSLog(@"已经购买过商品");

    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
    }
    break;
    case SKPaymentTransactionStateFailed:{
    NSLog(@"交易失败");
    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
    // [SVProgressHUD showErrorWithStatus:@"购买失败"];
    startConntentService = false;
    }
    break;
    default:
    break;
    }
    }
    }

    此处是最后的方法还有我和服务器交互的地方,但是此处我也不懂为什么这个方法会调用两次如果您明白,请在下面留言告诉我谢谢,所以我此处加了一个全局的判断,来约束它和服务器交互的次数。
    !!!看好我发送数据请求的方法其实就是AFNetworing的封装,都会用对吧。。。网址看好,别光顾着复制哦

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    - (void)completeTransaction:(SKPaymentTransaction *)transaction {
    NSLog(@"交易结束");
    [SVProgressHUD dismiss];

    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    if (!receipt) { /* No local receipt -- handle the error. */ }
    //因为此处可能会多次调用原因不明所以加判断只调用一次
    else if (receipt && startConntentService) {
    /**
    服务器要做的事情:
    接收ios端发过来的购买凭证。
    判断凭证是否已经存在或验证过,然后存储该凭证。
    将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
    如果需要,修改用户相应的会员权限
    */
    startConntentService = false;
    //字典中第二个参数是为了debug准备的,正常你不用写
    NSDictionary *requestContents = @{
    @"receipt-data": [receipt base64EncodedStringWithOptions:0],
    @"XDEBUG_SESSION_START":@"12477"
    };
    [HTTPClient postWithURLString:@"你的后台网址" parameters:requestContents success:^(id returnValue) {
    id name = returnValue;

    } failure:^(id failureValue) {

    }];
    }
    //这个千万别忘了 ,要不你就会犯第一条问题(配置消耗型物品重复购买却显示此项目免费恢复)
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    下面是PHP代码,我用的是ThinkPHP框架(会php都会框架,像我只会框架暂时还不会php.咳咳…)

    只有两个方法
    1 外部调用的方法,此处我的逻辑都系在了Controller中,是为了大家方便,我建议还是写在Model中(MVC哦,其实感觉都一样哈哈)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public function iosIAPPay() {

    $status = array('status'=>-1);
    //获取 App 发送过来的数据,设置时候是沙盒状态
    $receipt = $_POST['receipt-data'];
    $isSandbox = true;
    //开始执行验证
    try
    {
    $info = $this->getReceiptData($receipt, $isSandbox);
    // 通过product_id 来判断是下载哪个资源
    switch($info['product_id']){
    case '你的Product_ID':
    $status['status'] = 1;
    Header("Location:xxxx.zip");
    break;
    }
    return $status;
    }
    //捕获异常
    catch(\Exception $e)
    {
    $this->ajaxReturn($status);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    //服务器二次验证代码
    function getReceiptData($receipt, $isSandbox = false)
    {
    if ($isSandbox) {
    $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
    }
    else {
    $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
    }

    $postData = json_encode(array("receipt-data" => $receipt));;

    $ch = curl_init($endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); //这两行一定要加,不加会报SSL 错误
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);


    $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);
    //此处是看到先人们的指导,又看到apple的官方说法改的。否则会审核不过貌似是审核也会走沙盒测试者,
    //此处先判断一次返回的status是否=21007 这数据是从测试环境,但它发送到生产环境中进行验证。它发送到测试环境来代替。
    if ($data->status == 21007) {
    $this->getReceiptData($receipt,true);
    return;
    }
    //判断返回的数据是否是对象
    if (!is_object($data)) {
    throw new \Exception('Invalid response data');
    }
    //判断购买时候成功
    if (!isset($data->status) || $data->status != 0) {
    throw new \Exception('Invalid receipt');
    }
    $in_app = $data->receipt->in_app;
    //返回产品的信息

    $status['data'] = array(
    'quantity' => $in_app->quantity,
    'product_id' => $in_app->product_id,
    'transaction_id' => $in_app->transaction_id,
    'purchase_date' => $in_app->purchase_date,
    'app_item_id' => $data->receipt->app_item_id,
    );
    return(
    $status;
    }

    我一直都困在和后台交互的时候苹果返回的数据中status=21002,哭死。。。因为status=0才是购买成功.

    • OC中将苹果所需的凭证其实就是
    1
    NSData *receipt               = [NSData dataWithContentsOfURL:receiptURL];

    base64EncodedStringWithOptions一次
    在PHP在将这个数据json_encode(array(“receipt-data” => $receipt));一次在传给苹果服务器,自己写百度谷歌一起来真心好难。
    一遍一遍的改OC和PHP代码,还好公司太小两个都是我在做,我有大把时间和不用麻烦别人。

    总结一下

    帮大家理理思路也是我目前知道的最好的app和服务器交互的方法:
    在支付之前在后台服务器记录一次你的数据,包括product_id,之后才开始去支付支付成功后再和服务器进行比对product_id可以预防app本地破解支付和防止用户篡改(比如他买的518却只支付6块)等.
    还有就是漏单问题,就是用户买东西成功了,但是和自己服务器交互出现了意外(不会怀孕),我现在并未尝试但觉得可行(在更新这个博客之前,因为今天周五要回家了所以下周测试),就是讲xcode的凭证保存到本地,这样用户就可以自己重新点击一次,在和服务器交互一次,将虚拟物品重新不给他一次。

参考:

应用内支付IAP全部流程
iOS开发内购全套图文教程


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!