'Windows 64位', 'macos_arm64' => 'macOS(Apple芯片)', 'macos_x64' => 'macOS(Intel芯片)', ]; $packages = []; foreach (self::PLATFORMS as $platform) { $package = self::plainPackage((array) ($config['packages'][$platform] ?? []), $platform); $url = parse_url($package['url']); if (!is_array($url) || !in_array(strtolower($url['scheme'] ?? ''), ['https', 'http'], true) || empty($url['host']) || isset($url['user']) || isset($url['pass']) || !preg_match('/^[a-f0-9]{64}$/', $package['sha256'])) { continue; } $packages[] = ['platform' => $platform, 'label' => $labels[$platform]] + $package; } return [ 'version' => self::normalizeVersion((string) ($config['latest_version'] ?? '')), 'packages' => $packages, ]; } /** * @notes 保存升级策略与各平台安装包 */ public static function setConfig(array $params): bool { $stored = self::normalizeInput($params); ConfigService::set(self::CONFIG_TYPE, 'enabled', $stored['enabled']); ConfigService::set(self::CONFIG_TYPE, 'latest_version', $stored['latest_version']); ConfigService::set(self::CONFIG_TYPE, 'min_version', $stored['min_version']); ConfigService::set(self::CONFIG_TYPE, 'force_update', $stored['force_update']); ConfigService::set(self::CONFIG_TYPE, 'title', $stored['title']); ConfigService::set(self::CONFIG_TYPE, 'notes', $stored['notes']); ConfigService::set(self::CONFIG_TYPE, 'packages', $stored['packages']); return true; } /** * @notes 桌面端检测是否需要升级(免登录) */ public static function check(array $params): array { return self::evaluate( self::present(self::loadStored()), (string) ($params['current_version'] ?? ''), (string) ($params['platform'] ?? ''), (string) ($params['arch'] ?? '') ); } /** * @notes 根据已发布配置计算客户端检测结果 * @param array $config getConfig() 形态的配置 */ public static function evaluate(array $config, string $currentVersion, string $platform, string $arch): array { $current = self::normalizeVersion($currentVersion); $latest = self::normalizeVersion((string) ($config['latest_version'] ?? '')); $minVersion = self::normalizeVersion((string) ($config['min_version'] ?? '')); $enabled = (int) ($config['enabled'] ?? 0) === 1; $packageKey = self::packageKey($platform, $arch); $package = $packageKey !== '' ? self::plainPackage($config['packages'][$packageKey] ?? [], $packageKey) : self::emptyPackage(); $canInstall = $package['url'] !== '' && $package['sha256'] !== ''; $hasUpdate = $enabled && $latest !== '' && $current !== '' && self::compareVersion($current, $latest) < 0; $belowMin = $minVersion !== '' && $current !== '' && self::compareVersion($current, $minVersion) < 0; $wantsForce = $hasUpdate && (((int) ($config['force_update'] ?? 0) === 1) || $belowMin); return [ 'has_update' => $hasUpdate, 'force' => $hasUpdate && $wantsForce && $canInstall, 'enabled' => $enabled, 'current_version' => $current, 'latest_version' => $latest, 'min_version' => $minVersion, 'title' => (string) ($config['title'] ?? ''), 'notes' => (string) ($config['notes'] ?? ''), 'platform' => self::normalizePlatform($platform), 'arch' => self::normalizeArch($arch), 'package' => $canInstall ? $package : null, 'can_install' => $hasUpdate && $canInstall, ]; } public static function compareVersion(string $left, string $right): int { return self::versionParts($left) <=> self::versionParts($right); } public static function normalizeVersion(string $version): string { $version = trim($version); if ($version === '') { return ''; } if (!preg_match('/^\d+(?:\.\d+){0,3}$/', $version)) { return ''; } $parts = array_map(static fn(string $part): int => (int) $part, explode('.', $version)); $parts = array_pad(array_slice($parts, 0, 3), 3, 0); return implode('.', $parts); } public static function packageKey(string $platform, string $arch): string { $os = self::normalizePlatform($platform); $cpu = self::normalizeArch($arch); if ($os === '' || $cpu === '') { return ''; } $key = $os . '_' . $cpu; return in_array($key, self::PLATFORMS, true) ? $key : ''; } public static function normalizePlatform(string $platform): string { $value = strtolower(trim($platform)); return match ($value) { 'windows', 'win', 'win32', 'win64' => 'windows', 'macos', 'mac', 'darwin', 'osx' => 'macos', default => '', }; } public static function normalizeArch(string $arch): string { $value = strtolower(trim($arch)); return match ($value) { 'x64', 'amd64', 'x86_64', 'x86-64' => 'x64', 'arm64', 'aarch64' => 'arm64', default => '', }; } /** * @return array */ private static function loadStored(): array { $packages = ConfigService::get(self::CONFIG_TYPE, 'packages', []); if (!is_array($packages)) { $packages = []; } return [ 'enabled' => self::asFlag(ConfigService::get(self::CONFIG_TYPE, 'enabled', 1)), 'latest_version' => (string) (ConfigService::get(self::CONFIG_TYPE, 'latest_version', '') ?? ''), 'min_version' => (string) (ConfigService::get(self::CONFIG_TYPE, 'min_version', '') ?? ''), 'force_update' => self::asFlag(ConfigService::get(self::CONFIG_TYPE, 'force_update', 0)), 'title' => (string) (ConfigService::get(self::CONFIG_TYPE, 'title', '') ?? ''), 'notes' => (string) (ConfigService::get(self::CONFIG_TYPE, 'notes', '') ?? ''), 'packages' => self::normalizePackages($packages, persist: false), ]; } /** * @param array $params * @return array */ public static function normalizeInput(array $params): array { $packages = $params['packages'] ?? []; if (!is_array($packages)) { $packages = []; } foreach (self::PLATFORMS as $key) { if (!isset($packages[$key]) || !is_array($packages[$key])) { $packages[$key] = [ 'url' => (string) ($params[$key . '_url'] ?? ''), 'sha256' => (string) ($params[$key . '_sha256'] ?? ''), 'size' => $params[$key . '_size'] ?? 0, 'filename' => (string) ($params[$key . '_filename'] ?? ''), 'type' => (string) ($params[$key . '_type'] ?? 'archive'), ]; } } return [ 'enabled' => self::asFlag($params['enabled'] ?? 0), 'latest_version' => self::normalizeVersion((string) ($params['latest_version'] ?? '')), 'min_version' => self::normalizeVersion((string) ($params['min_version'] ?? '')), 'force_update' => self::asFlag($params['force_update'] ?? 0), 'title' => mb_substr(trim((string) ($params['title'] ?? '')), 0, 80), 'notes' => mb_substr(trim((string) ($params['notes'] ?? '')), 0, 4000), 'packages' => self::normalizePackages($packages, persist: true), ]; } /** * @param array $config * @return array */ private static function present(array $config): array { $packages = []; foreach (self::PLATFORMS as $key) { $packages[$key] = self::publicPackage($config['packages'][$key] ?? [], $key); } $config['packages'] = $packages; return $config; } /** * @param array $packages * @return array> */ private static function normalizePackages(array $packages, bool $persist): array { $normalized = []; foreach (self::PLATFORMS as $key) { $row = is_array($packages[$key] ?? null) ? $packages[$key] : []; $url = trim((string) ($row['url'] ?? '')); if ($persist && $url !== '') { $url = FileService::setFileUrl($url); } $sha256 = strtolower(trim((string) ($row['sha256'] ?? ''))); $filename = trim((string) ($row['filename'] ?? '')); $size = (int) ($row['size'] ?? 0); $type = self::normalizePackageType((string) ($row['type'] ?? ''), $key); if ($persist) { $filled = self::fillLocalPackageMeta($url, $sha256, $size, $filename); $url = $filled['url']; $sha256 = $filled['sha256']; $size = $filled['size']; $filename = $filled['filename']; } $normalized[$key] = [ 'url' => $url, 'sha256' => $sha256, 'size' => max(0, $size), 'filename' => mb_substr($filename, 0, 180), 'type' => $type, ]; } return $normalized; } /** * @param array $row * @return array{url:string,sha256:string,size:int,filename:string,type:string} */ private static function publicPackage(array $row, string $key): array { $plain = self::plainPackage($row, $key); $plain['url'] = $plain['url'] === '' ? '' : FileService::getFileUrl($plain['url']); return $plain; } /** * @param array $row * @return array{url:string,sha256:string,size:int,filename:string,type:string} */ private static function plainPackage(array $row, string $key = ''): array { return [ 'url' => trim((string) ($row['url'] ?? '')), 'sha256' => strtolower(trim((string) ($row['sha256'] ?? ''))), 'size' => max(0, (int) ($row['size'] ?? 0)), 'filename' => (string) ($row['filename'] ?? ''), 'type' => self::normalizePackageType((string) ($row['type'] ?? ''), $key), ]; } /** * @return array{url:string,sha256:string,size:int,filename:string,type:string} */ private static function emptyPackage(): array { return ['url' => '', 'sha256' => '', 'size' => 0, 'filename' => '', 'type' => 'archive']; } public static function normalizePackageType(string $type, string $key = ''): string { $value = strtolower(trim($type)); if ($value === '') { return 'archive'; } if (!in_array($value, self::PACKAGE_TYPES, true)) { return 'archive'; } if ($value === 'inno_setup' && $key !== '' && $key !== 'windows_x64') { return 'archive'; } return $value; } /** * @return array{url:string,sha256:string,size:int,filename:string} */ private static function fillLocalPackageMeta(string $url, string $sha256, int $size, string $filename): array { $relative = $url; if ($relative !== '' && !preg_match('#^https?://#i', $relative)) { $path = public_path() . ltrim(str_replace('\\', '/', $relative), '/'); if (is_file($path)) { if ($sha256 === '' || !preg_match('/^[a-f0-9]{64}$/', $sha256)) { $sha256 = hash_file('sha256', $path) ?: $sha256; } if ($size <= 0) { $size = (int) filesize($path); } if ($filename === '') { $filename = basename($path); } } } return [ 'url' => $url, 'sha256' => strtolower($sha256), 'size' => $size, 'filename' => $filename, ]; } /** * @return array{0:int,1:int,2:int} */ private static function versionParts(string $version): array { $normalized = self::normalizeVersion($version); if ($normalized === '') { return [0, 0, 0]; } return array_map('intval', explode('.', $normalized)); } private static function asFlag(mixed $value): int { if (is_bool($value)) { return $value ? 1 : 0; } return in_array((string) $value, ['1', 'true', 'on', 'yes'], true) ? 1 : 0; } }