|null $deploymentDomains Channel-configured hosts; empty → CORUNA_LAB_CHANNEL_DOMAINS */ public function generate(string $channelId, ?array $deploymentDomains = null): void { $channelId = $this->normalizeChannelId($channelId); $deploymentDomains = $this->normalizeDomains($deploymentDomains) ?: $this->domains('coruna.deployment_domains'); $reportingDomains = $this->domains('coruna.reporting_domains'); if ($deploymentDomains === []) { throw new RuntimeException('投放域名未配置(CORUNA_LAB_CHANNEL_DOMAINS 或渠道 domains)'); } if ($reportingDomains === []) { throw new RuntimeException('上报域名未配置(CORUNA_LAB_AMIDN_DOMAINS)'); } $response = $this->request('post', $channelId, [ 'deployment_domains' => $deploymentDomains, 'reporting_domains' => $reportingDomains, ]); $this->ensureSuccessful($response, '生成渠道资源失败'); } public function deleteWebTree(string $channelId): void { try { $channelId = $this->normalizeChannelId($channelId); } catch (RuntimeException) { return; } $response = $this->request('delete', $channelId); if ($response->status() === 404) { return; } $this->ensureSuccessful($response, '删除渠道资源失败'); } private function request(string $method, string $channelId, array $payload = []): Response { $url = rtrim((string) config('coruna.build_service.url', ''), '/'); $token = trim((string) config('coruna.build_service.token', '')); if ($url === '') { throw new RuntimeException('渠道构建服务 URL 未配置'); } if ($token === '') { throw new RuntimeException('渠道构建服务令牌未配置'); } $method = strtoupper($method); $path = $method === 'POST' ? '/v1/channels/'.$channelId.'/build' : '/v1/channels/'.$channelId; try { $pending = Http::baseUrl($url) ->withToken($token) ->acceptJson() ->connectTimeout((float) config('coruna.build_service.connect_timeout', 5)) ->timeout((float) config('coruna.build_service.timeout', 600)); if ($method === 'POST') { return $pending->asJson()->post($path, $payload); } return $pending->send($method, $path); } catch (ConnectionException $e) { Log::error('Channel build service connection failed', [ 'channel_id' => $channelId, 'operation' => $method, 'error' => $e->getMessage(), ]); throw new RuntimeException('渠道构建服务连接失败: '.$e->getMessage(), 0, $e); } } private function ensureSuccessful(Response $response, string $message): void { if ($response->successful()) { return; } $detail = $response->json('message'); $detail = is_string($detail) && trim($detail) !== '' ? trim($detail) : trim($response->body()); $detail = mb_substr($detail !== '' ? $detail : 'unknown error', 0, 500); Log::error('Channel build service request failed', [ 'status' => $response->status(), 'detail' => $detail, ]); throw new RuntimeException("{$message} ({$response->status()}): {$detail}"); } private function normalizeChannelId(string $channelId): string { $channelId = strtolower(trim($channelId)); if (! preg_match('/^[a-z0-9]{32}$/', $channelId)) { throw new RuntimeException('Invalid channel id'); } return $channelId; } /** @return list */ private function domains(string $key): array { return $this->normalizeDomains(config($key, [])); } /** @return list */ private function normalizeDomains(mixed $domains): array { if (! is_array($domains)) { return []; } return array_values(array_unique(array_filter(array_map(static function ($domain) { if (! is_string($domain)) { return ''; } $domain = trim($domain); $domain = preg_replace('#^https?://#i', '', $domain) ?? $domain; return rtrim($domain, '/'); }, $domains)))); } }