From 08da50a0b77aabb63ea18a08d69d3657ddeedfc8 Mon Sep 17 00:00:00 2001 From: BvBeJ Date: Tue, 30 Nov 2021 21:24:49 +0800 Subject: [PATCH] first commit --- .gitignore | 5 + README.md | 3 + composer.json | 39 ++ config/apollo.php | 10 + src/Commands/ApolloConfigSync.php | 47 ++ src/Facades/StarPoolCloudFacade.php | 24 + src/Handles/Apollo/Client.php | 460 ++++++++++++++++++ src/Handles/Apollo/Signature.php | 56 +++ src/Handles/File/code.php | 262 ++++++++++ src/Handles/File/helper.php | 131 +++++ src/Handles/StarPoolCloudHandle.php | 227 +++++++++ src/Handles/Trait/ThrottlesLogins.php | 124 +++++ .../StarPoolCloudServiceProvider.php | 45 ++ 13 files changed, 1433 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/apollo.php create mode 100644 src/Commands/ApolloConfigSync.php create mode 100644 src/Facades/StarPoolCloudFacade.php create mode 100644 src/Handles/Apollo/Client.php create mode 100644 src/Handles/Apollo/Signature.php create mode 100644 src/Handles/File/code.php create mode 100644 src/Handles/File/helper.php create mode 100644 src/Handles/StarPoolCloudHandle.php create mode 100644 src/Handles/Trait/ThrottlesLogins.php create mode 100644 src/Providers/StarPoolCloudServiceProvider.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1633982 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea/ +/vendor/ +/test/ + +composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..9737c28 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +```shell +php artisan vendor:publish --provider=StarPoolCloud\Providers\StarPoolCloudServiceProvider +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b0ba984 --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "starpoolcloud/base-php", + "license": "MIT", + "authors": [ + { + "name": "lifangyi", + "email": "lifangyi@squrab.com" + } + ], + "require": { + "php": "^8.0", + "ext-openssl": "*", + "ext-curl": "*", + "ext-bcmath": "*", + "laravel/framework": "^8.40", + "ramsey/uuid": "^4.1", + "guzzlehttp/guzzle": "^7.3", + "zoujingli/ip2region": "^1.0" + }, + "autoload": { + "psr-4": { + "StarPoolCloud\\": "src/" + }, + "files": [ + "src/Handles/File/helper.php", + "src/Handles/File/code.php" + ] + }, + "extra": { + "laravel": { + "providers": [ + "StarPoolCloud\\Providers\\StarPoolCloudServiceProvider" + ], + "aliases": { + "StarPoolCloud": "StarPoolCloud\\Facades\\StarPoolCloudFacade" + } + } + } +} diff --git a/config/apollo.php b/config/apollo.php new file mode 100644 index 0000000..5d0f128 --- /dev/null +++ b/config/apollo.php @@ -0,0 +1,10 @@ + 'https://apollo.config.squrab.com', + 'cluster' => 'dev', + 'client_ip' => '127.0.0.1', + 'appid' => env('APP_NAME', ''), + 'secret' => env('APOLLO_SECRET', ''), + 'namespace' => env('APP_ENV', '') +]; diff --git a/src/Commands/ApolloConfigSync.php b/src/Commands/ApolloConfigSync.php new file mode 100644 index 0000000..0d8dac2 --- /dev/null +++ b/src/Commands/ApolloConfigSync.php @@ -0,0 +1,47 @@ +updateEnv(); + $this->info('Success'); + } catch (Exception $exception) { + $this->error("Error:{$exception->getMessage()}"); + } + } +} \ No newline at end of file diff --git a/src/Facades/StarPoolCloudFacade.php b/src/Facades/StarPoolCloudFacade.php new file mode 100644 index 0000000..57918ed --- /dev/null +++ b/src/Facades/StarPoolCloudFacade.php @@ -0,0 +1,24 @@ + $item) { + if (blank($item)) { + throw new Exception("必须配置$key", -1); + } + } + $this->configServerUrl = $config['server_addr']; + $this->clientIp = $config['client_ip']; + $this->clusterName = $config['cluster']; + $this->namespaceName = $config['namespace']; + $this->secret = $config['secret']; + $this->appId = $config['appid']; + $this->guzzleHttpClient = new GuzzleHttpClient(['http_errors' => false]); + } + + /** + * 从阿波罗服务器读取配置 + * @param bool $useCacheApi 是否通过带缓存的Http接口从Apollo读取配置,设置为false可以使用不带缓存的Http接口从Apollo读取配置 + * @param string $releaseKey 上一次的releaseKey + * @return array + */ + public function getConfig(bool $useCacheApi = false, string $releaseKey = ''): array + { + $this->isMultiGet = false; + $res = $this->multiGetConfig([$this->appId => [$this->namespaceName => $releaseKey]], $useCacheApi); + //当前应用指定namespace的http请求信息 + if (isset($this->httpInfo[$this->appId][$this->namespaceName])) { + $this->httpInfo = $this->httpInfo[$this->appId][$this->namespaceName]; + } else { + $this->httpInfo = []; + } + //当前应用指定namespace的错误信息 + if (isset($this->errorInfo[$this->appId][$this->namespaceName])) { + $this->errorInfo = $this->errorInfo[$this->appId][$this->namespaceName]; + } else { + $this->errorInfo = []; + } + if (!empty($this->errorInfo)) { + return []; + } + return $res[$this->appId][$this->namespaceName] ?? []; + } + + /** + * 更新.env文件 + * @throws Exception + */ + public function updateEnv() + { + $config = $this->getConfig(); + if (blank($config)) { + throw new Exception('未获取Apollo配置', -1); + } + $envFile = base_path(app()->environmentFile()); + $envFileContent = file_get_contents($envFile); + if ($envFileContent === false) { + throw new Exception('无法读取.env文件', -1); + } + $envArr = Dotenv::parse($envFileContent); + $toArray = collect($envArr)->merge($config)->sortKeys()->toArray(); + $newEnvFileContent = ''; + $group = []; + $i = 0; + foreach ($toArray as $name => $value) { + $prefix = ''; + $explode = explode('_', $name); + $group[$i] = $explode[0]; + if ($i > 0 && $group[$i - 1] !== $group[$i]) { + $prefix = "\n"; + } + $newEnvFileContent .= sprintf("%s%s=%s%s", $prefix,$name, $value, "\n"); + $i++; + } + file_put_contents($envFile, $newEnvFileContent); + } + + /** + * 批量读取配置 + * @param array $appNamespaceData 应用id及其Namespace列表信息,格式例子: + * Array( + * 'app_id_1' => [ + * 'application' => '', + * 'FX.apollo' => '' + * ], + * 'app_id_2' => [ + * 'application' => '' + * ] + * ) + * @param bool $useCacheApi 是否通过带缓存的Http接口从Apollo读取配置,设置为false可以使用不带缓存的Http接口从Apollo读取配置 + * @return array + */ + public function multiGetConfig(array $appNamespaceData, bool $useCacheApi = true): array + { + $this->httpInfo = []; + + $res = []; + if (empty($appNamespaceData)) { + return $res; + } + + $isMultiGet = $this->isMultiGet;//是否批量获取配置 + $asyncGetResult = $this->asyncGetResult;//通过回调函数异步获取返回结果 + $setHttpInfo = $this->setHttpInfo;//附带http信息 + $setErrorInfo = $this->setHttpInfo;//附带错误信息 + + foreach ($appNamespaceData as $appId => &$namespaceData) { + foreach ($namespaceData as $namespaceName => &$releaseKey) { + //带缓存接口,置空releaseKey + $useCacheApi === true && $releaseKey = ''; + //初始化返回结果 + !isset($res[$appId][$namespaceName]) && $res[$appId][$namespaceName] = []; + $this->promise = $this->requestAsync( + $this->buildGetConfigRequestUrl($appId, $namespaceName, $useCacheApi, $releaseKey), + $appId, + ['timeout' => 10]//默认10秒超时 + ); + $this->promise->then( + function (ResponseInterface $response) use ( + &$res, + $appId, + $namespaceName, + $useCacheApi, + $asyncGetResult, + $isMultiGet, + $setHttpInfo + ) { + $responseCode = $response->getStatusCode(); + $responseBody = (string)$response->getBody(); + if ($setHttpInfo === true) { + $this->httpInfo[$appId][$namespaceName] = [ + 'response_code' => $responseCode, + 'response_body' => $responseBody + ]; + } + switch ($responseCode) { + case 200: + $responseBody = json_decode($responseBody, true); + empty($responseBody) && $responseBody = []; + break; + case 304: + $responseBody = []; + break; + default: + $responseBody = false; + } + if ($useCacheApi === false) {//不带缓存的接口,配置项在configurations里面 + $res[$appId][$namespaceName] = $responseBody['configurations']; + } else {//带缓存的接口,responseBody就是配置项 + $res[$appId][$namespaceName] = $responseBody; + } + //把结果通过$asyncGetResult回调函数交给上层 + if (is_callable($asyncGetResult)) { + call_user_func($asyncGetResult, $isMultiGet === true ? $res : $res[$appId][$namespaceName]); + } + }, + function (RequestException $exception) use (&$res, $appId, $namespaceName, $setErrorInfo) { + if ($setErrorInfo === true) { + $this->errorInfo[$appId][$namespaceName] = [ + 'code' => $exception->getCode(), + 'message' => $exception->getMessage() + ]; + } + $res[$appId][$namespaceName] = false;//存在异常则设置结果为false + } + ); + } + } + + $this->promiseWait === true && $this->promiseWait(); + + return $res; + } + + /** + * 多个应用感知配置更新 + * @param array $appNotificationsData 应用id及notifications信息,格式例子: + * Array( + * 'app_id_1' => [ + * 'application' => 100, + * 'FX.apollo' => 200 + * ], + * 'app_id_2' => [ + * 'application' => 100 + * ] + * ) + * @param mixed|null $onConfigUpdate 当存在配置更新时触发的回调函数 + * @param null $onResponse + * @return void + */ + public function listenMultiAppConfigUpdate(array $appNotificationsData, mixed $onConfigUpdate = null, $onResponse = null): void + { + if (empty($appNotificationsData)) { + return; + } + $this->httpInfo = []; + + //以下是执行流程 + //发起http长轮询监听指定应用的配置更新(请求会被服务器hold住) + //如果被监听namespace发生配置变更(服务器会立刻响应当前请求,返回新的notificationId) + //本地拿到新的notificationId,更新本地的映射表,然后再次发起http长轮询监听指定应用的配置更新 + $loopForConfigUpdate = function ($appId, $namespaceNotificationMapping) use ( + &$onConfigUpdate, &$loopForConfigUpdate, &$onResponse + ) { + //生成notifications + $notifications = []; + foreach ($namespaceNotificationMapping as $namespaceName => $notificationId) { + $notifications[] = ['namespaceName' => $namespaceName, 'notificationId' => $notificationId]; + } + $this->promise = $this->requestAsync( + $this->buildAwareConfigUpdateUrl($appId, $notifications), $appId, ['timeout' => 63] + ); + unset($notifications); + + $this->promise->then( + function (ResponseInterface $response) use ( + $appId, &$loopForConfigUpdate, &$namespaceNotificationMapping, &$onConfigUpdate, &$onResponse + ) { + //触发响应函数 + if (is_callable($onResponse)) { + call_user_func_array($onResponse, [$appId, $response]); + } + $responseCode = $response->getStatusCode(); + if ($responseCode === 200) { + $body = $response->getBody(); + $body = json_decode($body, true); + if (!empty($body) && is_array($body)) { + foreach ($body as &$value) { + if ( + !isset($value['namespaceName']) || + !isset($value['notificationId']) + ) { + continue; + } + $namespaceName = &$value['namespaceName']; + $notificationId = &$value['notificationId']; + if ( + isset($namespaceNotificationMapping[$namespaceName]) && + $namespaceNotificationMapping[$namespaceName] != $notificationId + ) {//配置发生变更了 + //更新映射表 + $namespaceNotificationMapping[$namespaceName] = $notificationId; + //触发配置变更回调函数 + if (is_callable($onConfigUpdate)) { + $this->promiseWait = false;//关闭getConfig方法内的promise wait + $this->setHttpInfo = false;//关闭getConfig方法内的保存http信息的逻辑 + $this->setErrorInfo = false;//关闭getConfig方法内的保存错误信息的逻辑 + //由于接管了getConfig方法的promise wait,通过回调函数获取返回结果 + $this->asyncGetResult = function ($newConfig) use ( + $appId, + $namespaceName, + &$onConfigUpdate, + $notificationId, + &$namespaceNotificationMapping + ) { + if ($newConfig !== false) { + call_user_func_array( + $onConfigUpdate, + [ + $appId, + $namespaceName, + $newConfig, + $notificationId, + &$namespaceNotificationMapping + ] + ); + } + }; + //以下方法返回结果为空数组 + $this->getConfig(false); + } + } + } + } + } + //再次发起http长轮询监听指定应用的配置更新 + $loopForConfigUpdate($appId, $namespaceNotificationMapping); + }, + function (RequestException $exception) use ($appId, &$loopForConfigUpdate, &$namespaceNotificationMapping) {//偶尔有些超时请求会从此处产生 + //防止因为阿波罗服务器异常而导致进入无限死循环 + $nowTime = time(); + $errorTimeLimit = 5; + if (!empty($this->onRejectedTimeList[$appId])) { + if ( + count($this->onRejectedTimeList[$appId]) === $errorTimeLimit && + count(array_unique($this->onRejectedTimeList[$appId])) <= 2 + ) {//瞬间产生过多错误,退出event loop + die('错误码:' . $exception->getCode() . ',错误信息:' . $exception->getMessage() . PHP_EOL); + } + } + $this->onRejectedTimeList[$appId][] = $nowTime; + if (count($this->onRejectedTimeList[$appId]) > $errorTimeLimit) { + array_shift($this->onRejectedTimeList[$appId]); + } + //再次发起http长轮询监听指定应用的配置更新 + $loopForConfigUpdate($appId, $namespaceNotificationMapping); + } + ); + }; + + foreach ($appNotificationsData as $appId => $namespaceNotificationMapping) { + if (empty($namespaceNotificationMapping)) { + continue; + } + $loopForConfigUpdate($appId, $namespaceNotificationMapping); + } + + $this->promiseWait(); + } + + /** + * 获取http请求信息 + * @return array + */ + public function getHttpInfo(): array + { + return $this->httpInfo; + } + + /** + * 获取错误信息 + * @return array + */ + public function getErrorInfo(): array + { + return $this->errorInfo; + } + + /** + * 构建用于请求的阿波罗接口链接 + * @param string $appId 应用的appId + * @param string $namespaceName Namespace的名字 + * @param bool $useCacheApi 是否通过带缓存的Http接口从Apollo读取配置 + * @param string $releaseKey 上一次的releaseKey + * @return string + */ + private function buildGetConfigRequestUrl(string $appId, string $namespaceName, bool $useCacheApi = true, string $releaseKey = ''): string + { + if (empty($appId) || empty($namespaceName)) { + return ''; + } + if ($useCacheApi === true) { + $url = "$this->configServerUrl/configfiles/json/$appId/$this->clusterName/$namespaceName"; + } else { + $url = "$this->configServerUrl/configs/$appId/$this->clusterName/$namespaceName"; + } + $params = []; + if (!empty($this->clientIp)) { + $params['ip'] = $this->clientIp; + } + if (!empty($releaseKey)) { + $params['releaseKey'] = $releaseKey; + } + if (!empty($params)) { + $url .= '?' . http_build_query($params); + } + return $url; + } + + /** + * 构建用于请求的阿波罗接口链接 + * @param string $appId 应用的appId + * @param array $notifications notifications信息,格式为二维数组,格式例子: + * Array( + * ['namespaceName' => 'application', 'notificationId' => 100], + * ['namespaceName' => 'FX.apollo', 'notificationId' => 200] + * ) + * @return string + */ + private function buildAwareConfigUpdateUrl(string $appId, array $notifications = []): string + { + if (empty($appId) || empty($notifications)) { + return ''; + } + $notifications = urlencode(json_encode($notifications)); + return "$this->configServerUrl/notifications/v2?appId=$appId&cluster=$this->clusterName¬ifications=$notifications"; + } + + /** + * 发起异步请求 + * @param string $url 请求链接 + * @param string $appId 应用的appId + * @param array $options 请求配置,参考guzzlehttp文档 + * @return PromiseInterface + */ + private function requestAsync(string $url, string $appId = '', array $options = []): PromiseInterface + { + if ( + !empty($this->secret) && + !empty($appId) + ) {//追加访问密钥 + $timestamp = time() * 1000; + $urlInfo = parse_url($url); + if (!empty($urlInfo['path'])) { + $pathWithQuery = $urlInfo['path']; + if (!empty($urlInfo['query'])) { + $pathWithQuery .= '?' . $urlInfo['query']; + } + $options['headers'][Signature::HTTP_HEADER_AUTHORIZATION] = Signature::getAuthorizationString( + $appId, $timestamp, $pathWithQuery, $this->secret + ); + $options['headers'][Signature::HTTP_HEADER_TIMESTAMP] = $timestamp; + } + unset($urlInfo); + } + return $this->guzzleHttpClient->requestAsync('GET', $url, $options); + } + + /** + * 发起异步请求 + * @return void + */ + private function promiseWait(): void + { + if (!is_null($this->promise)) { + try { + $this->promise->wait(); + } catch (Exception) { + //屏蔽promise wait的错误,因为错误信息已经在promise then的onRejected回调函数中返回 + } + } + } +} \ No newline at end of file diff --git a/src/Handles/Apollo/Signature.php b/src/Handles/Apollo/Signature.php new file mode 100644 index 0000000..84a3c8f --- /dev/null +++ b/src/Handles/Apollo/Signature.php @@ -0,0 +1,56 @@ + [ + 'cn' => '写入错误', + 'en' => 'Write error', + 'hk' => '寫入錯誤', + 'ja' => '書き込みエラー', + ], + 10502 => [ + 'cn' => '用户令牌无效', + 'en' => 'Invalid user token', + 'hk' => '用戶令牌無效', + 'ja' => '無効なユーザートークン', + ], + 10503 => [ + 'cn' => '存储服务错误', + 'en' => 'Storage service error', + 'hk' => '存儲服務錯誤', + 'ja' => 'ストレージサービスエラー', + ], + 10504 => [ + 'cn' => '数据库写入错误', + 'en' => 'Database write error', + 'hk' => '數據庫寫入錯誤', + 'ja' => 'データベース書き込みエラー', + ], + 10505 => [ + 'cn' => '订单匹配错误', + 'en' => 'Order matching error', + 'hk' => '訂單匹配錯誤', + 'ja' => '注文照合エラー', + ], + 10506 => [ + 'cn' => '口令请求错误', + 'en' => 'Token request error', + 'hk' => '口令請求錯誤', + 'ja' => 'パスワードリクエストエラー', + ], + + + //业务级别 20500 - 20699 + + 20501 => [ + 'cn' => '参数缺失', + 'en' => 'Missing parameters', + 'hk' => '參數缺失', + 'ja' => 'パラメータがありません', + ], + 20502 => [ + 'cn' => '暂无数据', + 'en' => 'No data', + 'hk' => '暫無數據', + 'ja' => 'データなし', + ], + 20503 => [ + 'cn' => '商品/规格信息不匹配', + 'en' => 'Product/specification information does not match', + 'hk' => '商品/規格信息不匹配', + 'ja' => '製品/仕様情報が一致しません', + ], + 20504 => [ + 'cn' => '订单信息错误', + 'en' => 'Incorrect order information', + 'hk' => '訂單信息錯誤', + 'ja' => '注文情報が正しくありません', + ], + 20505 => [ + 'cn' => '没有文件需要上传', + 'en' => 'No files to upload', + 'hk' => '沒有文件需要上傳', + 'ja' => 'アップロードするファイルがありません', + ], + 20506 => [ + 'cn' => '文件上传失败', + 'en' => 'File upload failed', + 'hk' => '文件上傳失敗', + 'ja' => 'ファイルのアップロードに失敗しました', + ], + 20507 => [ + 'cn' => '参数格式错误', + 'en' => 'Parameter format error', + 'hk' => '參數格式錯誤', + 'ja' => 'パラメータフォーマットエラー', + ], + 20508 => [ + 'cn' => '请注明审核失败原因', + 'en' => 'Please indicate the reason for the audit failure', + 'hk' => '請註明審核失敗原因', + 'ja' => '監査失敗の理由を示してください', + ], + 20509 => [ + 'cn' => '未找到提现申请记录', + 'en' => 'No withdrawal application record found', + 'hk' => '未找到提現申請記錄', + 'ja' => '引き出し申請記録が見つかりません', + ], + 20510 => [ + 'cn' => '请添加节点地址', + 'en' => 'Please add node address', + 'hk' => '請添加節點地址', + 'ja' => 'ノードアドレスを追加してください', + ], + 20511 => [ + 'cn' => '商品信息错误', + 'en' => 'Product information error', + 'hk' => '商品信息錯誤', + 'ja' => '製品情報エラー', + ], + 20512 => [ + 'cn' => '商品库存不足', + 'en' => 'Insufficient product inventory', + 'hk' => '商品庫存不足', + 'ja' => '不十分な製品在庫', + ], + 20513 => [ + 'cn' => '图片资源不存在', + 'en' => 'Image resource does not exist', + 'hk' => '圖片資源不存在', + 'ja' => '画像リソースが存在しません', + ], + 20514 => [ + 'cn' => '限时抢购商品活动资格不足', + 'en' => 'Insufficient qualifications for limited-time rush-buying activities', + 'hk' => '限時搶購商品活動資格不足', + 'ja' => '期間限定のラッシュバイ活動の資格が不十分', + ], + 20515 => [ + 'cn' => '商品已下架,无法购买', + 'en' => 'The product has been removed and cannot be purchased', + 'hk' => '商品已下架,無法購買', + 'ja' => '製品は削除されており、購入できません', + ], + 20516 => [ + 'cn' => '请先创建订单', + 'en' => 'Please create an order first', + 'hk' => '請先創建訂單', + 'ja' => '最初に注文を作成してください', + ], + + + 20601 => [ + 'cn' => '用户未实名认证', + 'en' => 'User is not authenticated by real name', + 'hk' => '用戶未實名認證', + 'ja' => 'ユーザーは本名で認証されていません', + ], + 20602 => [ + 'cn' => '登录密码错误', + 'en' => 'Incorrect login password', + 'hk' => '登錄密碼錯誤', + 'ja' => 'ログインパスワードが正しくありません', + ], + 20603 => [ + 'cn' => '验证码不正确', + 'en' => 'Incorrect verification code', + 'hk' => '驗證碼不正確', + 'ja' => '不正な確認コード', + ], + 20604 => [ + 'cn' => '邮箱校验失败', + 'en' => 'Email verification failed', + 'hk' => '郵箱校驗失敗', + 'ja' => 'メールの確認に失敗しました', + ], + 20605 => [ + 'cn' => '发送邮箱失败', + 'en' => 'Failed to send mailbox', + 'hk' => '發送郵箱失敗', + 'ja' => 'メール送信に失敗しました', + ], + 20606 => [ + 'cn' => '请勿重复申请', + 'en' => 'Please do not apply twice', + 'hk' => '請勿重複申請', + 'ja' => '二度と応募しないでください', + ], + 20607 => [ + 'cn' => '余额不足', + 'en' => "Not sufficient funds", + 'hk' => '餘額不足', + 'ja' => '残高不足です', + ], + + + //后台管理业务级 20700-20899 + 20700 => [ + 'cn' => '密码错误', + 'en' => 'wrong password', + 'hk' => '密碼錯誤', + 'ja' => '間違ったパスワード', + ], + 20701 => [ + 'cn' => '重复发放', + 'en' => 'Reissue', + 'hk' => '重複發放', + 'ja' => '再発行', + ], + 20702 => [ + 'cn' => '认证信息不存在', + 'en' => 'Authentication information does not exist', + 'hk' => '認證信息不存在', + 'ja' => '認証情報が存在しません', + ], + 20703 => [ + 'cn' => '未配置佣金比例,请联系管理员', + 'en' => 'Commission rate is not configured, please contact the administrator', + 'hk' => '未配置佣金比例,請聯繫管理員', + 'ja' => '手数料率が設定されていません。管理者に連絡してください', + ], + 20704 => [ + 'cn' => '该系列类型未按照日期顺序发放,请检查后重试', + 'en' => 'This series type is not issued in the order of date, please check and try again', + 'hk' => '該系列類型未按照日期順序發放,請檢查後重試', + 'ja' => 'このシリーズタイプは日付順に発行されていませんので、ご確認の上、お試しください。', + ], + 20705 => [ + 'cn' => '该系列类型已经存在相同合约周期', + 'en' => 'The same contract period already exists for this series type', + 'hk' => '該系列類型已經存在相同合約週期', + 'ja' => 'このシリーズタイプには、同じ契約期間がすでに存在します', + ], + 20706 => [ + + 'cn' => '无操作权限', + 'en' => 'No operation authority', + 'hk' => '無操作許可權', + 'ja' => '操作権限がありません', + ], + + 20707 => [ + 'cn' => '用户的质押币余额不足', + 'en' => "The user's staking currency balance is insufficient", + 'hk' => '用戶的質押幣餘額不足', + 'ja' => 'ユーザーの賭け通貨残高が不十分です', + ], + + 20708 => [ + 'cn' => '在线获取质押比例失败,请检查服务网络连接', + 'en' => "Failed to obtain the pledge ratio online, please check the service network connection", + 'hk' => '在線獲取質押比例失敗,請檢查服務網絡連接', + 'ja' => 'オンラインで質権比率を取得できませんでした。サービスネットワーク接続を確認してください。', + ], + + 50000 => [ + 'cn' => '处理失败', + 'en' => 'Processing failed', + 'hk' => '處理失敗', + 'ja' => '処理に失敗しました', + ], +]; + + +function GetErrorMessage(int $code, string $lang = 'cn'): string +{ + $lang = strtolower($lang); + if (!in_array($lang, ['en', 'hk', 'ja', 'cn'])) { + $lang = 'cn'; + } + return RESPONSE_CODE[$code][$lang] ?? '未定义的错误'; +} \ No newline at end of file diff --git a/src/Handles/File/helper.php b/src/Handles/File/helper.php new file mode 100644 index 0000000..070bbca --- /dev/null +++ b/src/Handles/File/helper.php @@ -0,0 +1,131 @@ +header('Accept-Language'); + if (!in_array($lang, ['ja', 'en', 'hk'])) { + $lang = 'cn'; + } + return $lang; +} + +/** + * 失败响应 + * @param int $code + * @return JsonResponse + */ +function fail(int $code): JsonResponse +{ + return response()->json([ + 'code' => $code, + 'message' => GetErrorMessage($code, getLanguage()) + ], 417); +} + +/** + * 正常响应 + * @param Collection $data + * @return JsonResponse + */ +function success(Collection $data): JsonResponse +{ + return response()->json($data); +} + +/** + * 参数响应 + * @param array $errors + * @return JsonResponse + */ +function parameter(array $errors): JsonResponse +{ + return response()->json(['errors' => $errors], 400); +} + +/** + * 错误响应 + * @param int $status + * @param string $error + * @return JsonResponse + */ +function error(int $status, string $error): JsonResponse +{ + return response()->json(['error' => $error], $status); +} + +/** + * 获取订单号 + * @return string + * @throws Exception + */ +function getOrderNumber(): string +{ + $carbon = new Carbon(); + $parse = $carbon::parse('2021-06-01 00:00:00'); + $day = $parse->diffInDays($carbon::now()); + $prefix = str_pad($day, 5, '0', STR_PAD_LEFT); + $pow = pow(10, 10); + $random_int = random_int(0, $pow - 1); + return $prefix . str_pad($random_int, 10, '0', STR_PAD_LEFT); +} + +/** + * 获取毫秒时间戳 + * @return string + */ +function getMicroTime(): string +{ + return bcmul(microtime(true), 1000); +} + +/** + * @param string $ip + * @param string $type + * * @return string|bool + * @note: 获取ip地址信息 + * @throws Exception + * @author: fanete + * @time: 2021/7/28 11:24 下午 + */ +function getIpInfo(string $ip, string $type = 'all'): string|bool +{ + + $ip2region = new Ip2Region(); + try { + $info = $ip2region->btreeSearch($ip); + if ($info) { + $region = explode('|', $info['region']); + return match ($type) { + 'country' => $region[0], + 'province' => $region[2], + 'city' => $region[3], + 'detail' => $region[4], + default => json_encode($region), + }; + } else { + return false; + } + } catch (Exception) { + return false; + } +} + +function printSQL() +{ + DB::listen(function ($query) { + $bindings = $query->bindings; + $sql = $query->sql; + foreach ($bindings as $replace) { + $value = is_numeric($replace) ? $replace : "'" . $replace . "'"; + $sql = preg_replace('/\?/', $value, $sql, 1); + } + dump($sql); + }); +} + + diff --git a/src/Handles/StarPoolCloudHandle.php b/src/Handles/StarPoolCloudHandle.php new file mode 100644 index 0000000..eeef53d --- /dev/null +++ b/src/Handles/StarPoolCloudHandle.php @@ -0,0 +1,227 @@ +Base64UrlEncode(openssl_encrypt($data, 'AES-128-CBC', $key, OPENSSL_CIPHER_AES_128_CBC, $vi)); + } + + /** + * Aes解密 + * @param string $data + * @param string $key + * @param string $vi + * @return string|false + */ + public function AesDecrypt(string $data, string $key, string $vi): string|false + { + return openssl_decrypt($this->Base64UrlDecode($data), 'AES-128-CBC', $key, OPENSSL_CIPHER_AES_128_CBC, $vi); + } + + /** + * @param string $data + * @param bool $strict + * @return string + */ + public function Base64UrlEncode(string $data, bool $strict = false): string + { + if ($strict) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + return str_replace(['+', '/'], ['-', '_'], base64_encode($data)); + } + + /** + * @param string $data + * @param bool $strict + * @return string + */ + public function Base64UrlDecode(string $data, bool $strict = false): string + { + if ($strict) { + return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT)); + } + return base64_decode(str_replace(['-', '_'], ['+', '/'], $data)); + } + + /** + * @param string $url + * @param array $params + * @param array $header + * @return string + */ + public function PostJsonRequest(string $url, array $params, array $header = []): string + { + try { + $response = self::$client->post($url, [ + RequestOptions::JSON => $params, + RequestOptions::HEADERS => $header + ]); + return $response->getBody()->getContents(); + } catch (GuzzleException $exception) { + Log::error("SignatureRequestHttpPostByAes Error", [ + 'msg' => $exception->getMessage() + ]); + return ''; + } + } + + /** + * 签名调用请求 + * + * @param string $url + * @param $productID + * @param string $key + * @param string $vi + * @param array $params + * @return string|false + */ + public function SignatureRequestHttpPostByAes(string $url, $productID, string $key, string $vi, array $params): string|false + { + $timestamp = time(); + $form = array_merge($params, [ + 't' => $timestamp + ]); + ksort($form); + $http_build_query = http_build_query($form); + try { + $signature = $this->AesEncrypt($http_build_query, $key, $vi); + if ($signature === false) { + return false; + } + $response = self::$client->post($url, [ + RequestOptions::FORM_PARAMS => $form, + RequestOptions::HEADERS => [ + 'Signature' => $signature, + 'Signature-Date' => date($this::DATE_FORMAT, $timestamp), + 'Product-ID' => $productID + ] + ]); + } catch (GuzzleException $exception) { + Log::error("SignatureRequestHttpPostByAes Error", [ + 'msg' => $exception->getMessage() + ]); + return false; + } + return $response->getBody()->getContents(); + } + + /** + * 钉钉报警 + * + * @param string $accessToken + * @param string $secret + * @param array $params + * @return bool + */ + public function DingTalkAlert(string $accessToken, string $secret, array $params): bool + { + try { + $timestamp = getMicroTime(); + $signData = $timestamp . "\n" . $secret; + $hash_hmac = hash_hmac('sha256', $signData, $secret, true); + $sign = urlencode(base64_encode($hash_hmac)); + $httpQuery = http_build_query([ + 'access_token' => $accessToken, + 'sign' => $sign, + 'timestamp' => $timestamp, + ]); + $response = self::$client->post('https://oapi.dingtalk.com/robot/send?' . $httpQuery, [ + RequestOptions::JSON => $params, + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json;charset=utf-8' + ] + ]); + $body = json_decode($response->getBody()->getContents(), true); + return ($body['errcode'] ?? false) === 0; + } catch (GuzzleException $exception) { + Log::error("DingTalkAlert Error", [ + 'msg' => $exception->getMessage() + ]); + return false; + } + } + + /** + * 有道翻译 + * + * @param string $appID + * @param string $secret + * @param string $text + * @param string $lang + * @return string + */ + function YouDaoTextTranslator(string $appID, string $secret, string $text, string $lang): string + { + if ($lang === 'hk') { + $lang = 'zh-CHT'; + } + $uuid4 = Uuid::uuid4(); + $uuid = $uuid4->toString(); + $timestamp = (string)now()->utc()->timestamp; + $len = mb_strlen($text); + if ($len > 20) { + $input = mb_substr($text, 0, 10) . $len . mb_substr($text, $len - 10, $len); + $signStr = $appID . $input . $uuid . $timestamp . $secret; + } else { + $signStr = $appID . $text . $uuid . $timestamp . $secret; + } + $sign = hash('sha256', $signStr); + + try { + $response = self::$client->post("https://openapi.youdao.com/api", [ + RequestOptions::FORM_PARAMS => [ + 'q' => $text, + 'from' => 'zh-CHS', + 'to' => $lang, + 'appKey' => $appID, + 'salt' => $uuid, + 'sign' => $sign, + 'signType' => "v3", + 'curtime' => $timestamp, + 'strict' => "true", + ], + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8' + ] + ]); + $body = json_decode($response->getBody()->getContents(), true); + if (($body['errorCode'] ?? false) === '0') { + return $body['translation'][0]; + } + return ""; + } catch (GuzzleException $e) { + Log::error("YouDaoTextTranslator Error", [ + 'msg' => $e->getMessage() + ]); + return ""; + } + } +} \ No newline at end of file diff --git a/src/Handles/Trait/ThrottlesLogins.php b/src/Handles/Trait/ThrottlesLogins.php new file mode 100644 index 0000000..cac8ee7 --- /dev/null +++ b/src/Handles/Trait/ThrottlesLogins.php @@ -0,0 +1,124 @@ +limiter()->tooManyAttempts( + $this->throttleKey($request), $this->maxAttempts() + ); + } + + /** + * Increment the login attempts for the user. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function incrementLoginAttempts(Request $request) + { + $this->limiter()->hit( + $this->throttleKey($request), $this->decayMinutes() * 60 + ); + } + + /** + * Redirect the user after determining they are locked out. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function sendLockoutResponse(Request $request) + { + $seconds = $this->limiter()->availableIn( + $this->throttleKey($request) + ); + + throw ValidationException::withMessages([ + $this->username() => [trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ])], + ])->status(Response::HTTP_TOO_MANY_REQUESTS); + } + + /** + * Clear the login locks for the given user credentials. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function clearLoginAttempts(Request $request) + { + $this->limiter()->clear($this->throttleKey($request)); + } + + /** + * Fire an event when a lockout occurs. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function fireLockoutEvent(Request $request) + { + event(new Lockout($request)); + } + + /** + * Get the throttle key for the given request. + * + * @param \Illuminate\Http\Request $request + * @return string + */ + protected function throttleKey(Request $request) + { + return Str::lower($request->input($this->username())) . '|' . $request->ip(); + } + + /** + * Get the rate limiter instance. + * + * @return \Illuminate\Cache\RateLimiter + */ + protected function limiter() + { + return app(RateLimiter::class); + } + + /** + * Get the maximum number of attempts to allow. + * + * @return int + */ + public function maxAttempts() + { + return property_exists($this, 'maxAttempts') ? $this->maxAttempts : 5; + } + + /** + * Get the number of minutes to throttle for. + * + * @return int + */ + public function decayMinutes() + { + return property_exists($this, 'decayMinutes') ? $this->decayMinutes : 1; + } +} diff --git a/src/Providers/StarPoolCloudServiceProvider.php b/src/Providers/StarPoolCloudServiceProvider.php new file mode 100644 index 0000000..9c6765a --- /dev/null +++ b/src/Providers/StarPoolCloudServiceProvider.php @@ -0,0 +1,45 @@ +publishes([ + realpath(__DIR__ . '/../../config/apollo.php') => config_path('apollo.php') + ]); + if ($this->app->runningInConsole()) { + $this->commands([ + ApolloConfigSync::class + ]); + } + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->app->singleton('StarPoolCloud', function () { + return new StarPoolCloudHandle(); + }); + $this->app->singleton('StarPoolCloudApollo', function () { + return new Client(config('apollo')); + }); + } +} \ No newline at end of file