- ulthon-controller-response-throw 规则:揭露 success/error/result/redirect 都是 throw HttpResponseException 的陷阱,含两种正确范式(try 外 / 放行 HttpResponseException)与 grep 自查方法 - ulthon-system-config 规则:sysconfig 完整机制(存储/读取/保存/视图扩展),含新增配置项两种方式(加入已有组 / 新建独立 Tab) - AGENTS.md 索引同步:零散规则表 +2 行,工作流列表 +2 行(含上一 commit 的技能)
11 KiB
系统配置(sysconfig)机制与扩展规范
来源:框架内置(ulthon-) 作用域:需要通过后台界面管理的可变配置项(域名、密钥、开关等) 触发条件:新增/修改后台可变配置、读取
sysconfig()、覆盖system/config/*视图时加载
一、核心机制
1.1 数据存储
配置存储在 system_config 表,结构为 group + name + value 的键值对:
| 字段 | 说明 | 示例 |
|---|---|---|
group |
配置组(Tab 页粒度) | site、upload、wechat |
name |
配置键名 | site_domain、upload_type |
value |
配置值(字符串) | http://example.com、local_public |
remark |
后台显示的说明文字 | 站点域名 |
sort |
排序 | 0 |
1.2 读取:sysconfig() 辅助函数
定义位置:extend/base/helper.php:133。
// 1. 读单个配置项(推荐)
$value = sysconfig('site', 'site_domain');
// 2. 读整组配置(返回 [name => value] 数组)
$all = sysconfig('site');
// $all['site_domain'], $all['site_name'] ...
// 3. 提供默认值(值为 null 时返回默认)
$value = sysconfig('site', 'site_domain', 'https://default.com');
// 4. 跨组快捷查找($name 传 true,$group 参数当字段名用)
$value = sysconfig('site_domain', true);
缓存机制:
- 用
Cache::tag('sysconfig')缓存,TTL 3600 秒 - 单值缓存 key:
sysconfig_{group}_{name} - 整组缓存 key:
sysconfig_{group} - 保存配置时
TriggerService::updateSysconfig()自动清除整个sysconfig标签的缓存
1.3 保存:ConfigBase::save()
POST 到 system.config/save,控制器逻辑位于 extend/base/admin/controller/system/ConfigBase.php:
- 取
group_name隐藏域确定配置组 - 遍历 POST 的其余字段,逐个 upsert 到
system_config(group + name 存在则更新,不存在则创建) - 调用
TriggerService::updateSysconfig()(extend/base/admin/service/TriggerServiceBase.php:45)清缓存
关键:group_name 不存在时按 name 全局匹配更新(无 group 限定);有 group_name 时严格按 group + name 匹配。每个配置表单必须包含 <input type="hidden" name="group_name">。
二、配置视图结构
2.1 文件层级
配置页面入口(框架内核,可覆盖):
extend/base/admin/view/system/config/index.html ← Tab 容器
各 Tab 内容(include,可覆盖):
extend/base/admin/view/system/config/site.html ← 网站设置
extend/base/admin/view/system/config/logo.html ← LOGO 配置
extend/base/admin/view/system/config/upload.html ← 上传配置
2.2 index.html 结构(Tab 容器)
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title">
<li class="layui-this">网站设置</li>
<li>LOGO 配置</li>
<li>上传配置</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
{include file="system/config/site" /}
</div>
<!-- 更多 Tab -->
</div>
</div>
2.3 单个配置 Tab 模板(以 site.html 为例)
<form id="app-form" class="layui-form layuimini-form">
<!-- 1. 隐藏域:声明配置组名(必传) -->
<input type="hidden" name="group_name" value="site">
<!-- 2. 配置项:name=键名,value=当前值 -->
<div class="layui-form-item">
<label class="layui-form-label">站点域名</label>
<div class="layui-input-block">
<input type="text" name="site_domain"
class="layui-input" lay-verify="required"
value="{:sysconfig('site','site_domain')}">
<tip>填写说明文字</tip>
</div>
</div>
<!-- 3. 提交按钮:固定写法 -->
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm"
lay-submit="system.config/save" data-refresh="false">确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
2.4 常见表单控件写法
文本输入:
<input type="text" name="键名" class="layui-input"
value="{:sysconfig('组名','键名')}">
文本域:
<textarea name="键名" class="layui-textarea">{:sysconfig('组名','键名')}</textarea>
单选(radio):
{foreach ['值1'=>'标签1','值2'=>'标签2'] as $key=>$val}
<input type="radio" name="键名" value="{$key}" title="{$val}"
{if $key==sysconfig('组名','键名')}checked=""{/if}>
{/foreach}
带上传组件(图片/文件):
<div class="layui-input-block layuimini-upload">
<input name="键名" class="layui-input layui-col-xs6"
value="{:sysconfig('组名','键名')}">
<div class="layuimini-upload-btn">
<a class="layui-btn" data-upload="键名" data-upload-number="one" data-upload-exts="*image">
<i class="fa fa-upload"></i> 上传
</a>
<a class="layui-btn layui-btn-normal" data-upload-select="键名" data-upload-number="one">
<i class="fa fa-list"></i> 选择
</a>
</div>
</div>
三、新增配置项
方式 A:加入已有配置组(推荐)
在已有的 Tab(如 site)中增加字段,无需改 index.html。
步骤:
- 覆盖对应的 include 模板到
app/admin/view/system/config/下(如site.html),复制原内容并增加新字段 - 首次保存时
ConfigBase::save()会自动在system_config表插入新记录(group=site, name=新键名) - 代码中用
sysconfig('site', '新键名')读取
示例:在"网站设置"Tab 中增加「扫码域名」:
<!-- 在 app/admin/view/system/config/site.html 中追加 -->
<div class="layui-form-item">
<label class="layui-form-label">扫码域名</label>
<div class="layui-input-block">
<input type="text" name="box_scan_url" class="layui-input"
value="{:sysconfig('site','box_scan_url')}">
<tip>二维码扫码访问使用的域名,以 http:// 或 https:// 开头</tip>
</div>
</div>
读取:sysconfig('site', 'box_scan_url')
方式 B:新建独立配置组(新 Tab)
需要修改 index.html 增加新 Tab。由于 index.html 在 extend/base/ 不可改,需整体覆盖到 app/admin/view/。
步骤:
- 复制
extend/base/admin/view/system/config/index.html到app/admin/view/system/config/index.html - 在覆盖的
index.html中增加新 Tab 的<li>和<div class="layui-tab-item"> - 创建新 Tab 的 include 模板
app/admin/view/system/config/新组名.html - 首次保存后,配置数据自动写入
system_config表
示例:新增「业务配置」Tab:
<!-- app/admin/view/system/config/index.html 中增加 -->
<li>业务配置</li>
<!-- ... -->
<div class="layui-tab-item">
{include file="system/config/box" /}
</div>
<!-- app/admin/view/system/config/box.html -->
<form id="app-form" class="layui-form layuimini-form">
<input type="hidden" name="group_name" value="box">
<div class="layui-form-item">
<label class="layui-form-label">扫码域名</label>
<div class="layui-input-block">
<input type="text" name="scan_url" class="layui-input"
value="{:sysconfig('box','scan_url')}">
<tip>扫码访问域名</tip>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm"
lay-submit="system.config/save" data-refresh="false">确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
读取:sysconfig('box', 'scan_url')
四、视图覆盖优先级
ThinkPHP 模板解析顺序(先找到先使用):
1. app/admin/view/system/config/xxx.html ← 应用层覆盖(优先)
2. extend/base/admin/view/system/config/xxx.html ← 框架内核
详细机制见规则 ulthon-file-override-mechanism。
覆盖规则:
- 只需在
app/admin/view/system/config/下创建同名文件即可覆盖 {include file="system/config/xxx" /}的解析也遵循此优先级- 覆盖
index.html时,必须保留原有 Tab 的 include 引用,否则丢失功能
五、sysconfig vs .env 的选择
| 维度 | sysconfig | .env |
|---|---|---|
| 修改方式 | 后台界面修改,实时生效 | 手动编辑文件,需重启服务 |
| 适用场景 | 业务可变配置(域名、密钥、开关) | 环境固定配置(数据库连接、调试开关) |
| 缓存 | Cache::tag('sysconfig'),保存自动清除 |
无缓存(opcache 级别) |
| 多环境 | 各环境共享代码,各自配置值不同 | 各环境各自文件 |
| 命令行 | 正常读取(有 DB 连接即可) | 正常读取 |
判断标准:如果部署后可能需要修改 → sysconfig;如果绑定环境不会变 → .env。
六、注意事项
-
group_name必传:每个配置表单必须包含<input type="hidden" name="group_name" value="组名">,否则保存逻辑走全局 name 匹配,可能误更新其他组的同名字段 -
首次保存自动建记录:
ConfigBase::save()在 group+name 不存在时会自动create(),无需手动插入 DB 种子数据。但建议通过 DB 预置默认值,避免首次读取时返回null -
缓存 TTL:sysconfig 缓存 3600 秒。如果直接改 DB(不走后台保存),需手动清缓存:
Cache::tag('sysconfig')->clear() -
覆盖
index.html的维护成本:框架更新时如果index.html有变化(新增 Tab 等),覆盖版本不会自动同步。覆盖后需在框架升级时手动 diff 合并(php think admin:update)
相关文件
extend/base/helper.php(sysconfig()定义,第 133 行)extend/base/admin/controller/system/ConfigBase.php(配置控制器)extend/base/admin/service/TriggerServiceBase.php(updateSysconfig()清缓存,第 45 行)extend/base/admin/view/system/config/(框架默认视图:index/site/logo/upload)app/admin/model/SystemConfig.php(配置模型)
相关规则与技能
- ulthon-file-override-mechanism(视图覆盖机制,本规则依赖它实现配置视图覆盖)
- ulthon-base-app-architecture(Base/App 双层架构,决定
extend/base/admin/view/不可改、app/admin/view/可改) - ulthon-update-workflow(框架更新时如何处理覆盖的
index.html)