Files
ulthon_admin/.agents/rules/ulthon-system-config.md
augushong 4a4155070d docs(agents): 新增控制器响应陷阱与 sysconfig 机制规则
- ulthon-controller-response-throw 规则:揭露 success/error/result/redirect 都是 throw HttpResponseException 的陷阱,含两种正确范式(try 外 / 放行 HttpResponseException)与 grep 自查方法
- ulthon-system-config 规则:sysconfig 完整机制(存储/读取/保存/视图扩展),含新增配置项两种方式(加入已有组 / 新建独立 Tab)
- AGENTS.md 索引同步:零散规则表 +2 行,工作流列表 +2 行(含上一 commit 的技能)
2026-07-19 12:03:22 +08:00

277 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 系统配置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`
```php
// 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`
1.`group_name` 隐藏域确定配置组
2. 遍历 POST 的其余字段,逐个 upsert 到 `system_config`group + name 存在则更新,不存在则创建)
3. 调用 `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 容器)
```html
<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 为例)
```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 常见表单控件写法
**文本输入**
```html
<input type="text" name="键名" class="layui-input"
value="{:sysconfig('组名','键名')}">
```
**文本域**
```html
<textarea name="键名" class="layui-textarea">{:sysconfig('组名','键名')}</textarea>
```
**单选radio**
```html
{foreach ['值1'=>'标签1','值2'=>'标签2'] as $key=>$val}
<input type="radio" name="键名" value="{$key}" title="{$val}"
{if $key==sysconfig('组名','键名')}checked=""{/if}>
{/foreach}
```
**带上传组件**(图片/文件):
```html
<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`**。
**步骤**
1. 覆盖对应的 include 模板到 `app/admin/view/system/config/` 下(如 `site.html`),复制原内容并增加新字段
2. 首次保存时 `ConfigBase::save()` 会自动在 `system_config` 表插入新记录group=site, name=新键名)
3. 代码中用 `sysconfig('site', '新键名')` 读取
**示例**:在"网站设置"Tab 中增加「扫码域名」:
```html
<!-- 在 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/`
**步骤**
1. 复制 `extend/base/admin/view/system/config/index.html``app/admin/view/system/config/index.html`
2. 在覆盖的 `index.html` 中增加新 Tab 的 `<li>``<div class="layui-tab-item">`
3. 创建新 Tab 的 include 模板 `app/admin/view/system/config/新组名.html`
4. 首次保存后,配置数据自动写入 `system_config`
**示例**新增「业务配置」Tab
```html
<!-- app/admin/view/system/config/index.html 中增加 -->
<li>业务配置</li>
<!-- ... -->
<div class="layui-tab-item">
{include file="system/config/box" /}
</div>
```
```html
<!-- 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](./ulthon-file-override-mechanism.md)。
**覆盖规则**
- 只需在 `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`
## 六、注意事项
1. **`group_name` 必传**:每个配置表单必须包含 `<input type="hidden" name="group_name" value="组名">`,否则保存逻辑走全局 name 匹配,可能误更新其他组的同名字段
2. **首次保存自动建记录**`ConfigBase::save()` 在 group+name 不存在时会自动 `create()`,无需手动插入 DB 种子数据。但建议通过 DB 预置默认值,避免首次读取时返回 `null`
3. **缓存 TTL**sysconfig 缓存 3600 秒。如果直接改 DB不走后台保存需手动清缓存`Cache::tag('sysconfig')->clear()`
4. **覆盖 `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-file-override-mechanism.md)(视图覆盖机制,本规则依赖它实现配置视图覆盖)
- [ulthon-base-app-architecture](../skills/ulthon-base-app-architecture/SKILL.md)Base/App 双层架构,决定 `extend/base/admin/view/` 不可改、`app/admin/view/` 可改)
- [ulthon-update-workflow](../skills/ulthon-update-workflow/SKILL.md)(框架更新时如何处理覆盖的 `index.html`