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 \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');
}
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 \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);
}
}
}
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 \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);
}
/**
* 根據語言獲取當前地區時間
* 以本地服務器時間為准
* 比美國紐約快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 \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;
}
iOS內購:自動續期訂閱總結
https://www.jianshu.com/p/abd2ba4deb54