validateRequiredFields($data); // 生成应用ID和密钥 $data['app_id'] = OpenApiApp::generateAppId(); $data['app_secret'] = OpenApiApp::generateAppSecret(); // 设置默认值 $data['status'] = $data['status'] ?? config('openapi.app.default_status', APP_STATUS::PENDING->value); $data['auth_type'] = $data['auth_type'] ?? config('openapi.auth.default_type', AUTH_TYPE::API_KEY->value); $data['scopes'] = $data['scopes'] ?? config('openapi.scopes.default', []); // 设置过期时间 if (!isset($data['expires_at'])) { $expireDays = config('openapi.app.expire_days', 365); $data['expires_at'] = now()->addDays($expireDays); } // 加密应用密钥 if (config('openapi.security.encrypt_secrets', true)) { $data['app_secret'] = encrypt($data['app_secret']); } // 创建应用 $app = OpenApiApp::create($data); // 触发应用创建事件 Event::dispatch(new AppCreatedEvent($app)); return $app; } /** * 验证必填字段 * * @param array $data * @throws \InvalidArgumentException */ protected function validateRequiredFields(array $data): void { $requiredFields = config('openapi.app.required_fields', ['name', 'description', 'callback_url']); foreach ($requiredFields as $field) { if (empty($data[$field])) { throw new \InvalidArgumentException("字段 {$field} 是必填的"); } } } /** * 审核应用 * * @param int $appId * @param bool $approved * @param string $note * @param int $approvedBy * @return OpenApiApp */ public function approveApp(int $appId, bool $approved, string $note = '', int $approvedBy = null): OpenApiApp { $app = OpenApiApp::findOrFail($appId); if ($app->status !== APP_STATUS::PENDING->value) { throw new \InvalidArgumentException('只能审核待审核状态的应用'); } $app->update([ 'status' => $approved ? APP_STATUS::APPROVED->value : APP_STATUS::REJECTED->value, 'approved_at' => now(), 'approved_by' => $approvedBy, 'approved_note' => $note, ]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 激活应用 * * @param int $appId * @return OpenApiApp */ public function activateApp(int $appId): OpenApiApp { $app = OpenApiApp::findOrFail($appId); if (!in_array($app->status, [APP_STATUS::APPROVED->value, APP_STATUS::SUSPENDED->value])) { throw new \InvalidArgumentException('只能激活已审核或已暂停的应用'); } $app->update(['status' => APP_STATUS::ACTIVE->value]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 暂停应用 * * @param int $appId * @param string $reason * @return OpenApiApp */ public function suspendApp(int $appId, string $reason = ''): OpenApiApp { $app = OpenApiApp::findOrFail($appId); if ($app->status !== APP_STATUS::ACTIVE->value) { throw new \InvalidArgumentException('只能暂停激活状态的应用'); } $app->update([ 'status' => APP_STATUS::SUSPENDED->value, 'approved_note' => $reason, ]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 禁用应用 * * @param int $appId * @param string $reason * @return OpenApiApp */ public function disableApp(int $appId, string $reason = ''): OpenApiApp { $app = OpenApiApp::findOrFail($appId); $app->update([ 'status' => APP_STATUS::DISABLED->value, 'approved_note' => $reason, ]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 重新生成应用密钥 * * @param int $appId * @return OpenApiApp */ public function regenerateSecret(int $appId): OpenApiApp { $app = OpenApiApp::findOrFail($appId); if (!config('openapi.api_key.allow_regenerate', true)) { throw new \InvalidArgumentException('不允许重新生成密钥'); } $newSecret = OpenApiApp::generateAppSecret(); // 加密应用密钥 if (config('openapi.security.encrypt_secrets', true)) { $newSecret = encrypt($newSecret); } $app->update(['app_secret' => $newSecret]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 更新应用权限 * * @param int $appId * @param array $scopes * @return OpenApiApp */ public function updateScopes(int $appId, array $scopes): OpenApiApp { $app = OpenApiApp::findOrFail($appId); // 验证权限范围 $this->validateScopes($scopes); $app->update(['scopes' => $scopes]); // 清除缓存 $this->clearAppCache($app->app_id); return $app; } /** * 验证权限范围 * * @param array $scopes * @throws \InvalidArgumentException */ protected function validateScopes(array $scopes): void { $validScopes = array_column(SCOPE_TYPE::cases(), 'value'); $validScopes[] = '*'; // 允许通配符 foreach ($scopes as $scope) { if (!in_array($scope, $validScopes)) { throw new \InvalidArgumentException("无效的权限范围: {$scope}"); } } } /** * 通过应用ID获取应用信息 * * @param string $appId * @return OpenApiApp|null */ public function getAppByAppId(string $appId): ?OpenApiApp { $cacheKey = config('openapi.cache.keys.app_info'); $cacheKey = str_replace('{app_id}', $appId, $cacheKey); return Cache::remember($cacheKey, config('openapi.cache.default_ttl', 300), function () use ($appId) { return OpenApiApp::where('app_id', $appId)->first(); }); } /** * 验证应用认证 * * @param string $appId * @param string $appSecret * @return OpenApiApp|null */ public function validateApp(string $appId, string $appSecret): ?OpenApiApp { $app = $this->getAppByAppId($appId); if (!$app) { return null; } // 检查应用状态 if (!$app->can_call_api) { return null; } // 检查应用是否过期 if ($app->is_expired) { return null; } // 验证密钥 $storedSecret = $app->app_secret; if (config('openapi.security.encrypt_secrets', true)) { $storedSecret = decrypt($storedSecret); } if (!hash_equals($storedSecret, $appSecret)) { return null; } return $app; } /** * 检查应用权限 * * @param OpenApiApp $app * @param string $scope * @return bool */ public function checkScope(OpenApiApp $app, string $scope): bool { return $app->hasScope($scope); } /** * 检查IP白名单 * * @param OpenApiApp $app * @param string $ip * @return bool */ public function checkIpWhitelist(OpenApiApp $app, string $ip): bool { return $app->isIpAllowed($ip); } /** * 更新应用最后使用时间 * * @param OpenApiApp $app * @return void */ public function updateLastUsed(OpenApiApp $app): void { $app->updateLastUsed(); } /** * 获取应用统计信息 * * @param string $appId * @param string $period * @return array */ public function getAppStats(string $appId, string $period = 'day'): array { $startTime = $this->getStatsPeriodStart($period); // 获取基础统计 $stats = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId) ->where('created_at', '>=', $startTime) ->selectRaw(' SUM(request_count) as total_requests, SUM(success_count) as total_success, SUM(error_count) as total_errors, AVG(avg_response_time) as avg_response_time, MAX(max_response_time) as max_response_time, MIN(min_response_time) as min_response_time, SUM(rate_limit_hits) as total_rate_limit_hits, SUM(unique_ips) as total_unique_ips ') ->first(); $successRate = $stats->total_requests > 0 ? round(($stats->total_success / $stats->total_requests) * 100, 2) : 0; $errorRate = $stats->total_requests > 0 ? round(($stats->total_errors / $stats->total_requests) * 100, 2) : 0; return [ 'period' => $period, 'start_time' => $startTime, 'total_requests' => $stats->total_requests ?? 0, 'total_success' => $stats->total_success ?? 0, 'total_errors' => $stats->total_errors ?? 0, 'success_rate' => $successRate, 'error_rate' => $errorRate, 'avg_response_time' => round($stats->avg_response_time ?? 0, 2), 'max_response_time' => $stats->max_response_time ?? 0, 'min_response_time' => $stats->min_response_time ?? 0, 'total_rate_limit_hits' => $stats->total_rate_limit_hits ?? 0, 'total_unique_ips' => $stats->total_unique_ips ?? 0, ]; } /** * 清除应用缓存 * * @param string $appId * @return void */ protected function clearAppCache(string $appId): void { $cacheKey = config('openapi.cache.keys.app_info'); $cacheKey = str_replace('{app_id}', $appId, $cacheKey); Cache::forget($cacheKey); } /** * 批量处理过期应用 * * @return int */ public function handleExpiredApps(): int { $expiredApps = OpenApiApp::where('expires_at', '<', now()) ->where('status', '!=', APP_STATUS::EXPIRED->value) ->get(); $count = 0; foreach ($expiredApps as $app) { $app->update(['status' => APP_STATUS::EXPIRED->value]); $this->clearAppCache($app->app_id); $count++; } return $count; } /** * 获取用户的应用列表 * * @param int $userId * @return \Illuminate\Database\Eloquent\Collection */ public function getUserApps(int $userId) { return OpenApiApp::byUser($userId)->orderBy('created_at', 'desc')->get(); } /** * 检查用户是否可以创建更多应用 * * @param int $userId * @return bool */ public function canCreateMoreApps(int $userId): bool { $maxApps = config('openapi.app.max_apps_per_user', 10); $currentCount = OpenApiApp::byUser($userId)->count(); return $currentCount < $maxApps; } /** * 更新应用信息 * * @param int $appId * @param array $data * @return OpenApiApp */ public function updateApp(int $appId, array $data): OpenApiApp { $app = OpenApiApp::findOrFail($appId); // 过滤允许更新的字段 $allowedFields = ['name', 'description', 'website', 'logo', 'callback_url', 'contact_email']; $updateData = array_intersect_key($data, array_flip($allowedFields)); $app->update($updateData); // 清除缓存 $this->clearAppCache($app->app_id); return $app->fresh(); } /** * 重新生成应用密钥 * * @param int $appId * @return string */ public function regenerateAppSecret(int $appId): string { $app = OpenApiApp::findOrFail($appId); $newSecret = OpenApiApp::generateAppSecret(); $app->update(['app_secret' => $newSecret]); // 清除缓存 $this->clearAppCache($app->app_id); return $newSecret; } /** * 获取应用配额信息 * * @param string $appId * @return array */ public function getAppQuota(string $appId): array { $app = OpenApiApp::where('app_id', $appId)->firstOrFail(); // 获取今日使用量 $todayUsage = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId) ->where('date', now()->toDateString()) ->sum('request_count'); // 获取本月使用量 $monthUsage = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId) ->whereBetween('date', [ now()->startOfMonth()->toDateString(), now()->endOfMonth()->toDateString() ]) ->sum('request_count'); // 获取配额限制 $rateLimits = $app->rate_limits ?? []; $dailyLimit = $rateLimits['requests_per_day'] ?? 0; $monthlyLimit = $rateLimits['requests_per_month'] ?? 0; return [ 'today' => [ 'used' => $todayUsage, 'limit' => $dailyLimit, 'remaining' => max(0, $dailyLimit - $todayUsage), 'percentage' => $dailyLimit > 0 ? round(($todayUsage / $dailyLimit) * 100, 2) : 0, ], 'month' => [ 'used' => $monthUsage, 'limit' => $monthlyLimit, 'remaining' => max(0, $monthlyLimit - $monthUsage), 'percentage' => $monthlyLimit > 0 ? round(($monthUsage / $monthlyLimit) * 100, 2) : 0, ], ]; } /** * 获取统计周期的开始时间 * * @param string $period * @return \Carbon\Carbon */ protected function getStatsPeriodStart(string $period): \Carbon\Carbon { return match ($period) { 'hour' => now()->subHour(), 'day' => now()->subDay(), 'week' => now()->subWeek(), 'month' => now()->subMonth(), default => now()->subDay(), }; } }