From eb4e0f21dfd9d5f8010fd9f115a64ad746477ddf Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 20:46:21 +0800 Subject: [PATCH] =?UTF-8?q?docs=F0=9F=93=9D:=20teach=20the=20generic=20act?= =?UTF-8?q?ions=20instead=20of=20Generate()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md, docs/contract.md and the new-business-module skill showed the older actions and made returning a copy from Generate() a rule to remember. They now show the generic actions, whose type parameters are the rule, and say the Generate() rule holds only for modules still on the deprecated actions. --- .claude/skills/new-business-module/SKILL.md | 18 ++++++----- AGENTS.md | 34 ++++++++++----------- docs/contract.md | 4 +-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/.claude/skills/new-business-module/SKILL.md b/.claude/skills/new-business-module/SKILL.md index 1cc0b66c..c0a93e13 100644 --- a/.claude/skills/new-business-module/SKILL.md +++ b/.claude/skills/new-business-module/SKILL.md @@ -35,15 +35,17 @@ description: Scaffold a new single-table CRUD business module end to end — mig ### 3. 生成 model / dto / router 三个文件(Actions 模式) -不要手写 Api 与 Service。使用 `common/actions` 的通用 Action,一个模块只需 -model、dto、router 三个文件,完整写法照抄 `app/demo/` 的结构。 +不要手写 Api 与 Service。使用 `common/actions` 的泛型 Action +(`actions.Index[Model, Search]()` 等),一个模块只需 model、dto、router +三个文件,完整写法照抄 `app/demo/` 的结构。 -**关键正确性要求**(这三条是实际出问题最多的地方): +**关键正确性要求**: -- Model 实现 `models.ActiveRecord`(`Generate` / `GetId` / `TableName`), - `TableName()` 必须显式声明——GORM 配置了 `SingularTable`,不会自动推导 -- **`Generate()` 必须返回副本,不要就地返回**——Action 在并发请求间复用实例, - 就地返回会导致请求之间串数据;这个问题单人测试时几乎不出现,上线后才暴露 +- `TableName()` 必须显式声明——GORM 配置了 `SingularTable`,不会自动推导 +- 增改 DTO 写 `ToModel() (*Model, error)`;模型与 DTO 配错会编译不过 +- **不要写 `Generate()`**,也不要用旧的 `IndexAction` 等五个(已 Deprecated): + 旧写法在并发请求间复用实例,`Generate()` 一旦就地返回就会串数据, + 单人测试时几乎不出现、上线后才暴露;泛型 Action 每个请求新建自己的值 - 完成后确认 `cmd/api/` 中已用 `_` 导入新包,否则路由不会被注册 ### 4. 写菜单、接口与权限种子数据 @@ -82,7 +84,7 @@ model、dto、router 三个文件,完整写法照抄 `app/demo/` 的结构。 | 检查项 | 出错后果 | | --- | --- | -| `Generate()` 是否返回副本 | 并发请求之间串数据 | +| 路由是否用泛型 Action 而非旧的 `IndexAction` 等 | 旧写法要靠 `Generate()` 返回副本,漏了就并发串数据 | | 是否使用 `e.Orm` 而非全局 DB | 多租户下拿到错误的数据库连接 | | `TableName()` 是否显式声明 | GORM 不会自动推导 | | 迁移文件是否放在 `version/` | 放进 `version-local/` 会被忽略,别人拉代码看不到 | diff --git a/AGENTS.md b/AGENTS.md index b3e221be..01f544a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,34 +20,34 @@ Router → Api → Service → Model ## 优先使用通用 Action -单表 CRUD **不要手写 Api 与 Service**。`common/actions` 提供的五个 -Action 已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应: +单表 CRUD **不要手写 Api 与 Service**。`common/actions` 提供的泛型 Action +已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应: ```go r := v1.Group("/demo-product").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) { - m := &models.DemoProduct{} - r.GET("", actions.PermissionAction(), actions.IndexAction(m, new(dto.DemoProductSearch), func() interface{} { - list := make([]models.DemoProduct, 0); return &list - })) - r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.DemoProductById), func() interface{} { - return &models.DemoProduct{} - })) - r.POST("", actions.CreateAction(new(dto.DemoProductControl))) - r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.DemoProductControl))) - r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.DemoProductById))) + r.GET("", actions.PermissionAction(), actions.Index[models.DemoProduct, dto.DemoProductSearch]()) + r.GET("/:id", actions.PermissionAction(), actions.View[models.DemoProduct, dto.DemoProductById]()) + r.POST("", actions.Create[models.DemoProduct, dto.DemoProductControl]()) + r.PUT("/:id", actions.PermissionAction(), actions.Update[models.DemoProduct, dto.DemoProductControl]()) + r.DELETE("", actions.PermissionAction(), actions.Delete[models.DemoProduct, dto.DemoProductById]()) } ``` 这样一个模块只需 **model + dto + router** 三个文件,完整示例见 `app/demo/`。 -使用通用 Action 的前提: +类型参数就是约束,写错了编译不过: -- Model 实现 `models.ActiveRecord`(`Generate` / `GetId` / `TableName`) -- 列表 DTO 实现 `dto.Index`,增改删 DTO 实现 `dto.Control` -- **所有 `Generate()` 必须返回副本** —— Action 在并发请求间复用实例, - 就地返回会串数据(`app/demo` 的测试锁定了这一点) +- Model 需有 `TableName` / `GetId`,并内嵌 `models.ControlBy`(提供 `SetCreateBy` / `SetUpdateBy`) +- 列表 DTO:`Bind`、`GetNeedSearch`,内嵌 `dto.Pagination` +- 增改 DTO:`Bind` 与 `ToModel() (*Model, error)`——配错模型编译不过 - 详情/删除 DTO 内嵌 `dto.ObjectById` 即可继承 `Bind` 与 `GetId`,无需重写 +- 详情要返回与模型不同的结构时用 `ViewAs[Model, ById, Response]()`(见 `app/jobs`) +- **不需要 `Generate()`**:每个请求都新建自己的值,不存在跨请求共享的实例 + +旧的 `IndexAction` 等五个仍可用,已标记 Deprecated。它们在并发请求间复用 +注册时传入的实例,所以依赖「所有 `Generate()` 必须返回副本」这条约定—— +仍在用旧写法的模块要继续遵守它。 仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Api 与 Service,写法见下。 diff --git a/docs/contract.md b/docs/contract.md index 599e7f7f..d0a88070 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -356,11 +356,11 @@ if res.RowsAffected == 0 { return ErrAlreadyPaid } // 别人先改了 | `api.Api` | core `sdk/api` | 一条链式糖:`MakeContext` / `Bind` / `MakeOrm` / `OK` / `PageOK` / `Error` | | `service.Service` | core `sdk/service` | 一个装 `Orm` / `Log` / `Cache` / `Error` 的结构体加一个 `AddError` | | `MakeCondition` / `search` tag | core `sdk/contract/dto` | 把 DTO 上的 `search:"type:exact;column:name;table:xx"` 翻成 WHERE | -| 通用 CRUD Action | go-admin `common/actions` | `IndexAction` 等五个。**留在 go-admin,没有下沉** | +| 通用 CRUD Action | go-admin `common/actions` | 泛型的 `Index` / `View` / `ViewAs` / `Create` / `Update` / `Delete`(旧的 `IndexAction` 等五个已标 Deprecated)。**留在 go-admin,没有下沉** | 最后一行是有意的:CRUD Action 是最需要演进的一类东西(分页参数、批量操作、 软删语义、字段级权限),而 core 的每一个导出都是永久承诺——放进去容易, -拿出来不可能。想用就把那 294 行抄走,抄走的那份还能按你自己的需要改。 +拿出来不可能。想用就把 `common/actions` 抄走,抄走的那份还能按你自己的需要改。 主仓唯一的真实业务模块 `app/admin` **一个 CRUD Action 都没用**,全是手写 Service。 `MakeCondition` 返回的是 `func(db *gorm.DB) *gorm.DB` 闭包,方言从闭包里那个