概述:iOS内购IAP自动续订订阅类型服务端总结
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 \Freeios\Event\SystemEvent(); $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'); } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
自动续费
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 \Freeios\Event\SystemEvent(); $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); } } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
调用函数方法
public function check_apple_trans($notify,$transId,$originalTransId,$transTime,$receipt_data){ $eventOrder = new \Freeios\Event\OrderEvent(); $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); } 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; } 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; } 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 \Freeios\Event\SystemEvent(); $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; } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156