diff --git a/.agents/rules/ulthon-controller-response-throw.md b/.agents/rules/ulthon-controller-response-throw.md new file mode 100644 index 0000000..9439460 --- /dev/null +++ b/.agents/rules/ulthon-controller-response-throw.md @@ -0,0 +1,116 @@ +# 控制器响应是 throw(success/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 误吞响应异常。 + +## 四、正确范式 B(catch 前放行 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 响应格式与触发条件 diff --git a/.agents/rules/ulthon-system-config.md b/.agents/rules/ulthon-system-config.md new file mode 100644 index 0000000..ab23157 --- /dev/null +++ b/.agents/rules/ulthon-system-config.md @@ -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 匹配。**每个配置表单必须包含 ``**。 + +## 二、配置视图结构 + +### 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 +