介绍参考链接

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 形式返回。

状态码 - 详情

0 校验成功

21000 未使用HTTP POST请求方法向App Store发送请求。

21001 此状态代码不再由App Store发送。

21002 receipt-data属性中的数据格式错误或丢失。

21003 收据无法认证。

21004 您提供的共享密码与您帐户的文件共享密码不匹配。

21005 收据服务器当前不可用。

21006 该收据有效,但订阅已过期。当此状态代码返回到您的服务器时,收据数据也会被解码并作为响应的一部分返回。仅针对自动续订的iOS 6样式的交易收据返回。

21007 该收据来自测试环境,但已发送到生产环境以进行验证。

21008 该收据来自生产环境,但是已发送到测试环境以进行验证。

21009 内部数据访问错误。稍后再试。

21010 找不到或删除了该用户帐户。

5.关于续订

针对自动续期订阅类型,App Store会在订阅时间快到期之前,自动扣费帮助用户续订该服务。

server to server的校验方式,也是苹果推荐的校验方式 ,由苹果主动告知我们状态。 服务器需要接收苹果服务器发送过来的回调消息,根据消息类型进行续订,取消订阅,退订等操作。

6.配置接收通知地址 ----就是处理苹不同的回调事件

需要在App Store connect后台配置订阅状态URL ,用于接收 App Store 服务器回调通知的网址

官方文档: https://help.apple.com/app-store-connect/#/dev0067a330b

那我们首先来实现客户端+服务端支付功能

客户端实现自行实现

服务端需要编写

  1. 创建订单接口-给客户端订单号

  1. 客户端支付成功后请求通知接口用于发放奖励

  1. 恢复内购接口 -玩家可以恢复购买

  1. 苹果平台配置通知回调接口--处理回调事件

比如订阅、续费、升级 比如我们是周期扣款 其中订阅就是客户端付款的那一笔

续费以及升级需要我们在创建订单来发送奖励、退款收回奖励

关于用户如何与通知事件记录绑定

  1. 客户端发起付款我们会生成订单其中有用户ID以及初始化订单信息

  1. 客户端扣款成功我们解密回传数据可以得到

originalTransactionId -- 苹果用户与我们生成的唯一ID 以后都不会变

  1. 苹果通知事件中会存在originalTransactionId 我们与订单中的用户ID建立绑定就好

如果产品存在多用户请自行处理相关逻辑

  1. 如何保证回传事件是否同一笔订单

transactionId、webOrderLineItemId 每次交易都会生成唯一订单ID 这里使用的是originalTransactionId +webOrderLineItemId 来确定事件订单

具体代码实现

  1. 路由

  Route::any('ios/notify', 'IosController@notify');//IOS支付回调Route::any('renew/notify', 'IosController@iosNotify');//IOS事件通知Route::post('ios/payment', 'IosController@payment');//IOS支付Route::post('ios/repay', 'IosController@repay');//IOS恢复内购
  1. 控制器

<?phpnamespace App\Http\Controllers;use Illuminate\Support\Facades\Log;
use App\Libs\DingTalk;
use App\Models\User;
use App\Models\Order;
use App\Models\VipSetmeal;
use App\Models\Transaction;
use App\Models\KuaishouAds;
use App\Models\TranslationNotify;class IosController extends Controller
{protected $apple_url = '';public function __construct(){
//        $this->middleware(['api']);}/*** 苹果校验接口** @param $san_box* @return string*/private function _getAppleUrl($san_box){return $san_box ? 'https://sandbox.itunes.apple.com/verifyReceipt' : 'https://buy.itunes.apple.com/verifyReceipt';}/*** 请求苹果接口** @param $order_id* @param $url* @param $params* @return bool|string*/private function _requestAppleUrl($order_id, $url, $params){try {$result = $this->http_post($url, json_encode($params));} catch (\Throwable $e) {$result = $e->getMessage();}try {// 请求苹果记录入库db('orders_apple_request_log')->insert(['order_id' => $order_id,'params'   => json_encode($params, JSON_UNESCAPED_UNICODE),'response' => $result,]);} catch (\Throwable $e) {Log::channel('orders')->error('苹果请求error:' . $e->getMessage());}return $result;}/*** 订单异常钉钉推送** @param $order_info* @param $at_phones*/private function _errorSendDingMsg($order_info, $at_phones = ''){$channel_desc = [1 => '支付宝',2 => '微信',3 => 'apple',];$error_msg = "【支付-回调异常】\n\n【平台】:{$channel_desc[$order_info['channel']]}\n【订单】:{$order_info['order_id']}\n【原因】:{$order_info['msg']}\n【链接】:www.test.com'DingTalk::ding(1, $error_msg, 'text', $at_phones);}/*** IOS支付* @return \Illuminate\Http\JsonResponse*/public function payment(){//timbao add  支付埋点Log::info('PPPPP:有人准备支付啦!');$user = getApiUser();//timbao add //在支付api处埋点,看看用户是否有支付不成功的情况(目前有用户反馈此问题)Log::info('PPPPP:userid' . $user['id'] . '发起支付');$setmeal_id = request("setmeal_id");// 套餐id$setmeal    = VipSetmeal::query()->where('setmealid', $setmeal_id)->first();if (!$setmeal) {//timbao add  支付埋点Log::info('支付失败,套餐id错误');return json(4001, '参数错误,套餐不存在');}
//        $order = Order::query()->where([
//            "user_id" => $user['id'],
//            "status" => 1
//        ])->where('created_at','>',now()->subDays(1)->toDateTimeString())->first();
//        if(!isset($order)){$price = $setmeal['money'];//套餐金额$num   = $setmeal['num'];//数量$title = $setmeal['title'];//标题$title = $user['name'] . "购买" . $title;switch ($setmeal['date_type']) {case 1:$date = 'week';break;case 2:$date = 'onemonth';break;case 3:$date = 'month';break;case 4:$date = 'year';break;case 5:$date = 'oneyear';break;case 6:$date = 'perpetual';break;default:$date = 'week';break;}$orderno = date("YmdHis") . rand(1111, 9999);$other['num']       = $num;$other['price']     = $price;$other['type']      = 'setmeal';$other['date']      = $date;$other['setmealid'] = $setmeal['setmealid'];// 保存订单信息$order = Order::create(["user_id"   => $user['id'],"title"     => $title,"ordernum"  => $orderno,"prepay_id" => null,"remark"    => request('remark'),"money"     => $price,"channel"   => 3,"status"    => 1,"other"     => $other,'setmealid' => $setmeal['setmealid']]);
//        }//timbao add 支付信息埋点Log::info('PPPPP:支付请求信息成功返回!userid=' . $user['id']);return json(1001, '支付信息请求成功', $order);}/*** 支付回调* @return \Illuminate\Http\JsonResponse*/public function notify(){$contentArr = request()->json()->all();// 参数验证if (empty($contentArr['order_id']) || empty($contentArr['apple_receipt'])) {return json(4001, '参数异常,请重试');}$order_id = $contentArr['order_id'];try {// 回调记录日志入库db('orders_notify_log')->insert(['order_id'      => $order_id,'channel'       => 3,'notify_params' => json_encode($contentArr, JSON_UNESCAPED_UNICODE)]);} catch (\Throwable $e) {Log::channel('orders')->error(json_encode($contentArr, JSON_UNESCAPED_UNICODE));Log::channel('orders')->error('回调入库异常:' . $e->getMessage());}$arr['password']     = '2160564429*******9749c06e7f9';$arr['receipt-data'] = $contentArr['apple_receipt'];//苹果内购的验证收据$order = Order::query()->where('ordernum', $order_id)->first();if (!$order || $order['status'] == 2) {//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,支付订单在数据库没有");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel' => 3, 'msg' => '支付订单在数据库没有!']);return json(4001, '订单状态异常');}$url    = $this->_getAppleUrl(config('pay.apple.san_box'));$result = $this->_requestAppleUrl($order_id, $url, $arr);$result = json_decode($result);if (!$result || !isset($result->status)) {//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,苹果校验支付信息失败 result为空");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel' => 3, 'msg' => '苹果校验支付信息失败 result为空']);return json(4001, '获取数据失败,请重试');}//如果校验失败if ($result->status != 0) {// 钉钉推送Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,苹果校验支付状态值异常 status={$result->status}");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel' => 3, 'msg' => '苹果校验支付状态值异常 status=' . $result->status]);// receipt是Sandbox receipt,但却发送至生产系统的验证服务 - 沙箱接口重试if ($result->status == 21007) {$url    = $this->_getAppleUrl(true);$result = $this->_requestAppleUrl($order_id, $url, $arr);$result = json_decode($result);if (!$result || !isset($result->status)) {//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,苹果校验二次重试支付信息失败 result为空");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel' => 3, 'msg' => '苹果校验二次重试支付信息失败 result为空']);return json(4001, '获取数据失败,请重试');}if ($result->status != 0) {//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,苹果校验二次支付状态值异常 status={$result->status}");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel' => 3, 'msg' => '苹果校验二次支付状态值异常 status=' . $result->status]);return json(4001, '校验失败');}} else {return json(4001, '校验失败');}}// 校验成功if ($result->status == 0) {Log::channel('orders')->info("apple:order_id:{$order_id}:msg:校验成功状态值正常");//在此处理业务逻辑$product = $result->pending_renewal_info;
//            $order->original_transaction_id = $product[0]->original_transaction_id;
//            $order->save(); // 保存订单//            $orders = db('orders')
//                ->where('original_transaction_id',$product[0]->original_transaction_id)
//                ->where('created_at','>',now()->subSeconds(5)->toDateTimeString())
//                ->where('setmealid',$order->other['setmealid'])
//                ->first();
//            if(isset($orders)){
//                return json(4001,'该订单已经买过了');
//            }$order->pay_time                = now()->toDateTimeString(); // 更新支付时间为当前时间$order->status                  = 2;$order->original_transaction_id = $product[0]->original_transaction_id;$order->save(); // 保存订单if ($product[0]->auto_renew_product_id != $order->other['setmealid']) {//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:支付失败,套餐id对不上 check:{$product[0]->auto_renew_product_id}-order:{$order->other['setmealid']}");$this->_errorSendDingMsg(['order_id' => $order_id, 'channel_id' => 3, 'msg' => "支付失败,套餐id对不上 check:{$product[0]->auto_renew_product_id}-order:{$order->other['setmealid']}"]);return json(4001, '验证失败!');}try {$kuaishou = new KuaishouAds();$kuaishou->reportEvent($order->user_id, 3, ['amount' => $order->money]);} catch (\Throwable $e) {//timbao modify//针对快手增加了userid log, 目前有error log : Call to a member function toArray() on nullLog::channel('orders')->info("kuaishou_apple:order_id:{$order_id}:msg:{$e->getMessage()}:user_id={$order->user_id}");}//更新用户到期时间,剩余次数User::saveVip($order);//timbao add 支付信息埋点Log::channel('orders')->info("apple:order_id:{$order_id}:msg:用户支付成功:user_id={$order->user_id}");//返回给客户端需要结束交易的transaction_id列表return json(1001, 'SUCCESS');}}/*** 验证AppStore内付* @param  string $receipt_data 付款后凭证* @return array                验证是否成功*/protected function validate_apple_pay($receipt_data, $ios_sandBox){/*** 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环境的验证服务*/$POSTFIELDS = '{"receipt-data":"' . $receipt_data . '"}';if ($ios_sandBox) {// 请求验证$data = $this->httpRequest('https://sandbox.itunes.apple.com/verifyReceipt', $POSTFIELDS);} else {// 请求验证$data = $this->httpRequest('https://buy.itunes.apple.com/verifyReceipt', $POSTFIELDS);}return $data;}/*** POST请求* @param $url* @param array $postData* @param bool $json* @return bool|mixed|string*/protected function httpRequest($url, $postData = array(), $json = true){$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);if ($postData) {curl_setopt($ch, CURLOPT_POST, 1);curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);}curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);$data = curl_exec($ch);curl_close($ch);if ($json) {return json_decode($data, true);} else {return $data;}}/*** POST请求* @param $url* @param $data_string* @return bool|string*/public function http_post($url, $data_string){$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-AjaxPro-Method:ShowList','Content-Type: application/json; charset=utf-8','Content-Length: ' . strlen($data_string)));curl_setopt($ch, CURLOPT_POST, 1);curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);$data = curl_exec($ch);curl_close($ch);return $data;}/*** 恢复内购* @return \Illuminate\Http\JsonResponse*/public function repay(){try {$contentArr          = request()->json()->all();$arr['password']     = '2160564429*******9749c06e7f9';$arr['receipt-data'] = $contentArr['apple_receipt'];$ios_sandBox         = env('IOS_SANDBOX', true);//判断生产环境,开发环境if ($ios_sandBox) {$url = 'https://sandbox.itunes.apple.com/verifyReceipt';} else {$url = 'https://buy.itunes.apple.com/verifyReceipt';}$result = $this->http_post($url, json_encode($arr));
//            Log::info('复购'.$result);$result = json_decode($result);if (!$result || !isset($result->status)) {return json(4001, '获取数据失败,请重试');}//如果校验失败if ($result->status != 0) {return json(4001, '校验失败');}if ($result->status == 0) {//在此处理业务逻辑$product = $result->pending_renewal_info;$orders  = db('orders')->where('original_transaction_id', $product[0]->original_transaction_id)->first();if (isset($orders)) {return json(4001, '该订单已经买过了');}$setmeal_id = $product[0]->auto_renew_product_id;$setmeal    = VipSetmeal::query()->where('setmealid', $setmeal_id)->first();if (!$setmeal) {return json(4001, '参数错误,套餐不存在');}$user  = getApiUser();$price = $setmeal['money'];//套餐金额$num   = $setmeal['num'];//数量$title = $setmeal['title'];//标题$title = $user['name'] . "购买" . $title;switch ($setmeal['date_type']) {case 1:$date = 'week';break;case 2:$date = 'onemonth';break;case 3:$date = 'month';break;case 4:$date = 'year';break;case 5:$date = 'oneyear';break;case 6:$date = 'perpetual';break;default:$date = 'week';break;}$orderno = date("YmdHis") . rand(1111, 9999);$key    = md5($setmeal_id . $user['id']);$orders = db('orders')->where(['onlykey' => $key])->first();if (isset($orders)) {return json(4001, '已经恢复购买过了');}$other['num']       = $num;$other['price']     = $price;$other['type']      = 'setmeal';$other['date']      = $date;$other['setmealid'] = $setmeal_id;// 保存订单信息$re    = Order::create(["user_id"                 => $user['id'],"title"                   => $title,"ordernum"                => $orderno,"prepay_id"               => null,"remark"                  => request('remark'),"money"                   => $price,"channel"                 => 3,"status"                  => 2,'original_transaction_id' => $product[0]->original_transaction_id,"onlykey"                 => $key,"other"                   => $other,]);$order = Order::query()->where('id', $re['id'])->first();//更新用户到期时间,剩余次数
//                User::saveNewVip($order);User::saveVip($order);//返回给客户端需要结束交易的transaction_id列表return json(1001, '恢复购买');}return json(4001, '校验失败');} catch (\Exception $e) {return json(5001, $e->getMessage());}}/*** 记录通知信息** @param $data*/private function _saveNotify($data){try {$notify_info = TranslationNotify::query()->where(['notification_uuid' => $data['notificationUUID']])->first();$notify_info = objectToArray($notify_info);if ($notify_info) {return;}$transaction_info = verifyAppleToken($data['data']['signedTransactionInfo']);$renewal_info     = verifyAppleToken($data['data']['signedRenewalInfo']);TranslationNotify::create(['notification_uuid'       => $data['notificationUUID'],'notification_type'       => $data['notificationType'],'sub_type'                => $data['subtype'] ?? '','notification_version'    => $data['notificationVersion'] ?? '','app_apple_id'            => $data['data']['appAppleId'] ?? '','bundle_id'               => $data['data']['bundleId'],'bundle_version'          => $data['data']['bundleVersion'],'environment'             => $data['data']['environment'],'transaction_id'          => $transaction_info['transactionId'],'original_transaction_id' => $transaction_info['originalTransactionId'],'web_order_line_item_id'  => $transaction_info['webOrderLineItemId'],'signed_renewal_info'     => json_encode($renewal_info, JSON_UNESCAPED_UNICODE),'signed_transaction_info' => json_encode($transaction_info, JSON_UNESCAPED_UNICODE),]);} catch (\Throwable $t) {Log::channel('transaction')->error($data['notificationUUID'] . '入库失败 error:' . $t->getMessage());}}/*** ios 事件通知* @return bool*/public function iosNotify(){// 通知事件// 订阅类型-入库  首次订阅 重新订阅 续费 套餐升级// 自动续费状态   自动订阅状态 1开启 2-关闭// 自动续费结果   0默认 1成功 2失败 3过期// 是否退款// 是否升级try {$raw = request()->json()->all();Log::channel('transaction')->info('notify-' . json_encode($raw));$token = $raw['signedPayload'] ?? '';$data  = verifyAppleToken($token);if (empty($data)) {//timbao add//记录苹果通知逻辑中的bugLog::channel('transaction')->info('解析苹果通知异常,data为空', $raw['signedPayload']);return true;}// 记录通知信息$this->_saveNotify($data);$transaction_info = verifyAppleToken($data['data']['signedTransactionInfo']);switch ($data['notificationType']) {case 'DID_RENEW': // 续订Transaction::autoRenew($transaction_info);break;case 'REFUND': // 退款Transaction::refund($transaction_info);break;case 'DID_CHANGE_RENEWAL_STATUS': // 连续续订状态 变更Transaction::changeRenewStatus($transaction_info, $data['subtype']);break;case 'EXPIRED': // 过期通知Transaction::updateExpiredTrans($transaction_info);break;case 'SUBSCRIBED': // 订阅通知Transaction::updateSubscribed($transaction_info, $data['subtype']);break;case 'DID_FAIL_TO_RENEW': // 续订失败Transaction::updateRenewFailTrans($transaction_info);break;case 'DID_CHANGE_RENEWAL_PREF': //升级 降级if ($data['subtype'] == 'UPGRADE') {Transaction::autoRenew($transaction_info, true);}break;}return true;} catch (\Throwable $t) {//timbao add //记录自动续费逻辑中的bugLog::channel('transaction')->warning('解析苹果通知过程中异常,msg=' . $t->getMessage());return false;}}
}

3 . 模型


<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use DateTimeInterface;
use Prettus\Repository\Contracts\Transformable;
use Prettus\Repository\Traits\TransformableTrait;/*** Class Transaction.** @package namespace App\Models;*/
class Transaction extends Model implements Transformable
{use TransformableTrait;protected $table = 'transaction';protected $primaryKey = 'id';protected $guarded = [];protected function serializeDate(DateTimeInterface $date){return $date->format('Y-m-d H:i:s');}/*** 续费or升级** @param $transaction_info* @param bool $is_upgrade* @return bool*/public static function autoRenew($transaction_info, $is_upgrade = false){$original_transaction_id = $transaction_info['originalTransactionId']; // 订阅ID$web_order_line_item_id  = $transaction_info['webOrderLineItemId'];    // 交易ID$transaction_id          = $transaction_info['transactionId']; // 苹果订单号$setmeal_id              = $transaction_info['productId'];$sub_type_desc = $is_upgrade == true ? '升级' : '续费';$base_msg      = self::_getBaseMsg($original_transaction_id, $web_order_line_item_id) . "- {$sub_type_desc}:";// 检测订阅$check_result = self::_checkTransaction($transaction_info, $base_msg);if ($check_result['check'] === false) {return false;}$order_info = $check_result['data']['order_info'];$setmeal    = $check_result['data']['setmeal'];// 4.订阅记录入库$user_id = $order_info->user_id;$data    = self::_getAddData($user_id, $transaction_info);// 升级新增参数if ($is_upgrade == true) {$data['sub_type']     = 'UPGRADE';$data['is_upgrade']   = 1;$data['upgrade_time'] = date('Y-m-d H:i:s');}Transaction::query()->create($data);// 订单入库$user  = User::query()->find($user_id);$price = $setmeal['money'];//套餐金额$num   = $setmeal['num'];//数量$title = $setmeal['title'];//标题$title = $user['name'] . "续费" . $title;$other['num']       = $num;$other['price']     = $price;$other['type']      = 'setmeal';$other['date']      = VipSetmeal::getDate($setmeal['date_type']);$other['setmealid'] = $setmeal_id;// 保存订单信息$res   = Order::query()->create(["user_id"                 => $user_id,"title"                   => $title,"ordernum"                => order::getOrderNum(),"prepay_id"               => null,"remark"                  => request('remark'),"money"                   => $price,"channel"                 => 3,"status"                  => 2,"transaction_id"          => $transaction_id,'original_transaction_id' => $original_transaction_id,"onlykey"                 => Order::getOnlykey($transaction_id, $user_id),"other"                   => $other,'setmealid'               => $setmeal_id]);$order = Order::query()->where('id', $res['id'])->first();//更新用户到期时间,剩余次数User::saveVip($order);return true;}/*** 检测交易** @param $transaction_info* @param $base_msg* @return array*/private static function _checkTransaction($transaction_info, $base_msg){$ret = ['check' => false,'data'  => [],];// 1.判断是否存在订阅ID订单$order_info = Order::query()->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->where('channel', '=', 3)->where('status', '=', 2)->orderBy('id')->first();if (!$order_info) {Log::channel('transaction')->warning($base_msg . 'not find order info');return $ret;}// 2.判断交易ID是否已经入库过$is_exist = Transaction::query()->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->count();if ($is_exist) {Log::channel('transaction')->warning($base_msg . " 交易ID存在");return $ret;}// 3.判断套餐是否存在$transaction_id = $transaction_info['transactionId']; // 苹果订单号$setmeal_id     = $transaction_info['productId'];$setmeal        = VipSetmeal::query()->where('setmealid', $setmeal_id)->first();if (!$setmeal) {Log::channel('transaction')->warning($base_msg . ' 自动续费没找到对应套餐,transaction_id=' . $transaction_id . ' and setmeal_id=' . $setmeal_id);return $ret;}$ret['check'] = 'success';$ret['data']  = compact('setmeal', 'order_info');return $ret;}/*** 退款** @param $transaction_info* @return bool*/public static function refund($transaction_info){$original_transaction_id = $transaction_info['originalTransactionId']; // 订阅ID$web_order_line_item_id  = $transaction_info['webOrderLineItemId']; // 交易ID$base_msg = self::_getBaseMsg($original_transaction_id, $web_order_line_item_id) . '- 退款:';$transaction_info_data = Transaction::query()->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->first();if (!$transaction_info_data) {Log::channel('transaction')->warning($base_msg . ' not find transaction info');return false;}// 修改退款时间以及状态Transaction::query()->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->update(['cancellation_date_ms' => date('Y-m-d H:i:s', $transaction_info['revocationDate'] / 1000),'is_cancellation'      => 1,]);// 修改用户会员为到期User::saveExpireVip($transaction_info_data->user_id);return true;}/*** 订阅** @param $transaction_info* @param $subtype* @return bool*/public static function updateSubscribed($transaction_info, $subtype){if (!in_array($subtype, ['INITIAL_BUY', 'RESUBSCRIBE'])) {return false;}$original_transaction_id = $transaction_info['originalTransactionId']; // 订阅ID$web_order_line_item_id  = $transaction_info['webOrderLineItemId']; // 交易ID$base_msg                = self::_getBaseMsg($original_transaction_id, $web_order_line_item_id) . '- 订阅:';// 检测订阅$check_result = self::_checkTransaction($transaction_info, $base_msg);if ($check_result['check'] === false) {return false;}$order_info = $check_result['data']['order_info'];$user_id = $order_info->user_id;// 4.订阅记录入库$data             = self::_getAddData($user_id, $transaction_info);$data['sub_type'] = "SUBSCRIBED-{$subtype}";Transaction::query()->create($data);return true;}/*** 续订失败** @param $transaction_info* @return bool*/public static function updateExpiredTrans($transaction_info): bool{return Transaction::query()->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->update(['auto_renew_result' => 3,]);}/*** 续订失败** @param $transaction_info* @return bool*/public static function updateRenewFailTrans($transaction_info): bool{return Transaction::query()->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->update(['auto_renew_status' => 2,]);}/*** 自动续费状态修改** @param $transaction_info* @param $subtype* @return bool*/public static function changeRenewStatus($transaction_info, $subtype): bool{switch ($subtype) {case 'AUTO_RENEW_ENABLED':$auto_renew_status = 1;break;case 'AUTO_RENEW_DISABLED':$auto_renew_status = 2;break;default:return false;}return Transaction::query()->where('web_order_line_item_id', '=', $transaction_info['webOrderLineItemId'])->where('original_transaction_id', '=', $transaction_info['originalTransactionId'])->update(['auto_renew_status' => $auto_renew_status,]);}/*** 获取日志** @param $original_transaction_id* @param $web_order_line_item_id* @return string*/private static function _getBaseMsg($original_transaction_id, $web_order_line_item_id){return "notify-{$original_transaction_id}-{$web_order_line_item_id} ";}/*** 获取入库信息** @param $user_id* @param $transaction_info* @return array*/private static function _getAddData($user_id, $transaction_info){return ['user_id'                       => $user_id, // fixme 此处不考虑用户ID切换'transaction_id'                => $transaction_info['transactionId'],'product_id'                    => $transaction_info['productId'],'web_order_line_item_id'        => $transaction_info['webOrderLineItemId'],'original_transaction_id'       => $transaction_info['originalTransactionId'],'original_purchase_date_ms'     => date('Y-m-d H:i:s', $transaction_info['originalPurchaseDate'] / 1000), //首次订阅时间'purchase_date_ms'              => date('Y-m-d H:i:s', $transaction_info['purchaseDate'] / 1000), // 购买时间'expires_date_ms'               => date('Y-m-d H:i:s', $transaction_info['expiresDate'] / 1000), // 过期时间'subscription_group_identifier' => $transaction_info['subscriptionGroupIdentifier'] ?? '','in_app_ownership_type'         => $transaction_info['inAppOwnershipType'],'environment'                   => $transaction_info['environment'] ?? '','sub_type'                      => 'DID_RENEW', // DID_RENEW UPGRADE  SUBSCRIBED-INITIAL_BUY  SUBSCRIBED-RESUBSCRIBE'auto_renew_result'             => 1, // 自动续费结果 0默认 1成功 2失败 3过期'auto_renew_status'             => 1, // 自动订阅状态 1开启 2-关闭];}
}

苹果秘钥解密

if (!function_exists('verifyAppleToken')) {/*** 苹果token解密** @param $token* @return array|bool|mixed|string*/function verifyAppleToken($token){$arr = explode('.',$token);if(count($arr) != 3){return [];}$header = $arr[0];$header = base64_decode($header);$header = json_decode($header,true);if(empty($header)){return [];}if($header['alg'] != 'ES256'){return [];}$data = $arr[1];$data = base64_decode($data);$data = json_decode($data,true);if(empty($data)){return [];}$sign = $arr[2];if(empty($sign)){return [];}return $data;}
}

相关表设计

#订单表
CREATE TABLE `orders` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`ordernum` varchar(100) DEFAULT '' COMMENT '系统订单号',`title` varchar(255) DEFAULT '' COMMENT '套餐名称',`user_id` varchar(20) NOT NULL COMMENT '用户id',`money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '支付金额',`pay_time` datetime DEFAULT NULL COMMENT '支付时间',`channel` tinyint(1) DEFAULT NULL COMMENT '支付渠道:1支付宝 2微信 3apple',`transaction_id` varchar(30) CHARACTER SET utf8 DEFAULT '' COMMENT '微信支付交易号',`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '支付结果:1待支付 2已支付 3已关闭 4支付失败',`remark` varchar(255) DEFAULT NULL COMMENT '备注',`other` varchar(255) DEFAULT NULL,`prepay_id` varchar(50) DEFAULT NULL COMMENT '微信支付',`original_transaction_id` varchar(30) DEFAULT NULL,`onlykey` varchar(35) DEFAULT NULL COMMENT '唯一字符串',`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL,`setmealid` varchar(50) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `ordernum` (`ordernum`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='订单表'#事件日志表
CREATE TABLE `translation_notify` (`id` int(11) NOT NULL AUTO_INCREMENT,`notification_uuid` varchar(255) DEFAULT NULL COMMENT '通知唯一id',`notification_type` varchar(255) DEFAULT NULL COMMENT '通知类型',`sub_type` varchar(255) DEFAULT NULL COMMENT '通知子类型',`notification_version` varchar(255) DEFAULT NULL COMMENT '通知版本',`app_apple_id` varchar(255) DEFAULT NULL COMMENT '应用苹果id',`bundle_id` varchar(255) DEFAULT NULL COMMENT '包id',`bundle_version` varchar(255) DEFAULT NULL COMMENT '包版本',`environment` varchar(255) DEFAULT NULL COMMENT '环境',`signed_renewal_info` varchar(1024) DEFAULT NULL COMMENT '签名数据',`signed_transaction_info` varchar(1024) DEFAULT NULL COMMENT '数据',`transaction_id` varchar(255) NOT NULL DEFAULT '' COMMENT '订单号',`original_transaction_id` varchar(255) NOT NULL DEFAULT '' COMMENT '原始订单号',`web_order_line_item_id` varchar(255) NOT NULL DEFAULT '' COMMENT '目测唯一订单号',`created_at` timestamp NULL DEFAULT NULL,`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `idx_uuid` (`notification_uuid`) USING BTREE,KEY `idx_oid_wid` (`original_transaction_id`,`web_order_line_item_id`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COMMENT='事件日志表'#订阅交易表CREATE TABLE `transaction` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',`transaction_id` varchar(255) NOT NULL DEFAULT '' COMMENT '订单号',`original_transaction_id` varchar(255) NOT NULL DEFAULT '' COMMENT '苹果唯一ID',`original_purchase_date_ms` datetime DEFAULT NULL COMMENT '订阅时间',`purchase_date_ms` datetime DEFAULT NULL COMMENT '购买时间',`expires_date_ms` datetime DEFAULT NULL COMMENT '过期时间',`cancellation_date_ms` datetime DEFAULT NULL COMMENT '退款时间',`web_order_line_item_id` varchar(255) NOT NULL,`is_trial_period` int(11) NOT NULL DEFAULT '0',`is_in_intro_offer_period` int(11) NOT NULL DEFAULT '0',`in_app_ownership_type` varchar(255) NOT NULL,`product_id` varchar(255) NOT NULL DEFAULT '' COMMENT '产品id',`subscription_group_identifier` varchar(255) NOT NULL DEFAULT '',`environment` varchar(20) NOT NULL DEFAULT '' COMMENT '环境',`sub_type` varchar(50) NOT NULL DEFAULT '' COMMENT '订阅类型',`is_upgrade` tinyint(1) NOT NULL DEFAULT '2' COMMENT '是否升级 1-是 2-否 ',`is_cancellation` tinyint(1) NOT NULL DEFAULT '2' COMMENT '是否退款 1-是 2-否 ',`auto_renew_status` tinyint(1) NOT NULL DEFAULT '2' COMMENT '自动订阅状态 1开启 2-关闭',`auto_renew_result` tinyint(1) NOT NULL DEFAULT '0' COMMENT '自动续费结果 0默认 1成功 2失败 3过期',`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`upgrade_time` datetime DEFAULT NULL COMMENT '升级时间',PRIMARY KEY (`id`) USING BTREE,KEY `idx_trans_id` (`transaction_id`) USING BTREE,KEY `idx_web_order_id` (`web_order_line_item_id`) USING BTREE,KEY `idx_origin_order_id` (`original_transaction_id`) USING BTREE,KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COMMENT='订阅交易表'

PHP苹果支付以及事件通知-周期订阅实现相关推荐

  1. python苹果支付(服务端)自动订阅版

    苹果支付分为沙盒环境和生产环境 苹果验单逻辑 苹果支付过程中离不开一个东西--receipt(凭证) 服务端接收APP发送的凭证 服务端拿着凭证到苹果的服务器验单(沙盒.生产) 服务端解析苹果服务器返 ...

  2. php苹果支付订阅付费_比较订阅,按错误付费和咨询软件业务模型

    php苹果支付订阅付费 在今年的FOSS Backstage,我主持了关于开源模型的讨论,并分享了为什么我认为订阅是支持开源产品的好方法. 选择一种商业模式 我与他人共同创立的公司Nextcloud仅 ...

  3. ECS事件通知之创建失败事件

    ECS提供了批量实例创建接口,可以一次调用创建最多100台实例.批量创建接口可以完成批量实例的创建.启动.IP分配等流程,可以快速完成实例资源的扩容. 在实例的创建过程中(实际后台异步创建),库存和V ...

  4. 苹果支付成功后,JAVA服务端二次验证

    原理简述: 苹果客户端在完成应用购买,下单后支付,苹果后台会给客户端返回信息,用来验证支付结果: 客户端在拿到返回值后,将指定返回值,通过接口形式请求应用服务器,应用服务器根据这个值调用苹果服务器进行 ...

  5. 一文解密 Netflix 的快速事件通知系统是如何工作的

    Netflix 拥有超过 2.2 亿活跃会员,他们会使用各种功能执行大量操作.近乎实时地对这些操作做出反应以保持跨设备的体验一致,这对于确保最佳会员体验至关重要.考虑到支持的设备种类繁多以及会员执行的 ...

  6. 苹果支付流程以及服务端php验证

    苹果支付和常规国内的支付流程完全不一样,流程如下: 步骤如下: 1.上架产品 首先需要在苹果网站上架对应的app产品,有对应的id和价格,名称等数据 2.前端拉起商品列表 用户登录app后,进入商品购 ...

  7. java验证苹果支付收据(转载)

    转自胖哥的整理,地址:http://blog.csdn.net/cnhome/article/details/79380557 苹果说明文档:https://developer.apple.com/l ...

  8. PJSIP开发手册之SIP事件通知(十三)

    第十三章 SIP特定的事件通知 SIP事件特定的通知在RFC3265"Session Initiation Protocol-SpecificEvent Notification" ...

  9. WMI技术介绍和应用——事件通知

    在<WMI技术介绍和应用--WMI概述>中,我们使用了下图介绍WMI构架(转载请指明出于breaksoftware的csdn博客) 我们之前介绍的使用WMI查询系统.硬件等信息的功能,是通 ...

最新文章

  1. VScode+SSH Remote多级连跳配置
  2. 组装简历必备的9大要件
  3. 计算机硬件四大部分组成部分,2014考研计算机大纲 组成原理部分四大变化解析...
  4. Oracle优化09-绑定变量
  5. markdown数学公式手册
  6. JavaSE——MD5、16位流
  7. matlab的combuilder系列-matlab下做com组件 zzfrom SMTH bbs
  8. matlab定义和调用函数m,Matlab学习-自定义函数与调用
  9. 用CocoaPods安装ReactiveCocoa遇到的问题
  10. 木.马查杀-应急工具-排查步骤
  11. PyQt5 the application failed to start because no Qt platform could be initialized
  12. html5页面关闭的回调函数,js回调函数例子 js 回调函数问题的执行结果想作为返回值...
  13. 微信一键激活 设置开卡字段
  14. GBS国标经纬度转高德经纬度
  15. 遥感影像中常用的目标检测数据集
  16. api zoom 实现自动预定_一种基于Web端API的网络地图图片自动截取拼接的方法与流程...
  17. php数据库用户名验证失败,PHP与MySQL 8.0+错误:服务器请求的身份验证方法未知...
  18. word长篇文档排版技巧教学视频
  19. 图片转字符画(python)
  20. 局域网lan_什么是局域网(LAN)?

热门文章

  1. 《文化相对论》:危机重重的世界,对话才能产生转机
  2. 在Windows中重装BootCamp服务
  3. Clean Code 读书笔记三——方法(函数)
  4. mysql按月统计最近一年,半年数量,本月每一天
  5. 在计算机系统中 接口是什么意思,计算机中操作系统是什么的接口
  6. python3爬虫进阶之自动登录网易云音乐并爬取指定歌曲评论
  7. 生活多快乐:笑死爹的程序段子
  8. 天气预报发展简史:从玄学到科学
  9. 【视音频编程学习】FFmpeg十个常用命令 || pcm与wav、amr、aac、mp3互转
  10. 简便的进制转换方法(不简便打我,反正也打不到,hhh)