diff --git a/app/demo/models/demo_product.go b/app/demo/models/demo_product.go new file mode 100644 index 00000000..fb616730 --- /dev/null +++ b/app/demo/models/demo_product.go @@ -0,0 +1,37 @@ +package models + +import ( + "go-admin/common/models" +) + +// DemoProduct 示例模型 +// +// 内嵌 ControlBy 与 ModelTime 后,创建人/更新人与时间戳由框架自动维护; +// 数据权限(actions.Permission)正是按 create_by 过滤,缺少 ControlBy 会使其失效。 +type DemoProduct struct { + models.Model + + Name string `json:"name" gorm:"size:128;comment:名称"` + Code string `json:"code" gorm:"size:64;comment:编码"` + Price float64 `json:"price" gorm:"comment:单价"` + Status string `json:"status" gorm:"size:4;comment:状态"` + Remark string `json:"remark" gorm:"size:255;comment:备注"` + + models.ControlBy + models.ModelTime +} + +func (DemoProduct) TableName() string { + return "demo_product" +} + +// Generate 返回副本,供通用 Action 使用。 +// 必须返回新实例:Action 在并发请求间复用同一个模型指针,就地返回会串数据。 +func (e *DemoProduct) Generate() models.ActiveRecord { + o := *e + return &o +} + +func (e *DemoProduct) GetId() interface{} { + return e.Id +} diff --git a/app/demo/router/demo_product.go b/app/demo/router/demo_product.go new file mode 100644 index 00000000..b1f7b8de --- /dev/null +++ b/app/demo/router/demo_product.go @@ -0,0 +1,49 @@ +package router + +import ( + "github.com/gin-gonic/gin" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + + "go-admin/app/demo/models" + "go-admin/app/demo/service/dto" + "go-admin/common/actions" + "go-admin/common/middleware" +) + +// 路由通过 init 自注册,无需在任何中心文件登记。 +// 新建应用时用 `go run main.go app -n <名称>` 生成骨架, +// 它会同时产出 cmd/api/<名称>.go 完成注册。 +func init() { + routerCheckRole = append(routerCheckRole, registerDemoProductRouter) +} + +// registerDemoProductRouter 标准 CRUD 的推荐写法。 +// +// 五个通用 Action 覆盖了增删改查的全部样板逻辑——参数绑定、数据权限过滤、 +// 操作人注入、分页、错误响应,因此本模块没有 apis 与 service 文件。 +// +// 仅当业务逻辑超出单表 CRUD(如跨表事务、外部调用、复杂校验)时,才需要 +// 自行编写 Handler 与 Service,写法参照 app/admin/apis/sys_post.go。 +func registerDemoProductRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { + r := v1.Group("/demo-product"). + Use(authMiddleware.MiddlewareFunc()). // JWT 认证 + Use(middleware.AuthCheckRole()) // Casbin 鉴权 + { + m := &models.DemoProduct{} + + // actions.PermissionAction() 注入数据权限上下文, + // 列表与详情缺少它会绕过 DataScope 过滤 + 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))) + } +} diff --git a/app/demo/router/router.go b/app/demo/router/router.go new file mode 100644 index 00000000..8d8141db --- /dev/null +++ b/app/demo/router/router.go @@ -0,0 +1,74 @@ +package router + +import ( + "github.com/gin-gonic/gin" + _ "github.com/gin-gonic/gin" + log "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk" + // "github.com/go-admin-team/go-admin-core/sdk/pkg" + "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + common "go-admin/common/middleware" + "os" +) + +var ( + routerNoCheckRole = make([]func(*gin.RouterGroup), 0) + routerCheckRole = make([]func(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware), 0) +) + +// InitRouter 路由初始化 +func InitRouter() { + var r *gin.Engine + h := sdk.Runtime.GetEngine() + if h == nil { + h = gin.New() + sdk.Runtime.SetEngine(h) + } + switch h.(type) { + case *gin.Engine: + r = h.(*gin.Engine) + default: + log.Fatal("not support other engine") + os.Exit(-1) + } + + // the jwt middleware + authMiddleware, err := common.AuthInit() + if err != nil { + log.Fatalf("JWT Init Error, %s", err.Error()) + } + + // 注册业务路由 + InitBusinessRouter(r, authMiddleware) +} + +func InitBusinessRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.Engine { + + // 无需认证的路由 + noCheckRoleRouter(r) + // 需要认证的路由 + checkRoleRouter(r, authMiddleware) + + return r +} + +// noCheckRoleRouter 无需认证的路由 +func noCheckRoleRouter(r *gin.Engine) { + // 可根据业务需求来设置接口版本 + v := r.Group("/api/v1") + + for _, f := range routerNoCheckRole { + f(v) + } +} + +// checkRoleRouter 需要认证的路由 +func checkRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddleware) { + // 可根据业务需求来设置接口版本 + v := r.Group("/api/v1") + + for _, f := range routerCheckRole { + f(v, authMiddleware) + } +} diff --git a/app/demo/service/dto/demo_product.go b/app/demo/service/dto/demo_product.go new file mode 100644 index 00000000..7516794b --- /dev/null +++ b/app/demo/service/dto/demo_product.go @@ -0,0 +1,96 @@ +package dto + +import ( + "github.com/gin-gonic/gin" + + "go-admin/app/demo/models" + "go-admin/common/dto" + common "go-admin/common/models" +) + +// DemoProductSearch 列表查询条件 +// +// search tag 决定 MakeCondition 拼出的 WHERE: +// +// exact 精确匹配 / icontains 忽略大小写模糊 / gte 大于等于 … +// +// 未打 search tag 的字段不参与查询,可避免无意间开放过滤维度。 +type DemoProductSearch struct { + dto.Pagination `search:"-"` + + Name string `form:"name" search:"type:icontains;column:name;table:demo_product"` + Code string `form:"code" search:"type:exact;column:code;table:demo_product"` + Status string `form:"status" search:"type:exact;column:status;table:demo_product"` + + DemoProductOrder +} + +// DemoProductOrder 排序字段单独成组,避免与查询字段混在一起 +type DemoProductOrder struct { + CreatedAtOrder string `form:"createdAtOrder" search:"type:order;column:created_at;table:demo_product"` +} + +func (m *DemoProductSearch) GetNeedSearch() interface{} { return *m } + +func (m *DemoProductSearch) Bind(ctx *gin.Context) error { + return ctx.ShouldBind(m) +} + +func (m *DemoProductSearch) Generate() dto.Index { + o := *m + return &o +} + +// DemoProductControl 新增与修改共用的入参 +// +// 通用 Action(Create / Update)通过 GenerateM 拿到落库对象, +// 因此这里不直接暴露 Model,字段校验用 validate tag 声明。 +type DemoProductControl struct { + Id int `json:"id" comment:"主键"` + Name string `json:"name" comment:"名称" validate:"required"` + Code string `json:"code" comment:"编码" validate:"required"` + Price float64 `json:"price" comment:"单价" validate:"gte=0"` + Status string `json:"status" comment:"状态"` + Remark string `json:"remark" comment:"备注"` +} + +func (s *DemoProductControl) Bind(ctx *gin.Context) error { + return ctx.ShouldBind(s) +} + +func (s *DemoProductControl) Generate() dto.Control { + o := *s + return &o +} + +func (s *DemoProductControl) GetId() interface{} { return s.Id } + +// GenerateM 组装落库对象。CreateBy / UpdateBy 由通用 Action 在此之后注入, +// 此处不要手动赋值。 +func (s *DemoProductControl) GenerateM() (common.ActiveRecord, error) { + return &models.DemoProduct{ + Model: common.Model{Id: s.Id}, + Name: s.Name, + Code: s.Code, + Price: s.Price, + Status: s.Status, + Remark: s.Remark, + }, nil +} + +// DemoProductById 详情与删除共用,支持单个 id 与批量 ids +type DemoProductById struct { + dto.ObjectById +} + +// Bind 与 GetId 由内嵌的 dto.ObjectById 提供:它已处理好 uri 绑定、 +// DELETE 时的批量 ids 合并与参数校验,无需在此重复实现。 + +func (s *DemoProductById) Generate() dto.Control { + o := *s + return &o +} + +func (s *DemoProductById) GenerateM() (common.ActiveRecord, error) { + return &models.DemoProduct{}, nil +} diff --git a/app/demo/service/dto/demo_product_test.go b/app/demo/service/dto/demo_product_test.go new file mode 100644 index 00000000..32b1a7f3 --- /dev/null +++ b/app/demo/service/dto/demo_product_test.go @@ -0,0 +1,91 @@ +package dto + +import ( + "testing" + + "go-admin/app/demo/models" + "go-admin/common/dto" + common "go-admin/common/models" +) + +// 通用 Action 依赖 DTO 与 Model 实现一组接口。这些约束在编译期无法完全覆盖 +// (接口是在路由注册处才被要求的),因此用测试锁定,避免改动后在运行时才暴露。 + +func TestImplementsIndexInterface(t *testing.T) { + var _ dto.Index = (*DemoProductSearch)(nil) +} + +func TestImplementsControlInterface(t *testing.T) { + var _ dto.Control = (*DemoProductControl)(nil) + var _ dto.Control = (*DemoProductById)(nil) +} + +func TestModelImplementsActiveRecord(t *testing.T) { + var _ common.ActiveRecord = (*models.DemoProduct)(nil) +} + +// Generate 必须返回副本:通用 Action 在并发请求间复用同一个实例, +// 就地返回会导致请求之间串数据。 +func TestGenerateReturnsCopy(t *testing.T) { + src := &DemoProductControl{Id: 1, Name: "原始"} + got := src.Generate().(*DemoProductControl) + + if got == src { + t.Fatal("Generate 返回了同一指针,应返回副本") + } + got.Name = "被修改" + if src.Name != "原始" { + t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name) + } +} + +func TestSearchGenerateReturnsCopy(t *testing.T) { + src := &DemoProductSearch{Name: "原始"} + got := src.Generate().(*DemoProductSearch) + + if got == src { + t.Fatal("Generate 返回了同一指针,应返回副本") + } + got.Name = "被修改" + if src.Name != "原始" { + t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name) + } +} + +func TestModelGenerateReturnsCopy(t *testing.T) { + src := &models.DemoProduct{Name: "原始"} + got := src.Generate().(*models.DemoProduct) + + if got == src { + t.Fatal("Generate 返回了同一指针,应返回副本") + } + got.Name = "被修改" + if src.Name != "原始" { + t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name) + } +} + +// GenerateM 组装落库对象,主键需正确传递,否则更新会退化成插入。 +func TestGenerateMCarriesId(t *testing.T) { + c := &DemoProductControl{Id: 42, Name: "示例", Code: "P-42", Price: 9.9} + m, err := c.GenerateM() + if err != nil { + t.Fatalf("GenerateM 返回错误: %v", err) + } + p, ok := m.(*models.DemoProduct) + if !ok { + t.Fatalf("GenerateM 返回类型错误: %T", m) + } + if p.Id != 42 { + t.Errorf("主键未传递: got %d, want 42", p.Id) + } + if p.Name != "示例" || p.Code != "P-42" || p.Price != 9.9 { + t.Errorf("字段映射有误: %+v", p) + } +} + +func TestTableName(t *testing.T) { + if got := (models.DemoProduct{}).TableName(); got != "demo_product" { + t.Errorf("TableName() = %q, want %q", got, "demo_product") + } +} diff --git a/cmd/api/demo.go b/cmd/api/demo.go new file mode 100644 index 00000000..ac3503d9 --- /dev/null +++ b/cmd/api/demo.go @@ -0,0 +1,8 @@ +package api + +import "go-admin/app/demo/router" + +func init() { + //注册路由 fixme 其他应用的路由,在本目录新建文件放在init方法 + AppRouters = append(AppRouters, router.InitRouter) +} \ No newline at end of file