zoukankan      html  css  js  c++  java
  • iOS内购:自动续期订阅总结

    iOS内购(IAP)自动续订订阅类型服务端总结

    https://blog.csdn.net/qq_23564667/article/details/105512349

    iOS内购(IAP)自动续订订阅类型服务端总结
    IOS 后台需注意
    iOS 的 App 内购类型有四种:
    App 专用共享密钥
    订阅状态 URL
    内购流程
    流程简述
    服务端验证
    自动续费
    调用函数方法
    IOS 后台需注意
    iOS 的 App 内购类型有四种:
    消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买。
    示例:钓鱼 App 中的鱼食。
    非消耗型商品:只需购买一次,不会过期或随着使用而减少的产品。
    示例:游戏 App 的赛道。
    自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
    示例:每月订阅提供流媒体服务的 App。
    非续期订阅:允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
    示例:为期一年的已归档文章目录订阅。
    App 专用共享密钥
    需要创建一个 “App 专用共享密钥”,它是用于接收此 App 自动续订订阅收据的唯一代码。这个秘钥用来想苹果服务器进行校验票据 receipt,不仅需要传 receipt,还需要传这个秘钥。
    如果您需要将此 App 转让给其他开发人员,或者需要将主共享密钥设置为专用,可能需要使用 App 专用共享密钥。


    订阅状态 URL


    内购流程
    流程简述
    先来看一下iOS内购的通用流程


    用户向苹果服务器发起购买请求,收到购买完成的回调(购买完成后会把钱打给申请内购的银行卡内)
    购买成功流程结束后, 向服务器发起验证凭证(app端自己也可以不依靠服务器自行验证)
    自己的服务器工作分 4 步:

    1、接收 iOS 端发过来的购买凭证。
    2、判断凭证是否已经存在或验证过,然后存储该凭证。
    3、将该凭证发送到苹果的服务器(区分沙盒环境还是正式环境)验证,并将验证结果返回给客户端。
    sandbox 开发环境:https://sandbox.itunes.apple.com/verifyReceipt
    prod 生产环境:https://buy.itunes.apple.com/verifyReceipt
    4、修改用户相应的会员权限或发放虚拟物品。
    简单来说就是将该购买凭证用 Base64 编码,然后 POST 给苹果的验证服务器,苹果将验证结果以 JSON 形式返回。


    服务端验证
    ios客户端发送给服务端的数据

    /**
    * 订单同步
    */
    public function verify_order(){
    $eventSystem = new FreeiosEventSystemEvent();
    $request_uri = addslashes($_SERVER['REQUEST_URI']);
    $resp_str = file_get_contents( "php://input");
    $eventSystem->add_error('苹果端回调-input',$request_uri,$resp_str);
    $resp_str = stripslashes($resp_str);
    $resp_data = json_decode($resp_str,true);

    //苹果内购的验证收据,可以根据需要传递订单或者用户信息过来
    $receipt_data = $resp_data['apple_receipt'];
    $uid = $this->uid;
    if (!$uid){
    return_json_data(-99,'请先登录');
    }
    $eventSystem->add_error('苹果端回调-apple_receipt',$request_uri,$receipt_data);

    // 验证支付状态
    $result=$this->validate_apple_pay($receipt_data);
    if(!$result['status']){ // 凭据验证不通过
    $eventSystem->add_error('苹果端回调-result',$request_uri,'凭据验证不通过');
    return_json_data(0,'Credential verification failed');
    }

    $notify = $result['data'];
    $transId = $notify['transaction_id']; // 交易的标识
    $originalTransId = $notify['original_transaction_id']; // 原始交易ID
    $transTime = $this->toTimeZone($notify['purchase_date']); // 购买时间
    $transResult = $this->check_apple_trans($notify,$transId,$originalTransId,$transTime,$receipt_data);
    if($transResult['status']<=0){
    $eventSystem->add_error('苹果端回调-result',$request_uri,'交易号已经出现过了');
    return_json_data(0,'交易号已经出现过了');
    }
    // 处理订单数据
    $buyerInfo = $result['sandbox']; // 1 沙盒数据 0 正式
    $productId = $notify['product_id']; // 订单类型
    $is_trial_period = $notify['is_trial_period'] == 'false' ? 0 : 1; //是否首次购买
    $purchaseDate = str_replace(' America/Los_Angeles','',$notify['purchase_date_pst']);

    $pay_detail = $this->pay_detail[$reward]; // 购买畅读卡
    $products = array_column($pay_detail,null,'expend_identifier');
    $products = $products[$productId];
    $total_fee = $products['pay']*100; // 分
    $type = 3; // 苹果内购支付
    if($buyerInfo == 1){
    $type = 6;//沙盒模式
    }

    // 写入订单(这个其实可以在IOS发起支付的时候请求服务端,先生成订单,并返回订单号)
    $orderId = 'ios_a'.$this->uid.date("mdHis").rand(2000,8000);
    if(!$orderId ){
    $eventSystem->add_error('苹果端回调-result',$request_uri,'订单处理出错');
    return_json_data(0,'写入订单失败');
    }

    // 处理订单
    $rs = 1;
    if(!$rs){
    $eventSystem->add_error('苹果端回调-result',$request_uri,'更新数据错误失败');
    return_json_data(0,'更新数据错误失败');
    }
    $eventSystem->add_error('苹果端回调-result',$request_uri,'订单处理成功');
    return_json_data(1,'ok');

    }
    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    自动续费

    /*
    * 自动续费订阅回调
    * password 秘钥: 43f37f26****adc66a1be
    * */
    public function renew(){
    $resp_str = file_get_contents( "php://input");
    if(empty($resp_str)){
    $inputArr = I('','trim','');
    $resp_str = '';
    foreach($inputArr as $key=>$value){
    $resp_str.=$key."=".$value."&";
    }
    }
    $eventSystem = new FreeiosEventSystemEvent();
    $eventSystem->add_error('renew','AppleAutoPay',$resp_str);

    $data = json_decode($resp_str,true);
    if(!empty($resp_str)) {//有时候苹果那边会传空数据调用
    // notification_type 几种状态
    // NOTIFICATION_TYPE 描述
    // INITIAL_BUY 初次购买订阅。latest_receipt通过在App Store中验证,可以随时将您的服务器存储在服务器上以验证用户的订阅状态。
    // CANCEL Apple客户支持取消了订阅。检查Cancellation Date以了解订阅取消的日期和时间。
    // RENEWAL 已过期订阅的自动续订成功。检查Subscription Expiration Date以确定下一个续订日期和时间。
    // INTERACTIVE_RENEWAL 客户通过使用应用程序界面或在App Store中的App Store中以交互方式续订订阅。服务立即可用。
    // DID_CHANGE_RENEWAL_PREF 客户更改了在下次续订时生效的计划。当前的有效计划不受影响。
    $notification_type = $data['notification_type'];//通知类型
    $password = $data['password']; // 共享秘钥
    if ($password == "43f37f26****c66a1be") {
    $receipt = isset($data['latest_receipt_info']) ? $data['latest_receipt_info'] : $data['latest_expired_receipt_info']; //latest_expired_receipt_info 好像只有更改续订状态才有
    $product_id = $receipt['product_id']; // //商品的标识
    $original_transaction_id = $receipt['original_transaction_id']; // //原始交易ID
    $transaction_id = $receipt['transaction_id']; // //交易的标识
    $purchaseDate = str_replace(' America/Los_Angeles','',$receipt['purchase_date_pst']);
    //查询出该apple ID最后充值过的用户
    $userid = 0; // 去数据库查询是否充值过
    if ($notification_type == 'CANCEL') { //取消订阅,做个记录
    if ($userid > 0) {
    $eventSystem->add_error('renew','AppleAutoPay','用户订阅取消记录成功');
    }
    } else {
    //自动续订,给用户加时间
    //排除几种状态不用处理,1,表示订阅续订状态的更改 2,表示客户对其订阅计划进行了更改 3,在最初购买订阅时发生
    //if ($notification_type != "DID_CHANGE_RENEWAL_PREF" && $notification_type != "DID_CHANGE_RENEWAL_STATUS" && $notification_type != "INITIAL_BUY") {
    if ($notification_type == "INTERACTIVE_RENEWAL" || $notification_type == "RENEWAL") {
    $transTime = $this->toTimeZone($receipt['purchase_date']);
    //查询数据库,该订单是否已经处理过了
    $appleTransCnt = 1; // 去数据库查看该订单是否处理过
    if ($appleTransCnt == 0) { //没有使用过,继续走
    $order_type = $this->products[$product_id];
    $order_money = $this->product_money[$order_type];

    $eventSystem->add_error('renew','AppleAutoPay','续订成功');
    } else {
    $eventSystem->add_error('renew','AppleAutoPay','此次支付订单已处理过');
    }
    } else {
    $eventSystem->add_error('renew','AppleAutoPay','该类型通知不予处理--notification_type:' . $notification_type);
    }
    }
    } else {
    $eventSystem->add_error('renew','AppleAutoPay','该通知传递的密码不正确--password:' . $password);
    }
    }
    }

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    调用函数方法

    /**
    * 验证这个交易号是否存在过了
    * @param $notify
    * @param $transId
    * @param $totalAmount
    * @param $tradeId
    * @param $receipt_data
    */
    public function check_apple_trans($notify,$transId,$originalTransId,$transTime,$receipt_data){
    $eventOrder = new FreeiosEventOrderEvent();
    $where = ['trade_no'=>$transId, ];
    $appleTransCnt = $eventOrder->get_order_count($where);
    if($appleTransCnt>0){
    return ['status'=>-1,'appleTransCnt'=>$appleTransCnt];
    }else{
    $eventOrder->add_order_log_apple([
    'trans_id'=>$transId,
    'original_trans_id'=>$originalTransId,
    'content'=>json_encode(['appleTransCnt'=>$appleTransCnt,'notify'=>$notify,'receipt_data'=>$receipt_data]),
    ]);
    return ['status'=>1];
    }
    }

    private function toTimeZone($src, $from_tz = 'Etc/GMT', $to_tz = 'Asia/Shanghai', $fm = 'Y-m-d H:i:s') {
    $datetime = new DateTime($src, new DateTimeZone($from_tz));
    $datetime->setTimezone(new DateTimeZone($to_tz));
    return $datetime->format($fm);
    }

    /**
    * 根据语言获取当前地区时间
    * 以本地服务器时间为准
    * 比美国纽约快12小时
    * 比泰国,印尼快1小时
    * 比葡萄牙里本斯快7小时
    * @param $language
    */
    private function format_time_zone($language,$is_format=true){
    if($language == 1){
    $f_time = strtotime('-12 hours');
    }else if($language == 2 || $language == 3){
    $f_time = strtotime('-1 hours');
    }else{//葡萄牙语
    $f_time = strtotime('-7 hours');
    }
    if($is_format){
    $f_time = date('Y-m-d H:i:s',$f_time);
    }
    return $f_time;
    }

    private function format_to_time_zone($time_zone){
    date_default_timezone_set($time_zone);//设置时区
    $f_time = date('Y-m-d H:i:s');
    date_default_timezone_set('Asia/Shanghai');//设置回默认的
    return $f_time;
    }

    /**
    * 21000 App Store不能读取你提供的JSON对象
    * 21002 receipt-data域的数据有问题
    * 21003 receipt无法通过验证
    * 21004 提供的shared secret不匹配你账号中的shared secret
    * 21005 receipt服务器当前不可用
    * 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
    * 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
    * 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
    */
    private function acurl($receipt_data, $sandbox=0){
    //小票信息
    $POSTFIELDS = array("receipt-data" => $receipt_data,"password"=>"43f37f26****c66a1be");
    $POSTFIELDS = json_encode($POSTFIELDS);

    //正式购买地址 沙盒购买地址
    $url_buy = "https://buy.itunes.apple.com/verifyReceipt";
    $url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
    $url = $sandbox ? $url_sandbox : $url_buy;

    //简单的curl
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $POSTFIELDS);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); //这两行一定要加,不加会报SSL 错误
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
    }

    /**
    * 验证AppStore内付
    * @param string $receipt_data 付款后凭证
    * @return array 验证是否成功
    */
    private function validate_apple_pay($receipt_data){
    // 验证参数
    if (strlen($receipt_data)<20){
    $result=array(
    'status'=>false,
    'message'=>' Illegal param'
    );
    return $result;
    }
    // 请求验证
    $html = $this->acurl($receipt_data);
    $data = json_decode($html,true);

    $data['sandbox'] = '0';
    // 如果是沙盒数据 则验证沙盒模式
    if($data['status']=='21007'){
    $html = $this->acurl($receipt_data, 1);
    $data = json_decode($html,true);
    $data['sandbox'] = '1';
    }

    $eventSystem = new FreeiosEventSystemEvent();
    $eventSystem->add_error('苹果验证','validate_apple_pay',json_encode($data));

    // 判断是否购买成功
    if(intval($data['status'])===0){ // 成功
    $receipts = $data['latest_receipt_info']; // 自动续订的订阅项 时才会有
    if(!isset($data['latest_receipt_info'])){
    $receipts = $data['receipt']['in_app']; // 消费类型
    }
    if(count($receipts)>0){
    $maxDate = '0'; //最新的日期,时间戳
    $appData = null; //最新的那组数组
    foreach($receipts as $k=>$app){
    if($maxDate<$app['purchase_date_ms']){
    $appData = $app;
    $maxDate = $app['purchase_date_ms'];
    }
    }
    $result=array(
    'status'=>true,
    'message'=>'Purchase success',
    'data'=>$appData,
    'sandbox'=>$data['sandbox'],
    );
    }else{
    $result=array(
    'status'=>false,
    'message'=>'No data status:'.$data['status']
    );
    }
    }else{ // 失败
    $result=array(
    'status'=>false,
    'message'=>'Failed purchase status:'.$data['status']
    );
    }
    return $result;
    }


    iOS内购:自动续期订阅总结

    https://www.jianshu.com/p/abd2ba4deb54

  • 相关阅读:
    [leetcode] Valid Sudoku
    [leetcode] Count and Say
    [leetcode] Decode Ways
    [leetcode] Sqrt(x)
    [leetcode] Best Time to Buy and Sell Stock II
    7-27 兔子繁衍问题
    7-26 最大公约数和最小公倍数
    7-25 求奇数和
    7-24 猜数字游戏
    7-23 分段计算居民水费
  • 原文地址:https://www.cnblogs.com/itlover2013/p/15041519.html
Copyright © 2011-2022 走看看