app_id; $data['status'] = $data['status'] ?? 'ACTIVE'; $data['secret'] = $data['secret'] ?? $this->generateWebhookSecret(); return OpenApiWebhook::create($data); } /** * 发送Webhook通知 * * @param string $appId * @param string $event * @param array $payload * @param bool $async 是否异步发送 * @return bool */ public function sendWebhook(string $appId, string $event, array $payload, bool $async = true): bool { $webhooks = $this->getActiveWebhooks($appId, $event); if (empty($webhooks)) { return true; // 没有配置Webhook,视为成功 } foreach ($webhooks as $webhook) { if ($async) { // 异步发送 Queue::push(function () use ($webhook, $event, $payload) { $this->deliverWebhook($webhook, $event, $payload); }); } else { // 同步发送 $this->deliverWebhook($webhook, $event, $payload); } } return true; } /** * 投递Webhook * * @param OpenApiWebhook $webhook * @param string $event * @param array $payload * @return bool */ protected function deliverWebhook(OpenApiWebhook $webhook, string $event, array $payload): bool { $deliveryId = uniqid('webhook_'); $timestamp = time(); // 构建请求数据 $requestData = [ 'id' => $deliveryId, 'event' => $event, 'timestamp' => $timestamp, 'data' => $payload, ]; // 生成签名 $signature = $this->generateSignature($requestData, $webhook->secret); // 构建请求头 $headers = [ 'Content-Type' => 'application/json', 'User-Agent' => 'OpenAPI-Webhook/1.0', 'X-Webhook-Event' => $event, 'X-Webhook-Delivery' => $deliveryId, 'X-Webhook-Timestamp' => $timestamp, 'X-Webhook-Signature' => $signature, ]; $startTime = microtime(true); $success = false; $responseStatus = 0; $responseBody = ''; $errorMessage = ''; try { // 发送HTTP请求 $response = Http::timeout($webhook->timeout ?? 30) ->withHeaders($headers) ->post($webhook->url, $requestData); $responseStatus = $response->status(); $responseBody = $response->body(); $success = $response->successful(); if (!$success) { $errorMessage = "HTTP {$responseStatus}: {$responseBody}"; } } catch (\Exception $e) { $errorMessage = $e->getMessage(); Log::error('Webhook delivery failed', [ 'webhook_id' => $webhook->id, 'url' => $webhook->url, 'event' => $event, 'error' => $errorMessage, ]); } $responseTime = (microtime(true) - $startTime) * 1000; // 毫秒 // 记录投递日志 $this->logWebhookDelivery($webhook, $event, $requestData, [ 'delivery_id' => $deliveryId, 'success' => $success, 'response_status' => $responseStatus, 'response_body' => $responseBody, 'response_time' => $responseTime, 'error_message' => $errorMessage, ]); // 更新Webhook统计 $this->updateWebhookStats($webhook, $success); // 如果失败且配置了重试,安排重试 if (!$success && $webhook->retry_count > 0) { $this->scheduleRetry($webhook, $event, $payload, $deliveryId); } return $success; } /** * 获取活跃的Webhook配置 * * @param string $appId * @param string $event * @return array */ protected function getActiveWebhooks(string $appId, string $event): array { return OpenApiWebhook::where('app_id', $appId) ->where('status', 'ACTIVE') ->where(function ($query) use ($event) { $query->whereJsonContains('events', $event) ->orWhereJsonContains('events', '*'); }) ->get() ->toArray(); } /** * 生成Webhook签名 * * @param array $data * @param string $secret * @return string */ protected function generateSignature(array $data, string $secret): string { $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return 'sha256=' . hash_hmac('sha256', $payload, $secret); } /** * 验证Webhook签名 * * @param string $payload * @param string $signature * @param string $secret * @return bool */ public function verifySignature(string $payload, string $signature, string $secret): bool { $expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret); return hash_equals($expectedSignature, $signature); } /** * 生成Webhook密钥 * * @return string */ protected function generateWebhookSecret(): string { return bin2hex(random_bytes(32)); } /** * 记录Webhook投递日志 * * @param OpenApiWebhook $webhook * @param string $event * @param array $requestData * @param array $responseData * @return void */ protected function logWebhookDelivery(OpenApiWebhook $webhook, string $event, array $requestData, array $responseData): void { Log::info('Webhook delivered', [ 'webhook_id' => $webhook->id, 'app_id' => $webhook->app_id, 'url' => $webhook->url, 'event' => $event, 'delivery_id' => $responseData['delivery_id'], 'success' => $responseData['success'], 'response_status' => $responseData['response_status'], 'response_time' => $responseData['response_time'], 'error_message' => $responseData['error_message'] ?? null, ]); } /** * 更新Webhook统计 * * @param OpenApiWebhook $webhook * @param bool $success * @return void */ protected function updateWebhookStats(OpenApiWebhook $webhook, bool $success): void { $webhook->increment('total_deliveries'); if ($success) { $webhook->increment('successful_deliveries'); $webhook->update(['last_success_at' => now()]); } else { $webhook->increment('failed_deliveries'); $webhook->update(['last_failure_at' => now()]); } } /** * 安排重试 * * @param OpenApiWebhook $webhook * @param string $event * @param array $payload * @param string $originalDeliveryId * @return void */ protected function scheduleRetry(OpenApiWebhook $webhook, string $event, array $payload, string $originalDeliveryId): void { // 计算重试延迟(指数退避) $retryAttempt = $webhook->current_retry_count ?? 0; $delay = min(300, pow(2, $retryAttempt) * 10); // 最大5分钟 Queue::later($delay, function () use ($webhook, $event, $payload, $originalDeliveryId, $retryAttempt) { if ($retryAttempt < $webhook->retry_count) { $webhook->increment('current_retry_count'); $this->deliverWebhook($webhook, $event, $payload); } }); Log::info('Webhook retry scheduled', [ 'webhook_id' => $webhook->id, 'original_delivery_id' => $originalDeliveryId, 'retry_attempt' => $retryAttempt + 1, 'delay_seconds' => $delay, ]); } /** * 测试Webhook配置 * * @param OpenApiWebhook $webhook * @return array */ public function testWebhook(OpenApiWebhook $webhook): array { $testPayload = [ 'test' => true, 'message' => 'This is a test webhook delivery', 'timestamp' => now()->toISOString(), ]; $success = $this->deliverWebhook($webhook, 'test', $testPayload); return [ 'success' => $success, 'webhook_id' => $webhook->id, 'url' => $webhook->url, 'test_payload' => $testPayload, ]; } /** * 获取Webhook投递统计 * * @param string $appId * @param string $period * @return array */ public function getWebhookStats(string $appId, string $period = 'day'): array { $webhooks = OpenApiWebhook::where('app_id', $appId)->get(); $stats = [ 'total_webhooks' => $webhooks->count(), 'active_webhooks' => $webhooks->where('status', 'ACTIVE')->count(), 'total_deliveries' => $webhooks->sum('total_deliveries'), 'successful_deliveries' => $webhooks->sum('successful_deliveries'), 'failed_deliveries' => $webhooks->sum('failed_deliveries'), ]; $stats['success_rate'] = $stats['total_deliveries'] > 0 ? round(($stats['successful_deliveries'] / $stats['total_deliveries']) * 100, 2) : 0; return $stats; } }