mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-28 04:22:13 +00:00
docs📝: teach the generic actions instead of Generate()
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.
This commit is contained in:
@@ -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/` 会被忽略,别人拉代码看不到 |
|
||||
|
||||
@@ -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,写法见下。
|
||||
|
||||
+2
-2
@@ -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` 闭包,方言从闭包里那个
|
||||
|
||||
Reference in New Issue
Block a user