Files
go-admin/AGENTS.md
T
zhangwenjian ab0e8e6056 docs📝: add the new-business-module skill, and keep the rest of .claude out
The skill walks a single-table CRUD module end to end: migration, the
Actions-mode model, dto and router, and the sys_menu / sys_api /
casbin_rule seed data without which the module builds but never appears.

.claude was ignored wholesale. Un-ignoring the skills directory would
have committed every skill put there, including personal ones, so the
skills that ship are re-included one directory at a time.

AGENTS.md now points at 1786700001000_demo_menu.go for the seed data,
which is the runnable version of what the skill describes.
2026-08-23 13:20:26 +08:00

7.7 KiB
Raw Blame History

AGENTS.md — go-admin 后端

给 AI 编码工具与新贡献者的约定。只写"不遵守就会出错"的规则;技术栈版本以 go.mod 为准,命令以 Makefile 为准,此处不复述,避免与代码脱节。

标准 CRUD 模块的完整写法见 app/demo/ —— 那是可编译、有测试、CI 会跑的参照物。 本文与它冲突时,以 app/demo/ 为准。

分层

Router  →  Api      →  Service      →  Model
路由注册    参数绑定      业务逻辑         GORM 结构体
中间件链    调用 Service  操作数据库       TableName()

对应目录:app/{模块}/router|apis|service|modelsDTO 位于 service/dto

不可跨层Api 不直接操作 OrmService 不接触 gin.Context

优先使用通用 Action

单表 CRUD 不要手写 Handler 与 Servicecommon/actions 提供的五个 Action 已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应:

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)))
}

这样一个模块只需 model + dto + router 三个文件,完整示例见 app/demo/

使用通用 Action 的前提:

  • Model 实现 models.ActiveRecordGenerate / GetId / TableName
  • 列表 DTO 实现 dto.Index,增改删 DTO 实现 dto.Control
  • 所有 Generate() 必须返回副本 —— Action 在并发请求间复用实例, 就地返回会串数据(app/demo 的测试锁定了这一点)
  • 详情/删除 DTO 内嵌 dto.ObjectById 即可继承 BindGetId,无需重写

仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Handler 与 Service,写法见下。

Api 层(仅在通用 Action 不适用时)

结构体嵌入 api.Api,链式初始化后必须检查 Errors

func (e SysPost) GetPage(c *gin.Context) {
    s := service.SysPost{}
    req := dto.SysPostPageReq{}
    err := e.MakeContext(c).MakeOrm().Bind(&req, binding.Form).MakeService(&s.Service).Errors
    if err != nil {
        e.Logger.Error(err)
        e.Error(500, err, err.Error())
        return
    }
    // ... 调用 s.GetPage(...)
    e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}

响应一律走 e.OK / e.PageOK / e.Error,不要自行 c.JSON

Service 层(仅在通用 Action 不适用时)

结构体嵌入 service.Service(持有 OrmLog)。查询通过 Scopes 组合:

err = e.Orm.Model(&data).Scopes(
    cDto.MakeCondition(c.GetNeedSearch()),          // 由 search tag 生成 WHERE
    cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
    actions.Permission(data.TableName(), p),        // 数据权限,列表/详情必须带
).Find(list).Limit(-1).Offset(-1).Count(count).Error

遗漏 actions.Permission 会使数据权限配置静默失效 —— 这是最容易出的错。

错误一律 return err 向上传递,日志用 e.Log.Errorf,不使用 panic

DTO

搜索条件由 tag 声明,MakeCondition 据此拼 SQL

type SysPostPageReq struct {
    dto.Pagination `search:"-"`
    PostName string `form:"postName" search:"type:contains;column:post_name;table:sys_post"`
}
func (m *SysPostPageReq) GetNeedSearch() interface{} { return *m }

type 可选:exact iexact contains gt gte lt lte order left(联表)。

Model

type SysPost struct {
    PostId int `gorm:"primaryKey;autoIncrement" json:"postId"`
    // ... 业务字段
    models.ControlBy   // CreateBy / UpdateBy
    models.ModelTime   // CreatedAt / UpdatedAt / DeletedAt
}
func (SysPost) TableName() string { return "sys_post" }

TableName() 必须显式声明(GORM 配置了 SingularTable,不会自动推导复数)。

路由注册

通过 init() 自注册,不在中心文件手工添加:

func init() { routerCheckRole = append(routerCheckRole, registerSysPostRouter) }

func registerSysPostRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
    api := apis.SysPost{}
    r := v1.Group("/post").
        Use(authMiddleware.MiddlewareFunc()).
        Use(middleware.AuthCheckRole()).      // Casbin 鉴权
        Use(actions.PermissionAction())       // 注入数据权限
    { r.GET("", api.GetPage); r.POST("", api.Insert); /* ... */ }
}

新增路由文件后,需确认 cmd/api/ 中已用 _ 导入该包。

命名

对象 规则 示例
数据表 sys_ 前缀 + 下划线 sys_post
API 路径 /api/v1/ + kebab-case /api/v1/sys-user
DTO {Model}{Action}Req SysPostPageReq
权限标识 模块:资源:操作 admin:sysPost:add

权限标识需与前端 v-permisaction 一致,并写入 sys_menu 种子数据——完整可运行的 参照见 cmd/migrate/migration/version/1786700001000_demo_menu.gosys_api / sys_menu / sys_menu_api_rule / casbin_rule 四张表如何配齐,用的是幂等 upsert, 可以直接照抄结构)。

Swagger

Handler 必须带完整注解,go generate 会据此生成文档:

// @Summary 岗位列表
// @Tags 岗位
// @Success 200 {object} response.Response
// @Router /api/v1/post [get]
// @Security Bearer

本地运行

配置 driver: sqlite3 时必须带构建标签,否则启动即 panic

go run -tags sqlite3 . migrate -c config/settings.sqlite.yml
go run -tags sqlite3 . server  -c config/settings.sqlite.yml

原因:common/database/open.go//go:build !sqlite3,不加标签时编进的是 不含 sqlite3 的版本,opens["sqlite3"] 为 nil,调用时在 nil 函数上崩溃。 报错信息不会提到构建标签,容易误判成环境损坏。MySQL / PostgreSQL 无此问题。

对应 Makefilebuild-sqlite 目标。

数据库迁移

文件名前 13 位为时间戳版本号。已执行过的迁移文件不可修改 —— sys_migration 表按版本号去重,改动不会重跑,只能新增一个迁移来修正。

放哪个目录取决于身份:

目录 用途 是否入库
version/ 框架自带迁移,随仓库分发给所有使用者
version-local/ 使用者自己项目的迁移 否(已在 .gitignore

向本仓库提交迁移必须放 version/ —— 放进 version-local/ 会被忽略掉, git status 看不到,PR 里也不会出现。两个目录的包名分别是 versionversion_local(后者与目录名不一致,因为标识符不能含连字符)。

提交规范

格式 type+emoji: 描述

feat✨ fix🐛 style💄 docs📝 perf👌 test✅ refactor🎨 chore🔧

一个提交只做一件事。改动跨越多个语义时拆分提交,不要混在一起。

红线

  • 不使用全局 DB 变量,一律用 e.Orm(来自请求上下文,多租户依赖它)
  • 不在 Service 中引用 gin.Context
  • 生产部署前确认 mode: prod 且已修改 jwt.secret(dev 模式下 token 几乎不过期)
  • 不提交 config/settings.yml 中的真实凭据