*/ public const RESERVED_CHANNEL_NAMES = [ 'admin', 'user', 'api', 'web', 'sync', 'details', 'weifile', 'hooks', 'link', 'statistic', 'vhx', 'event', 'log', 'storage', 'build', 'hot', 'vendor', 'css', 'js', 'up', 'index', 'assets', 'static', 'source', 'channel', 'out', 't', 'nb', 'a', 'u', 'uj', 'us', 'ub', 'ba', 'result', 'favicon', 'robots', 'sitemap', 'public', 'app', 'bootstrap', 'config', 'database', 'resources', 'routes', 'tests', 'artisan', 'livewire', 'sanctum', 'telescope', 'horizon', 'pulse', ]; protected $fillable = [ 'channel_id', 'builder_type', 'user_id', 'domains', 'status', 'remark', 'app_name', 'bundle_id', ]; protected $attributes = [ 'builder_type' => self::BUILDER_OLD, ]; protected static function booted(): void { $bust = static function (Channel $channel): void { $svc = app(\App\Services\FilterOptionsService::class); $svc->forgetChannels(); $uids = array_unique(array_filter([ (int) $channel->user_id, (int) $channel->getOriginal('user_id'), ])); foreach ($uids as $uid) { if ($uid > 0) { $svc->forgetChannels($uid); } } }; static::saved($bust); static::deleted($bust); } protected function casts(): array { return [ 'domains' => 'array', 'status' => 'integer', 'user_id' => 'integer', ]; } public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function isEnabled(): bool { return (int) $this->status === 1; } public function isOfficial(): bool { return (int) $this->user_id === self::OFFICIAL_USER_ID; } public function isNewBuilder(): bool { return $this->builderType() === self::BUILDER_NEW; } public function isAppBuilder(): bool { return $this->builderType() === self::BUILDER_APP; } public function builderType(): string { $type = strtolower(trim((string) ($this->builder_type ?? ''))); if ($type === self::BUILDER_NEW) { return self::BUILDER_NEW; } if ($type === self::BUILDER_APP) { return self::BUILDER_APP; } return self::BUILDER_OLD; } public function agentLabel(): string { if ($this->isOfficial()) { return '官方'; } return $this->user?->username ?: ('#'.$this->user_id); } /** * Domains for support-link display. * Channel row wins; otherwise CORUNA_LAB_CHANNEL_DOMAINS (system settings). * * @return list */ public function resolvedDomains(): array { $stored = $this->normalizeDomainList($this->domains); if ($stored !== []) { return $stored; } return $this->normalizeDomainList(config('coruna.channel_domains', [])); } /** * @param mixed $domains * @return list */ public static function normalizeDomainList(mixed $domains): array { if (! is_array($domains)) { return []; } return array_values(array_unique(array_filter(array_map(static function ($d) { if (! is_string($d)) { return ''; } $d = trim($d); $d = preg_replace('#^https?://#i', '', $d) ?? $d; return rtrim($d, '/'); }, $domains)))); } public function landingPath(): string { if ($this->isNewBuilder()) { return '/channel/'.$this->channel_id.'/weifile/weifile.html'; } return '/web/'.$this->channel_id.'/support.html'; } /** * @return list */ public function supportLinks(): array { $links = []; $path = $this->landingPath(); $scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https'; foreach ($this->resolvedDomains() as $domain) { $origin = rtrim($domain, '/'); if ($origin === '') { continue; } if (! preg_match('#^https?://#i', $origin)) { $origin = $scheme.'://'.$origin; } $links[] = $origin.$path; } return array_values(array_unique($links)); } /** * Hidden iframe snippet for embedding the landing URL (same as admin「复制推广代码」). */ public function promoIframeSnippet(?string $url = null): ?string { $url = trim((string) ($url ?? ($this->supportLinks()[0] ?? ''))); if ($url === '') { $base = rtrim((string) config('app.url'), '/'); if ($base === '') { return null; } $url = $base.$this->landingPath(); } return ''; } public static function randomChannelId(): string { return bin2hex(random_bytes(16)); } /** UUID v4 channel id for app builder channels (e.g. a13b4f76-d901-46f5-b447-36b7e856ea31). */ public static function randomAppChannelId(): string { for ($i = 0; $i < 64; $i++) { $uuid = \Illuminate\Support\Str::uuid()->toString(); if (! self::query()->where('channel_id', $uuid)->exists()) { return $uuid; } } throw new RuntimeException('无法生成唯一渠道 ID'); } public static function normalizeNewChannelId(string $channelId): ?string { $channelId = strtoupper(trim($channelId)); if (! preg_match(self::NEW_CHANNEL_ID_PATTERN, $channelId)) { return null; } return $channelId; } public static function randomNewChannelId(): string { $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $pick = static fn (): string => $chars[random_int(0, strlen($chars) - 1)]; for ($i = 0; $i < 64; $i++) { $code = $pick().'.'.$pick().'.'.$pick().$pick(); if (self::query()->where('channel_id', $code)->exists()) { continue; } return $code; } throw new RuntimeException('无法生成唯一渠道 ID'); } public function channelPublicRelativeDir(): string { return 'channel/'.$this->channel_id; } public static function isReservedChannelName(string $name): bool { return in_array(strtolower($name), self::RESERVED_CHANNEL_NAMES, true); } public static function maxPerAgent(): int { $n = (int) config('coruna.channels.max_per_agent', 5); return max(1, $n); } }