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 的技能)
This commit is contained in:
augushong
2026-07-19 12:03:22 +08:00
parent 98dd244e61
commit 4a4155070d
3 changed files with 396 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
# 控制器响应是 throwsuccess/error/result/redirect 陷阱)
> 来源框架内置ulthon-
> 作用域所有控制器admin / tools 等全部模块)
> 触发条件:在控制器里写 try-catch、调用 `$this->success/error/result/redirect` 时加载
## 一、陷阱本质
框架的 `JumpTraitBase``extend/base/common/traits/JumpTraitBase.php`)中,`success()` / `error()` / `result()` / `redirect()` 四个方法**全部是 `throw new HttpResponseException($response)`,不是 return**
| 方法 | 行号 | throw 位置 |
|------|------|-----------|
| `success()` | 第 23 行 | 第 49 行 |
| `error()` | 第 62 行 | 第 86 行 |
| `result()` | 第 99 行 | 第 110 行 |
| `redirect()` | 第 122 行 | 第 130 行 |
因此它们一旦出现在 `try { ... } catch (\Throwable $e)` 块内,**成功响应的异常会被 catch 捕获吞掉**,随后走到 catch 里的 `$this->error(...)`,最终返回错误响应——而此时业务事务往往已经提交、数据已经变更。
## 二、症状特征
- HTTP 返回 `code:500`
- `msg` 形如 `"XX失败"`(冒号后为空),因为 `HttpResponseException``getMessage()` 默认为空字符串
- 但数据库实际已变更成功(事务已在抛异常前提交)
- 用户表现:**"提示失败,但刷新页面发现操作其实成功了"**
这是最难排查的一类 bug表象是失败实际是成功数据已经落库。
## 三、正确范式 A推荐响应调用放在 try 外)
```php
try {
$result = SomeService::do($id);
} catch (\Throwable $e) {
return $this->error('失败:' . $e->getMessage());
}
return $this->success('成功', $result); // ← 在 try 外
```
适用场景:大多数控制器流程。结构清晰,避免 catch 误吞响应异常。
## 四、正确范式 Bcatch 前放行 HttpResponseException
当 success 必须在 try 内调用时,在 catch 链最前面放行响应异常:
```php
try {
$result = SomeService::do($id);
return $this->success('成功', $result); // try 内也可
} catch (\think\exception\HttpResponseException $e) {
throw $e; // ← 放行响应异常,不吞
} catch (\Throwable $e) {
return $this->error('失败:' . $e->getMessage());
}
```
适用场景success 调用必须紧贴业务逻辑(如需要复用 `$result`),无法移到 try 外。
## 五、反例(会触发 bug禁止
```php
try {
$result = SomeService::do($id);
return $this->success('成功', $result); // ← 成功响应被 catch 吞掉
} catch (\Throwable $e) { // ← 缺 HttpResponseException 放行
return $this->error('失败:' . $e->getMessage());
}
```
特征:
- try 内调用 `$this->success/error/result/redirect`
- catch 用 `\Throwable``\Exception`(不区分响应异常)
**结果**:用户看到"操作失败",但数据库已变更。常见于"绑定/解绑/状态流转"等需要事务的操作。
## 六、特例:`return json()` 不受影响
`return json(...)` 是普通的 return不抛异常**放 try 内安全**
```php
try {
$result = SomeService::do($id);
return json($result); // ← 安全,不抛 HttpResponseException
} catch (\Throwable $e) {
return $this->error('失败:' . $e->getMessage());
}
```
但通常推荐统一用 `$this->success()` 保持响应格式一致(`{code, msg, data}` 结构)。
## 七、自查方法
新增/修改控制器后,凡 try 块内出现 `$this->success` / `$this->error` / `$this->result` / `$this->redirect` 的,必须满足范式 A 或 B 之一。
排查命令(找所有受影响的文件):
```bash
# 1. 找所有调用响应方法的文件
grep -rn '\$this->success\|\$this->error\|\$this->result\|\$this->redirect' app/admin/controller
# 2. 找所有 try-catch 的文件
grep -rn 'catch\s*(\s*\\Throwable\|catch\s*(\s*\\Exception' app/admin/controller
```
两个结果取交集文件,逐个 Read 确认:
- 抛异常型响应success/error/result/redirect是否在 try 内
- 在 try 内的话,是否有 `HttpResponseException` 放行(范式 B
## 八、判定原则
- **不动框架内核**`JumpTraitBase` 的 throw 是 ThinkPHP 的标准契约success/error/result 均 throw且框架维护原则为"稳定性优先/向下兼容",改它会破坏既有行为
- **靠开发者在 `app/` 层遵守本规则规避**:响应放 try 外(范式 A或显式放行 HttpResponseException范式 B
## 相关技能
- [ulthon-page-api-dual-mode](../skills/ulthon-page-api-dual-mode/SKILL.md)success/error 的 JSON 响应格式与触发条件

View File

@@ -0,0 +1,276 @@
# 系统配置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`

View File

@@ -87,9 +87,11 @@
|---------|--------|------|
| [ulthon-naming-convention.md](./.agents/rules/ulthon-naming-convention.md) | 命名规范 | 目录命名与 PHP 文件命名约定 |
| [ulthon-controller-url.md](./.agents/rules/ulthon-controller-url.md) | 控制器路由 | URL 与控制器/方法的映射规则 |
| [ulthon-controller-response-throw.md](./.agents/rules/ulthon-controller-response-throw.md) | 控制器响应 | success/error/result/redirect 是 throw 不是 returntry-catch 内调用的陷阱与正确范式 |
| [ulthon-deploy-environment.md](./.agents/rules/ulthon-deploy-environment.md) | 部署与命令执行 | 部署栈模式与 Docker/宿主机命令判断 |
| [ulthon-source-directory.md](./.agents/rules/ulthon-source-directory.md) | source/ 目录 | 子项目/多端代码的目录约定与安全要求 |
| [ulthon-file-override-mechanism.md](./.agents/rules/ulthon-file-override-mechanism.md) | 文件加载机制 | app/ 覆盖 extend/base/ 的三类文件级覆盖机制与反例 |
| [ulthon-system-config.md](./.agents/rules/ulthon-system-config.md) | 系统配置sysconfig | 后台可变配置的存储、读取、保存、视图扩展规范 |
> 使用者业务规则索引见 `.agents/PROJECT.md` 的「规则索引」章节。
@@ -102,6 +104,7 @@ Skills 是"按场景调用的工作流说明",统一以 `.agents/skills/*/SKIL
- Base/App 架构与扩展指南(含身份分章节):[ulthon-base-app-architecture](./.agents/skills/ulthon-base-app-architecture/SKILL.md)
- Scheme + CURD 工作流:[ulthon-scheme-curd-workflow](./.agents/skills/ulthon-scheme-curd-workflow/SKILL.md)
- Scheme 定义指南(含表结构规范:特殊字段、字段后缀、组件类型、关联表参数):[ulthon-scheme-definition](./.agents/skills/ulthon-scheme-definition/SKILL.md)
- 后台表格机制与定制(按钮/字段/搜索/页面分类 A/B/C/D[ulthon-admin-table](./.agents/skills/ulthon-admin-table/SKILL.md)
- 数据库调试命令tools:db[ulthon-db-tools-debug](./.agents/skills/ulthon-db-tools-debug/SKILL.md)
- HTTP 调用工具tools:http:call[ulthon-tools-http-call](./.agents/skills/ulthon-tools-http-call/SKILL.md)
- 内置定时器与定时任务扩展含多节点协调、run_type 调度):[ulthon-timer](./.agents/skills/ulthon-timer/SKILL.md)
@@ -110,6 +113,7 @@ Skills 是"按场景调用的工作流说明",统一以 `.agents/skills/*/SKIL
- 权限与角色管理RBAC CLI[ulthon-permission-cli](./.agents/skills/ulthon-permission-cli/SKILL.md)
- 菜单管理admin:menu:\* CLI[ulthon-admin-menu-cli](./.agents/skills/ulthon-admin-menu-cli/SKILL.md)
- 测试工作流(设计哲学/决策/约束/运行/编写/回归保护):[ulthon-testing](./.agents/skills/ulthon-testing/SKILL.md)
- 页面验证 AI 自测零项目依赖6 步 checklist[ulthon-page-qa](./.agents/skills/ulthon-page-qa/SKILL.md)
- 框架更新工作流admin:update 同步上游):[ulthon-update-workflow](./.agents/skills/ulthon-update-workflow/SKILL.md)
- 零散规则管理(新增/维护 `.agents/rules/`[ulthon-rules-manager](./.agents/skills/ulthon-rules-manager/SKILL.md)