邹景立 7e87c0d65a style(config): 重整系统配置页布局
将系统配置首页从混合展示存储信息调整为聚焦运行模式与基础参数,移除首页对存储中心上下文的加载,降低配置页渲染耦合。

拆分系统参数表单的入口主题、插件中心、运行参数、安全资源和品牌信息构建逻辑,并加入专用响应式样式,让多列字段在桌面端更紧凑、小屏端自动堆叠。

优化系统与存储概览卡片的栅格展示,更新渲染测试断言,确保配置页不再展示旧存储中心入口,同时保留存储配置独立页面的概览信息。
2026-05-21 00:15:55 +08:00

594 lines
21 KiB
PHP

<?php
declare(strict_types=1);
namespace plugin\system\service;
use plugin\system\storage\StorageConfig;
use plugin\worker\service\ProcessService;
use think\admin\Exception;
use think\admin\Library;
use think\admin\Service;
use think\admin\service\AppService;
use think\admin\Storage;
/**
* 系统配置服务。
* @class ConfigService
*/
class ConfigService extends Service
{
private const STORAGE_SECRET_KEYS = ['password', 'access_key', 'secret_key'];
/**
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
public static function buildIndexContext(array $themes = []): array
{
$site = self::getSiteConfig($themes);
$security = self::getSecurityConfig();
$runtime = self::getRuntimeConfig();
$framework = AppService::getPluginLibrarys('topthink/framework');
$thinkadmin = AppService::getPluginLibrarys('zoujingli/think-library');
$showErrorMessage = '';
if (AuthService::isSuper() && UserService::verifyPassword('admin', strval(AuthService::getUser('password', '')))) {
$url = url('system/index/pass', ['id' => AuthService::getUserId()]);
$showErrorMessage = lang("默认超管密码仍未修改,<a data-modal='%s'>立即修改</a>。", [$url]);
}
$pluginCenter = PluginService::getConfig();
$systemInfo = [
'核心框架' => ['value' => 'ThinkPHP Version ' . strval($framework['version'] ?? 'None'), 'url' => 'https://www.thinkphp.cn'],
'平台框架' => ['value' => 'ThinkAdmin Version ' . strval($thinkadmin['version'] ?? '6.0.0'), 'url' => 'https://thinkadmin.top'],
'操作系统' => ['value' => php_uname()],
'运行环境' => ['value' => ucfirst(request()->server('SERVER_SOFTWARE', php_sapi_name())) . ' / PHP ' . PHP_VERSION . ' / ' . ucfirst(app()->db->connect()->getConfig('type'))],
];
if (($systemid = ProcessService::getRunVar('uni')) !== null && $systemid !== '') {
$systemInfo['系统序号'] = ['value' => strval($systemid)];
}
return [
'site' => $site,
'security' => $security,
'runtime' => $runtime,
'pluginCenter' => $pluginCenter,
'issuper' => AuthService::isSuper(),
'appDebug' => app()->isDebug(),
'canEditSystem' => auth('system'),
'showErrorMessage' => $showErrorMessage,
'systemInfo' => $systemInfo,
];
}
/**
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
public static function buildSystemContext(array $themes = []): array
{
$site = self::getSiteConfig($themes);
$security = self::getSecurityConfig();
$runtime = self::getRuntimeConfig();
$theme = strval($site['theme'] ?? 'default');
if (!isset($themes[$theme])) {
$theme = 'default';
}
return [
'title' => '修改系统参数',
'site' => $site,
'security' => self::maskSecurityConfig($security),
'runtime' => $runtime,
'themes' => $themes,
'siteThemeKey' => $theme,
'siteThemeLabel' => strval($themes[$theme]['label'] ?? $theme),
'themePickerUrl' => sysuri('system/index/theme'),
'pluginCenter' => PluginService::getConfig(),
];
}
/**
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
public static function buildSystemFormData(array $themes = []): array
{
$context = self::buildSystemContext($themes);
return [
'site' => $context['site'],
'security' => $context['security'],
'runtime' => $context['runtime'],
'plugin_center' => $context['pluginCenter'],
];
}
/**
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
public static function getSiteConfig(array $themes = []): array
{
return self::siteConfig($themes);
}
/**
* @return array<string, mixed>
*/
public static function getSecurityConfig(): array
{
return self::securityConfig();
}
/**
* @return array<string, mixed>
*/
public static function getRuntimeConfig(): array
{
return self::runtimeConfig();
}
/**
* @param array<string, array<string, string>> $themes
*/
public static function getSiteTheme(array $themes = []): string
{
return strval(self::siteConfig($themes)['theme'] ?? 'default') ?: 'default';
}
/**
* 获取站点域名。
*/
public static function getSiteHost(string $default = ''): string
{
$host = self::normalizeSiteHost(strval(self::siteConfig()['host'] ?? ''));
return $host !== '' ? $host : $default;
}
/**
* 同步站点域名。
*/
public static function syncSiteHost(string $domain): void
{
$domain = self::normalizeSiteHost($domain);
if ($domain !== '' && $domain !== self::getSiteHost()) {
sysdata('system.site.host', $domain);
}
}
/**
* 启动时同步 System 登录入口绑定。
*/
public static function bootSystemLoginEntryBinding(): void
{
self::applySystemLoginEntryBinding(self::storedSystemLoginEntry());
}
/**
* 将 System 登录入口同步到运行时插件绑定配置。
*/
public static function applySystemLoginEntryBinding(string $entry = ''): string
{
$entry = self::normalizeLoginEntry($entry);
if ($entry === '') {
$entry = self::defaultSystemLoginEntry(true);
}
$app = (array)Library::$sapp->config->get('app', []);
$plugin = is_array($app['plugin'] ?? null) ? $app['plugin'] : [];
$bindings = is_array($plugin['bindings'] ?? null) ? $plugin['bindings'] : [];
$bindings['system'] = $entry;
$plugin['bindings'] = $bindings;
$app['plugin'] = $plugin;
Library::$sapp->config->set($app, 'app');
AppService::clear();
return AppService::pluginPrefix('system', true) ?: $entry;
}
/**
* @param array<string, mixed> $post
* @param array<string, array<string, string>> $themes
*/
public static function saveSystemConfig(array $post, array $themes = []): void
{
$payload = self::normalizeSystemPayload($post, $themes);
$site = $payload['site'];
$security = $payload['security'];
$runtime = $payload['runtime'];
$pluginCenter = $payload['plugin_center'];
if (preg_match('#^https?://#', strval($site['browser_icon'] ?? ''))) {
try {
SystemService::setFavicon(strval($site['browser_icon'] ?? ''));
} catch (\Throwable $exception) {
trace_file($exception);
}
}
sysdata('system.site', $site);
sysdata('system.security', $security);
sysdata('system.runtime', $runtime);
PluginService::setConfig($pluginCenter);
self::applySystemLoginEntryBinding(strval($site['login_entry'] ?? ''));
sysoplog('系统参数配置', '更新系统参数');
}
/**
* @return array<string, mixed>
*/
public static function buildStorageIndexContext(): array
{
StorageConfig::initialize();
$storage = self::maskStorageViewData(StorageConfig::viewData());
$files = Storage::types();
$driver = strtolower((string)StorageConfig::global('default_driver', 'local'));
return [
'storage' => $storage,
'files' => $files,
'driver' => $driver,
'driverName' => strval($files[$driver] ?? $driver),
'canEdit' => self::canManageStorage(),
];
}
/**
* @return array<string, mixed>
*/
public static function buildStorageFormContext(string $type): array
{
StorageConfig::initialize();
$files = Storage::types();
$type = strtolower(trim($type));
if (!isset($files[$type])) {
$type = strval(array_key_first($files) ?: 'local');
}
return [
'title' => '修改存储驱动',
'type' => $type,
'driverName' => strval($files[$type] ?? $type),
'points' => Storage::regions($type),
'storage' => self::maskStorageViewData(StorageConfig::viewData()),
];
}
/**
* @return array<string, mixed>
*/
public static function buildStorageFormData(): array
{
StorageConfig::initialize();
return ['storage' => self::maskStorageViewData(StorageConfig::viewData())];
}
/**
* @param array<string, mixed> $post
* @return array<string, mixed>
*/
public static function normalizeStorageConfig(array $post): array
{
$data = (array)($post['storage'] ?? $post);
if (isset($data['allowed_extensions_text']) && !isset($data['allowed_extensions'])) {
$data['allowed_extensions'] = $data['allowed_extensions_text'];
}
unset($data['allowed_extensions_text']);
$current = StorageConfig::payload();
return self::restoreStorageSecrets(array_replace_recursive($current, $data), $current);
}
/**
* 是否允许查看存储配置.
*/
public static function canViewStorage(): bool
{
return AuthService::isSuper()
|| AuthService::check('system/config/storage')
|| AuthService::check('system/file/index')
|| self::canManageStorage();
}
/**
* 是否允许管理存储配置.
*/
public static function canManageStorage(): bool
{
return AuthService::isSuper()
|| AuthService::check('system/config/storage')
|| AuthService::check('system/file/edit')
|| AuthService::check('system/file/remove')
|| AuthService::check('system/file/distinct');
}
/**
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
private static function siteConfig(array $themes = []): array
{
$site = array_replace_recursive([
'login_title' => '系统管理',
'login_entry' => self::defaultSystemLoginEntry(),
'theme' => 'default',
'login_background_images' => [],
'browser_icon' => 'https://thinkadmin.top/static/img/logo.png',
'website_name' => 'ThinkAdmin',
'application_name' => 'ThinkAdmin',
'application_version' => 'v8',
'public_security_filing' => '',
'miit_filing' => '',
'copyright' => 'Copyright 2014-' . date('Y') . ' ThinkAdmin',
'host' => '',
], (array)sysget('system.site', []));
$site['login_title'] = strval($site['login_title']);
$site['login_entry'] = self::normalizeLoginEntry(strval($site['login_entry'] ?? ''));
$site['theme'] = strval($site['theme']) ?: 'default';
$site['browser_icon'] = strval($site['browser_icon']);
$site['website_name'] = strval($site['website_name']);
$site['application_name'] = strval($site['application_name']);
$site['application_version'] = strval($site['application_version']);
$site['public_security_filing'] = strval($site['public_security_filing']);
$site['miit_filing'] = strval($site['miit_filing']);
$site['copyright'] = strval($site['copyright']);
$site['host'] = self::normalizeSiteHost(strval($site['host']));
$site['login_background_images'] = array_values(array_filter(array_map('strval', (array)$site['login_background_images'])));
if ($site['login_entry'] === '') {
$site['login_entry'] = self::defaultSystemLoginEntry();
}
if (!isset($themes[$site['theme']]) && $themes !== []) {
$site['theme'] = 'default';
}
return $site;
}
/**
* @return array<string, mixed>
*/
private static function securityConfig(): array
{
$security = array_replace_recursive([
'jwt_secret' => bin2hex(random_bytes(16)),
], (array)sysget('system.security', []));
$security['jwt_secret'] = strval($security['jwt_secret']);
if (strlen($security['jwt_secret']) !== 32) {
$security['jwt_secret'] = bin2hex(random_bytes(16));
}
return $security;
}
/**
* @return array<string, mixed>
*/
private static function runtimeConfig(): array
{
$runtime = (array)sysget('system.runtime', []);
return [
'queue_retain_days' => max(1, intval($runtime['queue_retain_days'] ?? 7)),
];
}
/**
* @param array<string, mixed> $data
* @param array<string, array<string, string>> $themes
* @return array<string, mixed>
*/
private static function normalizeSiteConfig(array $data, array $themes = []): array
{
$site = array_replace_recursive(self::siteConfig($themes), $data);
$site['login_title'] = trim(strval($site['login_title'] ?? ''));
$site['login_entry'] = self::validateSystemLoginEntry(self::normalizeLoginEntry(strval($site['login_entry'] ?? '')));
$site['theme'] = trim(strval($site['theme'] ?? 'default'));
$site['browser_icon'] = trim(strval($site['browser_icon'] ?? ''));
$site['website_name'] = trim(strval($site['website_name'] ?? ''));
$site['application_name'] = trim(strval($site['application_name'] ?? ''));
$site['application_version'] = trim(strval($site['application_version'] ?? ''));
$site['public_security_filing'] = trim(strval($site['public_security_filing'] ?? ''));
$site['miit_filing'] = trim(strval($site['miit_filing'] ?? ''));
$site['copyright'] = trim(strval($site['copyright'] ?? ''));
$site['host'] = self::normalizeSiteHost(strval($site['host'] ?? ''));
$images = $site['login_background_images'] ?? [];
if (is_string($images)) {
$images = str2arr($images, '|');
}
$site['login_background_images'] = array_values(array_filter(array_map(static fn(mixed $item): string => trim(strval($item)), (array)$images)));
if (!isset($themes[$site['theme']]) && $themes !== []) {
$site['theme'] = 'default';
}
if ($site['login_title'] === '') {
$site['login_title'] = '系统管理';
}
if ($site['website_name'] === '') {
$site['website_name'] = 'ThinkAdmin';
}
if ($site['application_name'] === '') {
$site['application_name'] = 'ThinkAdmin';
}
return $site;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private static function normalizeSecurityConfig(array $data): array
{
$current = self::securityConfig();
$security = array_replace_recursive($current, $data);
$security['jwt_secret'] = trim(strval($security['jwt_secret'] ?? ''));
if (password_is_unchanged($security['jwt_secret'])) {
$security['jwt_secret'] = strval($current['jwt_secret'] ?? '');
return $security;
}
if (strlen($security['jwt_secret']) !== 32) {
$security['jwt_secret'] = bin2hex(random_bytes(16));
}
return $security;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private static function maskSecurityConfig(array $data): array
{
if (!empty($data['jwt_secret'])) {
$data['jwt_secret'] = password_mask(strlen(strval($data['jwt_secret'])));
}
return $data;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private static function maskStorageViewData(array $payload): array
{
foreach ((array)($payload['drivers'] ?? []) as $driver => $config) {
foreach (self::STORAGE_SECRET_KEYS as $key) {
$value = trim(strval($config[$key] ?? ''));
if ($value !== '') {
$payload['drivers'][$driver][$key] = password_mask();
}
}
}
return $payload;
}
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $current
* @return array<string, mixed>
*/
private static function restoreStorageSecrets(array $payload, array $current): array
{
foreach ((array)($payload['drivers'] ?? []) as $driver => $config) {
foreach (self::STORAGE_SECRET_KEYS as $key) {
$value = trim(strval($config[$key] ?? ''));
$origin = trim(strval($current['drivers'][$driver][$key] ?? ''));
if ($value === '' || password_is_mask($value)) {
$payload['drivers'][$driver][$key] = $origin;
}
}
}
return $payload;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private static function normalizeRuntimeConfig(array $data): array
{
return [
'queue_retain_days' => max(1, intval($data['queue_retain_days'] ?? self::runtimeConfig()['queue_retain_days'] ?? 7)),
];
}
/**
* @param array<string, mixed> $post
* @param array<string, array<string, string>> $themes
* @return array{site:array<string,mixed>,security:array<string,mixed>,runtime:array<string,mixed>,plugin_center:array<string,int>}
*/
private static function normalizeSystemPayload(array $post, array $themes = []): array
{
return [
'site' => self::normalizeSiteConfig((array)($post['site'] ?? []), $themes),
'security' => self::normalizeSecurityConfig((array)($post['security'] ?? [])),
'runtime' => self::normalizeRuntimeConfig((array)($post['runtime'] ?? [])),
'plugin_center' => self::normalizePluginCenterConfig((array)($post['plugin_center'] ?? [])),
];
}
/**
* @param array<string, mixed> $data
* @return array{enabled:int,show_menu:int}
*/
private static function normalizePluginCenterConfig(array $data): array
{
return [
'enabled' => empty($data['enabled']) ? 0 : 1,
'show_menu' => empty($data['show_menu']) ? 0 : 1,
];
}
private static function validateSystemLoginEntry(string $entry): string
{
if ($entry === '') {
$entry = self::defaultSystemLoginEntry(true);
}
if (!preg_match('/^[a-z][a-z0-9_-]*$/i', $entry)) {
throw new Exception(lang('后台登录入口格式错误,请使用英文字母开头的应用名或别名!'));
}
$entry = strtolower($entry);
if ($entry === strtolower(AppService::pluginApiPrefix())) {
throw new Exception(lang('后台登录入口不能与系统 API 入口冲突!'));
}
foreach (array_keys(AppService::local(true)) as $code) {
if (strtolower(strval($code)) === $entry) {
throw new Exception(lang('后台登录入口不能与本地应用名称冲突!'));
}
}
foreach (AppService::allPlugins(false, true) as $code => $plugin) {
if ($code === 'system') {
continue;
}
if (strtolower(strval($code)) === $entry) {
throw new Exception(lang('后台登录入口不能与其它插件入口或别名冲突!'));
}
if (strtolower(strval($plugin['alias'] ?? '')) === $entry) {
throw new Exception(lang('后台登录入口不能与其它插件入口或别名冲突!'));
}
foreach ((array)($plugin['prefixes'] ?? []) as $prefix) {
if (strtolower(strval($prefix)) === $entry) {
throw new Exception(lang('后台登录入口不能与其它插件入口或别名冲突!'));
}
}
}
return $entry;
}
private static function normalizeLoginEntry(string $entry): string
{
$entry = trim(strtolower($entry), " \t\n\r\0\x0B\\/");
if (str_contains($entry, '/')) {
$entry = strstr($entry, '/', true) ?: $entry;
}
if (str_contains($entry, '.')) {
$entry = strstr($entry, '.', true) ?: $entry;
}
return $entry;
}
private static function defaultSystemLoginEntry(bool $force = false): string
{
$entry = self::normalizeLoginEntry(strval(AppService::pluginPrefix('system', $force) ?: 'system'));
return $entry !== '' ? $entry : 'system';
}
private static function storedSystemLoginEntry(): string
{
$site = (array)sysget('system.site', []);
return self::normalizeLoginEntry(strval($site['login_entry'] ?? ''));
}
private static function normalizeSiteHost(string $host): string
{
return rtrim(trim($host), "\\/");
}
}