'v1', 'status' => config('thirdparty.defaults.service_status', 'INACTIVE'), 'auth_type' => config('thirdparty.defaults.auth_type', 'API_KEY'), 'timeout' => config('thirdparty.defaults.timeout', 30), 'retry_times' => config('thirdparty.defaults.retry_times', 3), 'retry_delay' => config('thirdparty.defaults.retry_delay', 1000), 'health_check_interval' => config('thirdparty.defaults.health_check_interval', 300), 'priority' => 0, ], $data); return ServiceModel::create($data); } /** * 调用第三方API * * @param string $serviceCode 服务代码 * @param string $endpoint API端点 * @param array $data 请求数据 * @param string $method HTTP方法 * @param array $options 额外选项 * @return array * @throws \Exception */ public static function callApi( string $serviceCode, string $endpoint, array $data = [], string $method = 'POST', array $options = [] ): array { $service = static::getServiceByCode($serviceCode); if (!$service) { throw new \Exception("服务 {$serviceCode} 不存在"); } if (!$service->canCallApi()) { throw new \Exception("服务 {$serviceCode} 当前不可用,状态:{$service->getStatusLabel()}"); } // 检查配额 if (!static::checkQuota($service)) { throw new \Exception("服务 {$serviceCode} 配额已用完"); } // 获取凭证 $credential = $service->getActiveCredential($options['environment'] ?? 'production'); if (!$credential) { throw new \Exception("服务 {$serviceCode} 没有可用的认证凭证"); } $requestId = ThirdPartyLog::generateRequestId(); $startTime = microtime(true); try { // 构建请求 $url = $service->getApiUrl($endpoint); $headers = array_merge($service->getDefaultHeaders(), $credential->generateAuthHeaders()); $params = array_merge($service->getDefaultParams(), $data); // 发送请求 $response = static::sendRequest($service, $url, $method, $params, $headers, $options); $responseTime = (int)((microtime(true) - $startTime) * 1000); // 记录日志 static::logApiCall($service, $credential, $requestId, $method, $url, $headers, $params, $response, $responseTime); // 更新配额 static::updateQuota($service); // 更新凭证使用统计 $credential->updateUsageStats(); return [ 'success' => true, 'data' => $response->json(), 'status_code' => $response->status(), 'response_time' => $responseTime, 'request_id' => $requestId, ]; } catch (\Exception $e) { $responseTime = (int)((microtime(true) - $startTime) * 1000); // 记录错误日志 static::logApiError($service, $credential, $requestId, $method, $url, $headers, $params, $e, $responseTime); throw $e; } } /** * 获取服务列表 * * @param array $filters * @return \Illuminate\Database\Eloquent\Collection */ public static function getServices(array $filters = []) { $query = ServiceModel::query(); if (isset($filters['type'])) { $query->where('type', $filters['type']); } if (isset($filters['status'])) { $query->where('status', $filters['status']); } if (isset($filters['provider'])) { $query->where('provider', $filters['provider']); } return $query->orderBy('priority')->orderBy('name')->get(); } /** * 根据代码获取服务 * * @param string $code * @return ServiceModel|null */ public static function getServiceByCode(string $code): ?ServiceModel { $cacheKey = "thirdparty:service:{$code}"; return Cache::remember($cacheKey, config('thirdparty.cache.ttl', 3600), function () use ($code) { return ServiceModel::where('code', $code)->first(); }); } /** * 更新服务状态 * * @param string $serviceCode * @param string $status * @return bool */ public static function updateServiceStatus(string $serviceCode, string $status): bool { $service = static::getServiceByCode($serviceCode); if (!$service) { return false; } $currentStatus = $service->getServiceStatusEnum(); $newStatus = SERVICE_STATUS::from($status); // 检查状态转换是否允许 if (!$currentStatus->canTransitionTo($newStatus)) { throw new \Exception("不能从状态 {$currentStatus->getLabel()} 转换到 {$newStatus->getLabel()}"); } $result = $service->update(['status' => $status]); // 清除缓存 Cache::forget("thirdparty:service:{$serviceCode}"); return $result; } /** * 检查服务配额 * * @param ServiceModel $service * @param int $amount * @return bool */ protected static function checkQuota(ServiceModel $service, int $amount = 1): bool { if (!config('thirdparty.quota.enabled', true)) { return true; } $quotas = $service->quotas()->active()->get(); foreach ($quotas as $quota) { if (!$quota->canUse($amount)) { return false; } } return true; } /** * 更新服务配额 * * @param ServiceModel $service * @param int $amount * @return void */ protected static function updateQuota(ServiceModel $service, int $amount = 1): void { if (!config('thirdparty.quota.enabled', true)) { return; } $quotas = $service->quotas()->active()->get(); foreach ($quotas as $quota) { $quota->incrementUsage($amount); } } /** * 发送HTTP请求 * * @param ServiceModel $service * @param string $url * @param string $method * @param array $data * @param array $headers * @param array $options * @return Response */ protected static function sendRequest( ServiceModel $service, string $url, string $method, array $data, array $headers, array $options ): Response { $httpOptions = [ 'timeout' => $options['timeout'] ?? $service->timeout, 'headers' => $headers, ]; // 配置重试 $retryTimes = $options['retry_times'] ?? $service->retry_times; $retryDelay = $options['retry_delay'] ?? $service->retry_delay; $http = Http::withOptions($httpOptions); if ($retryTimes > 0) { $http = $http->retry($retryTimes, $retryDelay, function ($exception, $request) { // 只在特定错误时重试 if ($exception instanceof \Illuminate\Http\Client\ConnectionException) { return true; } if ($exception instanceof \Illuminate\Http\Client\RequestException) { $status = $exception->response->status(); return in_array($status, config('thirdparty.retry.retry_on_status', [429, 500, 502, 503, 504])); } return false; }); } // 发送请求 return match (strtoupper($method)) { 'GET' => $http->get($url, $data), 'POST' => $http->post($url, $data), 'PUT' => $http->put($url, $data), 'PATCH' => $http->patch($url, $data), 'DELETE' => $http->delete($url, $data), default => throw new \Exception("不支持的HTTP方法: {$method}"), }; } /** * 记录API调用日志 * * @param ServiceModel $service * @param ThirdPartyCredential $credential * @param string $requestId * @param string $method * @param string $url * @param array $headers * @param array $params * @param Response $response * @param int $responseTime * @return void */ protected static function logApiCall( ServiceModel $service, ThirdPartyCredential $credential, string $requestId, string $method, string $url, array $headers, array $params, Response $response, int $responseTime ): void { if (!config('thirdparty.logging.enabled', true)) { return; } // 过滤敏感信息 $filteredHeaders = static::filterSensitiveData($headers); $filteredParams = static::filterSensitiveData($params); ThirdPartyLog::createLog([ 'service_id' => $service->id, 'credential_id' => $credential->id, 'request_id' => $requestId, 'method' => $method, 'url' => $url, 'headers' => $filteredHeaders, 'params' => $filteredParams, 'body' => json_encode($filteredParams), 'response_status' => $response->status(), 'response_headers' => $response->headers(), 'response_body' => static::truncateResponseBody($response->body()), 'response_time' => $responseTime, 'level' => $response->successful() ? LOG_LEVEL::INFO->value : LOG_LEVEL::ERROR->value, 'user_id' => auth()->id(), 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), ]); } /** * 记录API错误日志 * * @param ServiceModel $service * @param ThirdPartyCredential $credential * @param string $requestId * @param string $method * @param string $url * @param array $headers * @param array $params * @param \Exception $exception * @param int $responseTime * @return void */ protected static function logApiError( ServiceModel $service, ThirdPartyCredential $credential, string $requestId, string $method, string $url, array $headers, array $params, \Exception $exception, int $responseTime ): void { if (!config('thirdparty.logging.enabled', true)) { return; } $filteredHeaders = static::filterSensitiveData($headers); $filteredParams = static::filterSensitiveData($params); ThirdPartyLog::createLog([ 'service_id' => $service->id, 'credential_id' => $credential->id, 'request_id' => $requestId, 'method' => $method, 'url' => $url, 'headers' => $filteredHeaders, 'params' => $filteredParams, 'body' => json_encode($filteredParams), 'response_time' => $responseTime, 'error_message' => $exception->getMessage(), 'level' => LOG_LEVEL::ERROR->value, 'user_id' => auth()->id(), 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), ]); } /** * 过滤敏感数据 * * @param array $data * @return array */ protected static function filterSensitiveData(array $data): array { $sensitiveFields = config('thirdparty.logging.sensitive_fields', []); foreach ($sensitiveFields as $field) { if (isset($data[$field])) { $data[$field] = '***'; } } return $data; } /** * 截断响应体 * * @param string $body * @return string */ protected static function truncateResponseBody(string $body): string { $maxSize = config('thirdparty.logging.max_body_size', 10240); if (strlen($body) > $maxSize) { return substr($body, 0, $maxSize) . '... [truncated]'; } return $body; } }