ThirdPartyService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <?php
  2. namespace App\Module\ThirdParty\Services;
  3. use App\Module\ThirdParty\Models\ThirdPartyService as ServiceModel;
  4. use App\Module\ThirdParty\Models\ThirdPartyCredential;
  5. use App\Module\ThirdParty\Models\ThirdPartyLog;
  6. use App\Module\ThirdParty\Models\ThirdPartyQuota;
  7. use App\Module\ThirdParty\Enums\SERVICE_STATUS;
  8. use App\Module\ThirdParty\Enums\LOG_LEVEL;
  9. use Illuminate\Support\Facades\Http;
  10. use Illuminate\Support\Facades\Cache;
  11. use Illuminate\Http\Client\Response;
  12. /**
  13. * 第三方服务核心服务类
  14. */
  15. class ThirdPartyService
  16. {
  17. /**
  18. * 注册新的第三方服务
  19. *
  20. * @param array $data
  21. * @return ServiceModel
  22. */
  23. public static function registerService(array $data): ServiceModel
  24. {
  25. // 生成服务代码
  26. if (empty($data['code'])) {
  27. $data['code'] = ServiceModel::generateCode($data['name'], $data['provider']);
  28. }
  29. // 设置默认值
  30. $data = array_merge([
  31. 'version' => 'v1',
  32. 'status' => config('thirdparty.defaults.service_status', 'INACTIVE'),
  33. 'auth_type' => config('thirdparty.defaults.auth_type', 'API_KEY'),
  34. 'timeout' => config('thirdparty.defaults.timeout', 30),
  35. 'retry_times' => config('thirdparty.defaults.retry_times', 3),
  36. 'retry_delay' => config('thirdparty.defaults.retry_delay', 1000),
  37. 'health_check_interval' => config('thirdparty.defaults.health_check_interval', 300),
  38. 'priority' => 0,
  39. ], $data);
  40. return ServiceModel::create($data);
  41. }
  42. /**
  43. * 调用第三方API
  44. *
  45. * @param string $serviceCode 服务代码
  46. * @param string $endpoint API端点
  47. * @param array $data 请求数据
  48. * @param string $method HTTP方法
  49. * @param array $options 额外选项
  50. * @return array
  51. * @throws \Exception
  52. */
  53. public static function callApi(
  54. string $serviceCode,
  55. string $endpoint,
  56. array $data = [],
  57. string $method = 'POST',
  58. array $options = []
  59. ): array {
  60. $service = static::getServiceByCode($serviceCode);
  61. if (!$service) {
  62. throw new \Exception("服务 {$serviceCode} 不存在");
  63. }
  64. if (!$service->canCallApi()) {
  65. throw new \Exception("服务 {$serviceCode} 当前不可用,状态:{$service->getStatusLabel()}");
  66. }
  67. // 检查配额
  68. if (!static::checkQuota($service)) {
  69. throw new \Exception("服务 {$serviceCode} 配额已用完");
  70. }
  71. // 获取凭证
  72. $credential = $service->getActiveCredential($options['environment'] ?? 'production');
  73. if (!$credential) {
  74. throw new \Exception("服务 {$serviceCode} 没有可用的认证凭证");
  75. }
  76. $requestId = ThirdPartyLog::generateRequestId();
  77. $startTime = microtime(true);
  78. try {
  79. // 构建请求
  80. $url = $service->getApiUrl($endpoint);
  81. $headers = array_merge($service->getDefaultHeaders(), $credential->generateAuthHeaders());
  82. $params = array_merge($service->getDefaultParams(), $data);
  83. // 发送请求
  84. $response = static::sendRequest($service, $url, $method, $params, $headers, $options);
  85. $responseTime = (int)((microtime(true) - $startTime) * 1000);
  86. // 记录日志
  87. static::logApiCall($service, $credential, $requestId, $method, $url, $headers, $params, $response, $responseTime);
  88. // 更新配额
  89. static::updateQuota($service);
  90. // 更新凭证使用统计
  91. $credential->updateUsageStats();
  92. return [
  93. 'success' => true,
  94. 'data' => $response->json(),
  95. 'status_code' => $response->status(),
  96. 'response_time' => $responseTime,
  97. 'request_id' => $requestId,
  98. ];
  99. } catch (\Exception $e) {
  100. $responseTime = (int)((microtime(true) - $startTime) * 1000);
  101. // 记录错误日志
  102. static::logApiError($service, $credential, $requestId, $method, $url, $headers, $params, $e, $responseTime);
  103. throw $e;
  104. }
  105. }
  106. /**
  107. * 获取服务列表
  108. *
  109. * @param array $filters
  110. * @return \Illuminate\Database\Eloquent\Collection
  111. */
  112. public static function getServices(array $filters = [])
  113. {
  114. $query = ServiceModel::query();
  115. if (isset($filters['type'])) {
  116. $query->where('type', $filters['type']);
  117. }
  118. if (isset($filters['status'])) {
  119. $query->where('status', $filters['status']);
  120. }
  121. if (isset($filters['provider'])) {
  122. $query->where('provider', $filters['provider']);
  123. }
  124. return $query->orderBy('priority')->orderBy('name')->get();
  125. }
  126. /**
  127. * 根据代码获取服务
  128. *
  129. * @param string $code
  130. * @return ServiceModel|null
  131. */
  132. public static function getServiceByCode(string $code): ?ServiceModel
  133. {
  134. $cacheKey = "thirdparty:service:{$code}";
  135. return Cache::remember($cacheKey, config('thirdparty.cache.ttl', 3600), function () use ($code) {
  136. return ServiceModel::where('code', $code)->first();
  137. });
  138. }
  139. /**
  140. * 更新服务状态
  141. *
  142. * @param string $serviceCode
  143. * @param string $status
  144. * @return bool
  145. */
  146. public static function updateServiceStatus(string $serviceCode, string $status): bool
  147. {
  148. $service = static::getServiceByCode($serviceCode);
  149. if (!$service) {
  150. return false;
  151. }
  152. $currentStatus = $service->getServiceStatusEnum();
  153. $newStatus = SERVICE_STATUS::from($status);
  154. // 检查状态转换是否允许
  155. if (!$currentStatus->canTransitionTo($newStatus)) {
  156. throw new \Exception("不能从状态 {$currentStatus->getLabel()} 转换到 {$newStatus->getLabel()}");
  157. }
  158. $result = $service->update(['status' => $status]);
  159. // 清除缓存
  160. Cache::forget("thirdparty:service:{$serviceCode}");
  161. return $result;
  162. }
  163. /**
  164. * 检查服务配额
  165. *
  166. * @param ServiceModel $service
  167. * @param int $amount
  168. * @return bool
  169. */
  170. protected static function checkQuota(ServiceModel $service, int $amount = 1): bool
  171. {
  172. if (!config('thirdparty.quota.enabled', true)) {
  173. return true;
  174. }
  175. $quotas = $service->quotas()->active()->get();
  176. foreach ($quotas as $quota) {
  177. if (!$quota->canUse($amount)) {
  178. return false;
  179. }
  180. }
  181. return true;
  182. }
  183. /**
  184. * 更新服务配额
  185. *
  186. * @param ServiceModel $service
  187. * @param int $amount
  188. * @return void
  189. */
  190. protected static function updateQuota(ServiceModel $service, int $amount = 1): void
  191. {
  192. if (!config('thirdparty.quota.enabled', true)) {
  193. return;
  194. }
  195. $quotas = $service->quotas()->active()->get();
  196. foreach ($quotas as $quota) {
  197. $quota->incrementUsage($amount);
  198. }
  199. }
  200. /**
  201. * 发送HTTP请求
  202. *
  203. * @param ServiceModel $service
  204. * @param string $url
  205. * @param string $method
  206. * @param array $data
  207. * @param array $headers
  208. * @param array $options
  209. * @return Response
  210. */
  211. protected static function sendRequest(
  212. ServiceModel $service,
  213. string $url,
  214. string $method,
  215. array $data,
  216. array $headers,
  217. array $options
  218. ): Response {
  219. $httpOptions = [
  220. 'timeout' => $options['timeout'] ?? $service->timeout,
  221. 'headers' => $headers,
  222. ];
  223. // 配置重试
  224. $retryTimes = $options['retry_times'] ?? $service->retry_times;
  225. $retryDelay = $options['retry_delay'] ?? $service->retry_delay;
  226. $http = Http::withOptions($httpOptions);
  227. if ($retryTimes > 0) {
  228. $http = $http->retry($retryTimes, $retryDelay, function ($exception, $request) {
  229. // 只在特定错误时重试
  230. if ($exception instanceof \Illuminate\Http\Client\ConnectionException) {
  231. return true;
  232. }
  233. if ($exception instanceof \Illuminate\Http\Client\RequestException) {
  234. $status = $exception->response->status();
  235. return in_array($status, config('thirdparty.retry.retry_on_status', [429, 500, 502, 503, 504]));
  236. }
  237. return false;
  238. });
  239. }
  240. // 发送请求
  241. return match (strtoupper($method)) {
  242. 'GET' => $http->get($url, $data),
  243. 'POST' => $http->post($url, $data),
  244. 'PUT' => $http->put($url, $data),
  245. 'PATCH' => $http->patch($url, $data),
  246. 'DELETE' => $http->delete($url, $data),
  247. default => throw new \Exception("不支持的HTTP方法: {$method}"),
  248. };
  249. }
  250. /**
  251. * 记录API调用日志
  252. *
  253. * @param ServiceModel $service
  254. * @param ThirdPartyCredential $credential
  255. * @param string $requestId
  256. * @param string $method
  257. * @param string $url
  258. * @param array $headers
  259. * @param array $params
  260. * @param Response $response
  261. * @param int $responseTime
  262. * @return void
  263. */
  264. protected static function logApiCall(
  265. ServiceModel $service,
  266. ThirdPartyCredential $credential,
  267. string $requestId,
  268. string $method,
  269. string $url,
  270. array $headers,
  271. array $params,
  272. Response $response,
  273. int $responseTime
  274. ): void {
  275. if (!config('thirdparty.logging.enabled', true)) {
  276. return;
  277. }
  278. // 过滤敏感信息
  279. $filteredHeaders = static::filterSensitiveData($headers);
  280. $filteredParams = static::filterSensitiveData($params);
  281. ThirdPartyLog::createLog([
  282. 'service_id' => $service->id,
  283. 'credential_id' => $credential->id,
  284. 'request_id' => $requestId,
  285. 'method' => $method,
  286. 'url' => $url,
  287. 'headers' => $filteredHeaders,
  288. 'params' => $filteredParams,
  289. 'body' => json_encode($filteredParams),
  290. 'response_status' => $response->status(),
  291. 'response_headers' => $response->headers(),
  292. 'response_body' => static::truncateResponseBody($response->body()),
  293. 'response_time' => $responseTime,
  294. 'level' => $response->successful() ? LOG_LEVEL::INFO->value : LOG_LEVEL::ERROR->value,
  295. 'user_id' => auth()->id(),
  296. 'ip_address' => request()->ip(),
  297. 'user_agent' => request()->userAgent(),
  298. ]);
  299. }
  300. /**
  301. * 记录API错误日志
  302. *
  303. * @param ServiceModel $service
  304. * @param ThirdPartyCredential $credential
  305. * @param string $requestId
  306. * @param string $method
  307. * @param string $url
  308. * @param array $headers
  309. * @param array $params
  310. * @param \Exception $exception
  311. * @param int $responseTime
  312. * @return void
  313. */
  314. protected static function logApiError(
  315. ServiceModel $service,
  316. ThirdPartyCredential $credential,
  317. string $requestId,
  318. string $method,
  319. string $url,
  320. array $headers,
  321. array $params,
  322. \Exception $exception,
  323. int $responseTime
  324. ): void {
  325. if (!config('thirdparty.logging.enabled', true)) {
  326. return;
  327. }
  328. $filteredHeaders = static::filterSensitiveData($headers);
  329. $filteredParams = static::filterSensitiveData($params);
  330. ThirdPartyLog::createLog([
  331. 'service_id' => $service->id,
  332. 'credential_id' => $credential->id,
  333. 'request_id' => $requestId,
  334. 'method' => $method,
  335. 'url' => $url,
  336. 'headers' => $filteredHeaders,
  337. 'params' => $filteredParams,
  338. 'body' => json_encode($filteredParams),
  339. 'response_time' => $responseTime,
  340. 'error_message' => $exception->getMessage(),
  341. 'level' => LOG_LEVEL::ERROR->value,
  342. 'user_id' => auth()->id(),
  343. 'ip_address' => request()->ip(),
  344. 'user_agent' => request()->userAgent(),
  345. ]);
  346. }
  347. /**
  348. * 过滤敏感数据
  349. *
  350. * @param array $data
  351. * @return array
  352. */
  353. protected static function filterSensitiveData(array $data): array
  354. {
  355. $sensitiveFields = config('thirdparty.logging.sensitive_fields', []);
  356. foreach ($sensitiveFields as $field) {
  357. if (isset($data[$field])) {
  358. $data[$field] = '***';
  359. }
  360. }
  361. return $data;
  362. }
  363. /**
  364. * 截断响应体
  365. *
  366. * @param string $body
  367. * @return string
  368. */
  369. protected static function truncateResponseBody(string $body): string
  370. {
  371. $maxSize = config('thirdparty.logging.max_body_size', 10240);
  372. if (strlen($body) > $maxSize) {
  373. return substr($body, 0, $maxSize) . '... [truncated]';
  374. }
  375. return $body;
  376. }
  377. }