介绍参考链接 https://blog.csdn.net/qq_23564667/article/details/105512349 iOS内购(IAP)自动续订订阅类型服务端总结 ioS 后台需注意 iOS 的 App 内购类
介绍参考链接
https://blog.csdn.net/qq_23564667/article/details/105512349
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/
那我们首先来实现客户端+服务端支付功能
客户端实现自行实现
服务端需要编写
创建订单接口-给客户端订单号
客户端支付成功后请求通知接口用于发放奖励
恢复内购接口 -玩家可以恢复购买
苹果平台配置通知回调接口--处理回调事件
比如订阅、续费、升级 比如我们是周期扣款 其中订阅就是客户端付款的那一笔
续费以及升级需要我们在创建订单来发送奖励、退款收回奖励
关于用户如何与通知事件记录绑定
客户端发起付款我们会生成订单其中有用户ID以及初始化订单信息
客户端扣款成功我们解密回传数据可以得到
originalTransactionId -- 苹果用户与我们生成的唯一ID 以后都不会变
苹果通知事件中会存在originalTransactionId 我们与订单中的用户ID建立绑定就好
如果产品存在多用户请自行处理相关逻辑
如何保证回传事件是否同一笔订单
transactionId、WEBOrderLineItemId 每次交易都会生成唯一订单ID 这里使用的是originalTransactionId +webOrderLineItemId 来确定事件订单
具体代码实现
路由
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恢复内购
控制器
middleware(['api']); } private function _getAppleUrl($san_box) { return $san_box ? 'https://sandbox.itunes.apple.com/verifyReceipt' : 'https://buy.itunes.apple.com/verifyReceipt'; } 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; } 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); } 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); } 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 null Log::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'); } } protected function validate_apple_pay($receipt_data, $ios_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; } 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; } } 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; } 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()); } } 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()); } } 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 //记录苹果通知逻辑中的bug Log::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 //记录自动续费逻辑中的bug Log::channel('transaction')->warning('解析苹果通知过程中异常,msg=' . $t->getMessage()); return false; } }}
3 . 模型
format('Y-m-d H:i:s'); } 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; } 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; } 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; } 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; } 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, ]); } 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, ]); } 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, ]); } private static function _getBaseMsg($original_transaction_id, $web_order_line_item_id) { return "notify-{$original_transaction_id}-{$web_order_line_item_id} "; } 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')) { 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='订阅交易表'
来源地址:https://blog.csdn.net/woshihaiyong168/article/details/128634312
--结束END--
本文标题: PHP苹果支付以及事件通知-周期订阅实现
本文链接: https://www.lsjlt.com/news/392962.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0