|
|
@@ -0,0 +1,438 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Module\ThirdParty\Services;
|
|
|
+
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyService as ServiceModel;
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyCredential;
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyLog;
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyQuota;
|
|
|
+use App\Module\ThirdParty\Enums\SERVICE_STATUS;
|
|
|
+use App\Module\ThirdParty\Enums\LOG_LEVEL;
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
+use Illuminate\Support\Facades\Cache;
|
|
|
+use Illuminate\Http\Client\Response;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 第三方服务核心服务类
|
|
|
+ */
|
|
|
+class ThirdPartyService
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 注册新的第三方服务
|
|
|
+ *
|
|
|
+ * @param array $data
|
|
|
+ * @return ServiceModel
|
|
|
+ */
|
|
|
+ public static function registerService(array $data): ServiceModel
|
|
|
+ {
|
|
|
+ // 生成服务代码
|
|
|
+ if (empty($data['code'])) {
|
|
|
+ $data['code'] = ServiceModel::generateCode($data['name'], $data['provider']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置默认值
|
|
|
+ $data = array_merge([
|
|
|
+ 'version' => '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;
|
|
|
+ }
|
|
|
+}
|