iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > PHP编程 >PHP苹果支付以及事件通知-周期订阅实现
  • 475
分享到

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

phpPoweredby金山文档 2023-09-04 05:09:02 475人浏览 泡泡鱼
摘要

介绍参考链接 https://blog.csdn.net/qq_23564667/article/details/105512349 iOS内购(IAP)自动续订订阅类型服务端总结 ioS 后台需注意 iOS 的 App 内购类

介绍参考链接

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/

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

客户端实现自行实现

服务端需要编写

  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. 控制器

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文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作