Files
goalfylearning-admin/discuss/architecture_guide.md

33 KiB
Raw Blame History

GoalfyMax Admin 项目架构探索指南

1. 项目整体架构

1.1 目录结构

/Users/youziba/goalfyagent/goalfymax-admin/
├── cmd/
│   └── server/
│       └── main.go                 # 应用入口点
├── internal/
│   ├── api/
│   │   ├── handlers/               # HTTP 请求处理层
│   │   │   ├── user_level_config_handler.go
│   │   │   ├── page_handler.go
│   │   │   ├── role_handler.go
│   │   │   └── ...
│   │   ├── middlewares/            # 中间件
│   │   └── routes/
│   │       └── routes.go           # 路由配置
│   ├── config/
│   │   └── config.go               # 配置管理
│   ├── models/                     # 数据模型和请求/响应
│   │   ├── user_level_config.go
│   │   ├── rbac.go                 # 包含 Page, RolePagePermission 等
│   │   ├── common.go               # BaseModel, User, Role 等
│   │   ├── request.go              # 所有请求模型
│   │   └── response.go
│   ├── services/                   # 业务逻辑层
│   │   ├── user_level_config_service.go
│   │   ├── page_service.go
│   │   └── ...
│   └── storage/                    # 数据访问层
│       ├── user_level_config_storage.go
│       ├── page_storage.go
│       ├── database.go             # 数据库初始化和迁移
│       └── ...
├── pkg/
│   ├── middleware/                 # 通用中间件 (RBAC, SSO等)
│   └── utils/                      # 工具函数
├── etc/
│   ├── config.yaml                 # 开发配置
│   └── config-prod.yaml            # 生产配置
└── go.mod / go.sum

1.2 技术栈

  • 框架: Gin (Web 框架)
  • 数据库: MySQL + GORM (ORM)
  • 日志: Zap (结构化日志)
  • 配置: Viper (YAML 配置)
  • 认证: SSO + JWT
  • 架构模式: 三层架构 (Handler -> Service -> Storage)

2. 菜单系统实现 (页面管理)

2.1 菜单系统术语澄清

在此项目中,"菜单" 通过 "页面"(Page) 概念实现。每个页面对应前端的一个菜单项。

2.2 核心模型

文件: /Users/youziba/goalfyagent/goalfymax-admin/internal/models/rbac.go

// Page 页面模型(对应菜单项)
type Page struct {
    BaseModel
    Name      string `gorm:"size:50;not null" json:"name"`          // 菜单名称
    Path      string `gorm:"uniqueIndex;size:100;not null" json:"path"` // 菜单路径
    Icon      string `gorm:"size:50" json:"icon"`                   // 菜单图标
    SortOrder int    `gorm:"default:0" json:"sortOrder"`            // 排序顺序
    IsActive  bool   `gorm:"default:true" json:"isActive"`          // 是否激活
}

// RolePagePermission 角色-页面权限关联
type RolePagePermission struct {
    BaseModel
    RoleID uint `gorm:"not null" json:"roleId"`
    PageID uint `gorm:"not null" json:"pageId"`
    Role   Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
    Page   Page `gorm:"foreignKey:PageID" json:"page,omitempty"`
}

表名映射:

  • admin_pages - 页面/菜单表
  • admin_role_page_permissions - 角色-页面权限关联表

2.3 页面管理 API 路由

文件: /Users/youziba/goalfyagent/goalfymax-admin/internal/api/routes/routes.go (第174-181行)

// 页面管理 - 所有路由通过页面权限检查
pages := admin.Group("/pages")
{
    pages.GET("", pageHandler.List)          // 获取页面列表
    pages.POST("", pageHandler.Create)       // 创建页面
    pages.GET("/:id", pageHandler.GetByID)   // 获取页面详情
    pages.PUT("/:id", pageHandler.Update)    // 更新页面
    pages.DELETE("/:id", pageHandler.Delete) // 删除页面
}

2.4 页面管理完整流程

a) 创建页面 (菜单项)

Handler (/internal/api/handlers/page_handler.go):

func (h *PageHandler) Create(c *gin.Context) {
    var req models.PageCreateRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }
    
    page, err := h.pageService.Create(&req)
    if err != nil {
        h.response.InternalServerError(c, err.Error())
        return
    }
    
    h.response.Success(c, page)
}

Request Model (/internal/models/request.go):

type PageCreateRequest struct {
    Name      string `json:"name" binding:"required,min=2,max=50"`
    Path      string `json:"path" binding:"required"`
    Icon      string `json:"icon"`
    SortOrder int    `json:"sortOrder"`
    IsActive  bool   `json:"isActive"`
}

Service (/internal/services/page_service.go):

func (s *pageService) Create(req *models.PageCreateRequest) (*models.Page, error) {
    // 检查页面路径是否已存在
    _, err := s.pageStorage.GetByPath(req.Path)
    if err == nil {
        return nil, errors.New("页面路径已存在")
    }
    
    // 创建页面
    page := &models.Page{
        Name:      req.Name,
        Path:      req.Path,
        Icon:      req.Icon,
        SortOrder: req.SortOrder,
        IsActive:  req.IsActive,
    }
    
    err = s.pageStorage.Create(page)
    if err != nil {
        s.logger.Error("创建页面失败", zap.Error(err))
        return nil, errors.New("创建页面失败")
    }
    
    s.logger.Info("页面创建成功", zap.String("name", page.Name))
    return page, nil
}

Storage (/internal/storage/page_storage.go):

func (s *pageStorage) Create(page *models.Page) error {
    return s.db.Create(page).Error
}

b) 分配角色的页面权限

Handler (/internal/api/handlers/rbac_handler.go):

func (h *RBACHandler) AssignRolePagePermissions(c *gin.Context) {
    var req models.RolePagePermissionAssignRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }
    
    err := h.rbacService.AssignRolePagePermissions(req.RoleID, req.PageIDs)
    if err != nil {
        h.response.InternalServerError(c, err.Error())
        return
    }
    
    h.response.Success(c, nil)
}

2.5 权限检查流程

通过中间件动态检查用户对页面的访问权限,见 pkg/middleware/rbac.go


3. 现有配置模块实现 (用户等级配置)

3.1 模块文件清单

层级 文件位置 文件名
Models internal/models/ user_level_config.go
Storage internal/storage/ user_level_config_storage.go
Service internal/services/ user_level_config_service.go
Handler internal/api/handlers/ user_level_config_handler.go
Routes internal/api/routes/ routes.go (203-213行)

3.2 完整代码示例

a) 模型定义 (user_level_config.go)

package models

import "time"

// UserLevelConfig 用户等级配置表
type UserLevelConfig struct {
    ID              uint      `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
    LevelName       string    `json:"level_name" gorm:"not null;uniqueIndex:uk_level_name;type:varchar(50);comment:等级名称"`
    LevelCode       string    `json:"level_code" gorm:"not null;uniqueIndex:uk_level_code;type:varchar(50);comment:等级代码"`
    ProjectLimit    int       `json:"project_limit" gorm:"not null;default:0;comment:项目数限制0表示不限"`
    Description     string    `json:"description" gorm:"type:varchar(255);comment:等级描述"`
    SortOrder       int       `json:"sort_order" gorm:"not null;default:0;comment:排序顺序"`
    Status          int       `json:"status" gorm:"not null;default:1;comment:状态 1-启用 0-禁用"`
    CreatedAt       time.Time `json:"created_at"`
    UpdatedAt       time.Time `json:"updated_at"`
}

func (UserLevelConfig) TableName() string {
    return "admin_user_level_configs"
}

// 请求模型
type UserLevelConfigCreateRequest struct {
    LevelName    string `json:"level_name" binding:"required,min=1,max=50"`
    LevelCode    string `json:"level_code" binding:"required,min=1,max=50"`
    ProjectLimit int    `json:"project_limit" binding:"min=0"`
    Description  string `json:"description" binding:"max=255"`
    SortOrder    int    `json:"sort_order"`
}

type UserLevelConfigUpdateRequest struct {
    LevelName    string `json:"level_name" binding:"required,min=1,max=50"`
    ProjectLimit int    `json:"project_limit" binding:"min=0"`
    Description  string `json:"description" binding:"max=255"`
    SortOrder    int    `json:"sort_order"`
}

type UserLevelConfigListRequest struct {
    LevelName string `form:"level_name"`
    Status    *int   `form:"status"`
    Page      int    `form:"page,default=1"`
    Size      int    `form:"size,default=20"`
}

type UserLevelConfigUpdateStatusRequest struct {
    Status int `json:"status" binding:"required,oneof=0 1"`
}

b) 存储层接口 (user_level_config_storage.go)

package storage

import (
    "goalfymax-admin/internal/models"
    "gorm.io/gorm"
)

// 接口定义
type UserLevelConfigStorage interface {
    Create(config *models.UserLevelConfig) error
    GetByID(id uint) (*models.UserLevelConfig, error)
    GetByLevelCode(levelCode string) (*models.UserLevelConfig, error)
    Update(config *models.UserLevelConfig) error
    Delete(id uint) error
    List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error)
    UpdateStatus(id uint, status int) error
    GetAll() ([]models.UserLevelConfig, error)
}

// 实现
type userLevelConfigStorage struct {
    db *gorm.DB
}

func NewUserLevelConfigStorage() UserLevelConfigStorage {
    return &userLevelConfigStorage{db: DB}
}

func (s *userLevelConfigStorage) Create(config *models.UserLevelConfig) error {
    return s.db.Create(config).Error
}

func (s *userLevelConfigStorage) List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error) {
    var configs []models.UserLevelConfig
    var total int64

    query := s.db.Model(&models.UserLevelConfig{})

    if req.LevelName != "" {
        query = query.Where("level_name LIKE ?", "%"+req.LevelName+"%")
    }
    if req.Status != nil {
        query = query.Where("status = ?", *req.Status)
    }

    if err := query.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    offset := (req.Page - 1) * req.Size
    if err := query.Order("sort_order ASC, id DESC").Offset(offset).Limit(req.Size).Find(&configs).Error; err != nil {
        return nil, 0, err
    }

    return configs, total, nil
}

c) 服务层 (user_level_config_service.go)

package services

import (
    "errors"
    "goalfymax-admin/internal/models"
    "goalfymax-admin/internal/storage"
    "goalfymax-admin/pkg/utils"
    "go.uber.org/zap"
)

type UserLevelConfigService interface {
    Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error)
    GetByID(id uint) (*models.UserLevelConfig, error)
    Update(id uint, req *models.UserLevelConfigUpdateRequest) (*models.UserLevelConfig, error)
    Delete(id uint) error
    List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error)
    UpdateStatus(id uint, status int) error
    GetAll() ([]models.UserLevelConfig, error)
}

type userLevelConfigService struct {
    storage storage.UserLevelConfigStorage
    logger  *utils.Logger
}

func NewUserLevelConfigService(storage storage.UserLevelConfigStorage, logger *utils.Logger) UserLevelConfigService {
    return &userLevelConfigService{
        storage: storage,
        logger:  logger,
    }
}

func (s *userLevelConfigService) Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error) {
    // 检查等级代码是否已存在
    _, err := s.storage.GetByLevelCode(req.LevelCode)
    if err == nil {
        return nil, errors.New("等级代码已存在")
    }

    config := &models.UserLevelConfig{
        LevelName:    req.LevelName,
        LevelCode:    req.LevelCode,
        ProjectLimit: req.ProjectLimit,
        Description:  req.Description,
        SortOrder:    req.SortOrder,
        Status:       1, // 默认启用
    }

    err = s.storage.Create(config)
    if err != nil {
        s.logger.Error("创建用户等级配置失败", zap.Error(err))
        return nil, errors.New("创建用户等级配置失败")
    }

    s.logger.Info("用户等级配置创建成功", zap.String("level_name", config.LevelName))
    return config, nil
}

// ... 其他方法 Update, Delete, List 等

d) Handler 处理器 (user_level_config_handler.go)

package handlers

import (
    "goalfymax-admin/internal/models"
    "goalfymax-admin/internal/services"
    "goalfymax-admin/pkg/utils"
    "strconv"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

type UserLevelConfigHandler struct {
    service  services.UserLevelConfigService
    response *utils.Response
    logger   *utils.Logger
}

func NewUserLevelConfigHandler(service services.UserLevelConfigService, logger *utils.Logger) *UserLevelConfigHandler {
    return &UserLevelConfigHandler{
        service:  service,
        response: utils.NewResponse(),
        logger:   logger,
    }
}

func (h *UserLevelConfigHandler) Create(c *gin.Context) {
    var req models.UserLevelConfigCreateRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }

    config, err := h.service.Create(&req)
    if err != nil {
        h.logger.Error("创建用户等级配置失败", zap.Error(err))
        h.response.InternalServerError(c, err.Error())
        return
    }

    h.response.Success(c, config)
}

func (h *UserLevelConfigHandler) List(c *gin.Context) {
    var req models.UserLevelConfigListRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }

    if req.Page <= 0 {
        req.Page = 1
    }
    if req.Size <= 0 {
        req.Size = 20
    }

    configs, total, err := h.service.List(&req)
    if err != nil {
        h.logger.Error("获取用户等级配置列表失败", zap.Error(err))
        h.response.InternalServerError(c, "获取列表失败")
        return
    }

    h.response.Page(c, configs, total, req.Page, req.Size)
}

// ... 其他方法 Update, Delete, GetByID 等

4. Handler/Controller 层实现模式

4.1 Handler 结构

每个 Handler 都遵循相同的模式:

type [Resource]Handler struct {
    service  services.[Resource]Service  // 业务服务
    response *utils.Response              // 响应工具
    logger   *utils.Logger                // 日志
}

// 构造函数
func New[Resource]Handler(service services.[Resource]Service, logger *utils.Logger) *[Resource]Handler {
    return &[Resource]Handler{
        service:  service,
        response: utils.NewResponse(),
        logger:   logger,
    }
}

// 处理方法
func (h *[Resource]Handler) Create(c *gin.Context) { ... }
func (h *[Resource]Handler) GetByID(c *gin.Context) { ... }
func (h *[Resource]Handler) Update(c *gin.Context) { ... }
func (h *[Resource]Handler) Delete(c *gin.Context) { ... }
func (h *[Resource]Handler) List(c *gin.Context) { ... }

4.2 标准响应处理

Handler 使用 utils.Response 提供统一的响应:

h.response.Success(c, data)                    // 成功响应
h.response.BadRequest(c, "错误信息")           // 请求错误 (400)
h.response.NotFound(c, "资源不存在")           // 资源不存在 (404)
h.response.InternalServerError(c, "错误信息") // 服务器错误 (500)
h.response.ValidateError(c, err)              // 验证错误
h.response.Page(c, data, total, page, size)   // 分页响应

4.3 错误处理

if err := c.ShouldBindJSON(&req); err != nil {
    h.response.ValidateError(c, err)
    return
}

if err != nil {
    h.logger.Error("操作失败", zap.Error(err))
    h.response.InternalServerError(c, "错误消息")
    return
}

5. 路由配置

5.1 路由组织结构

文件: /Users/youziba/goalfyagent/goalfymax-admin/internal/api/routes/routes.go

func SetupRoutes(
    userService services.UserService,
    roleService services.RoleService,
    pageService services.PageService,
    quotaService services.QuotaService,
    ssoService services.SSOService,
    rbacService services.RBACService,
    userLevelConfigService services.UserLevelConfigService,
    logger *utils.Logger,
    appConfig *config.Config,
) *gin.Engine {
    r := gin.New()
    
    // 中间件设置
    r.Use(cors.New(cors.Config{...}))
    r.Use(middlewares.RequestLogMiddleware(logger))
    r.Use(gin.Recovery())
    
    // SSO 路由 (不需要认证)
    sso := api.Group("/sso")
    {
        sso.POST("/login", ssoHandler.HandleSSOLogin)
        // ...
    }
    
    // 管理员路由 (需要认证)
    admin := api.Group("/admin")
    admin.Use(authMiddleware.RequireAuth())
    {
        // 用户管理
        users := admin.Group("/users")
        {
            users.GET("", userHandler.List)
            users.POST("", userHandler.Create)
            users.GET("/:id", userHandler.GetByID)
            users.PUT("/:id", userHandler.Update)
            users.DELETE("/:id", userHandler.Delete)
        }
        
        // 页面管理
        pages := admin.Group("/pages")
        {
            pages.GET("", pageHandler.List)
            pages.POST("", pageHandler.Create)
            pages.GET("/:id", pageHandler.GetByID)
            pages.PUT("/:id", pageHandler.Update)
            pages.DELETE("/:id", pageHandler.Delete)
        }
        
        // 用户等级配置管理
        userLevelConfigs := admin.Group("/user-level-configs")
        {
            userLevelConfigs.GET("", userLevelConfigHandler.List)
            userLevelConfigs.GET("/all", userLevelConfigHandler.GetAll)
            userLevelConfigs.POST("", userLevelConfigHandler.Create)
            userLevelConfigs.GET("/:id", userLevelConfigHandler.GetByID)
            userLevelConfigs.PUT("/:id", userLevelConfigHandler.Update)
            userLevelConfigs.DELETE("/:id", userLevelConfigHandler.Delete)
            userLevelConfigs.PUT("/:id/status", userLevelConfigHandler.UpdateStatus)
        }
    }
    
    return r
}

5.2 路由注册步骤 (在 main.go 中)

// 创建服务实例
userLevelConfigService := services.NewUserLevelConfigService(
    storage.NewUserLevelConfigStorage(),
    logger,
)

// 设置路由并传入服务
router := routes.SetupRoutes(
    userService,
    roleService,
    pageService,
    quotaService,
    ssoService,
    rbacService,
    userLevelConfigService,  // 新增
    logger,
    cfg,
)

6. 数据库迁移

6.1 迁移方式

项目使用 GORM 的 AutoMigrate 自动迁移,无需手写 SQL 脚本。

文件: /Users/youziba/goalfyagent/goalfymax-admin/internal/storage/database.go

// AutoMigrate 自动迁移数据库表
func AutoMigrate() error {
    if DB == nil {
        return fmt.Errorf("数据库未初始化")
    }

    // 迁移所有模型
    err := DB.AutoMigrate(
        &models.UserLevelConfig{},
        // 添加其他模型...
    )

    if err != nil {
        return fmt.Errorf("数据库迁移失败: %w", err)
    }

    // 初始化默认数据
    if err := initDefaultUserLevelConfigs(); err != nil {
        return fmt.Errorf("初始化默认用户等级配置失败: %w", err)
    }

    return nil
}

// 初始化默认数据示例
func initDefaultUserLevelConfigs() error {
    var count int64
    if err := DB.Model(&models.UserLevelConfig{}).Count(&count).Error; err != nil {
        return err
    }

    if count > 0 {
        return nil
    }

    defaultConfigs := []models.UserLevelConfig{
        {
            LevelName:    "普通",
            LevelCode:    "normal",
            ProjectLimit: 2,
            Description:  "普通用户等级可创建2个项目",
            SortOrder:    1,
            Status:       1,
        },
        {
            LevelName:    "VIP",
            LevelCode:    "vip",
            ProjectLimit: 10,
            Description:  "VIP用户等级可创建10个项目",
            SortOrder:    2,
            Status:       1,
        },
    }

    return DB.Create(&defaultConfigs).Error
}

6.2 表结构生成规则

GORM 根据模型中的 tag 自动生成表:

type UserLevelConfig struct {
    ID            uint      `gorm:"primaryKey;autoIncrement;comment:主键ID"`  // 自增主键
    LevelName     string    `gorm:"uniqueIndex:uk_level_name;type:varchar(50);comment:等级名称"` // 唯一索引
    LevelCode     string    `gorm:"not null;uniqueIndex:uk_level_code"` // 非空 + 唯一索引
    ProjectLimit  int       `gorm:"default:0"` // 默认值
    Status        int       `gorm:"default:1"` // 默认值
    CreatedAt     time.Time // 自动时间戳
    UpdatedAt     time.Time // 自动时间戳
}

// 生成的表名
func (UserLevelConfig) TableName() string {
    return "admin_user_level_configs"
}

生成的表: admin_user_level_configs

6.3 数据库初始化流程

在 main.go 中:

// 初始化数据库
if err := storage.InitDatabase(logger); err != nil {
    logger.Fatal("初始化数据库失败", zap.Error(err))
}

// 自动迁移
if err := storage.AutoMigrate(); err != nil {
    logger.Fatal("数据库迁移失败", zap.Error(err))
}

7. 配置管理

7.1 配置文件结构

文件: /Users/youziba/goalfyagent/goalfymax-admin/etc/config.yaml

server:
  addr: "0.0.0.0"
  port: 8087

database:
  dsn: "user:password@tcp(host:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
  maxIdleConns: 10
  maxOpenConns: 100
  logLevel: "info"

gateway:
  base_url: "http://gateway:8080"
  timeout: 30
  auth:
    login_url: "http://gateway:8080/api/login"
    key: "api-key"

sso:
  sso_server_url: "https://sso.example.com"
  client_id: "your-client-id"
  client_secret: "your-client-secret"
  redirect_uri: "http://localhost:3003"
  scope: "openid profile email"
  resource_aud: "api://admin"
  timeout: 30s

log:
  level: "debug"
  format: "json"
  output: "stdout"

message_push:
  goalfymax_base_url: "https://goalfymax.example.com"
  timeout: 30
  retry_count: 3
  retry_interval: 1000

7.2 配置结构定义

文件: /Users/youziba/goalfyagent/goalfymax-admin/internal/config/config.go

type Config struct {
    Server      ServerConfig      `mapstructure:"server"`
    Database    DatabaseConfig    `mapstructure:"database"`
    Gateway     GatewayConfig     `mapstructure:"gateway"`
    SSO         SSOConfig         `mapstructure:"sso"`
    MessagePush MessagePushConfig `mapstructure:"message_push"`
    Log         LogConfig         `mapstructure:"log"`
}

type ServerConfig struct {
    Addr string `mapstructure:"addr"`
    Port int    `mapstructure:"port"`
}

type DatabaseConfig struct {
    DSN          string `mapstructure:"dsn"`
    MaxIdleConns int    `mapstructure:"maxIdleConns"`
    MaxOpenConns int    `mapstructure:"maxOpenConns"`
    LogLevel     string `mapstructure:"logLevel"`
}

8. 实现新功能的完整步骤

8.1 添加"通用配置"功能模块

假设要添加 GeneralConfig (通用配置) 模块:

步骤 1: 创建模型

文件: /internal/models/general_config.go

package models

import "time"

type GeneralConfig struct {
    ID        uint      `json:"id" gorm:"primaryKey;autoIncrement"`
    Key       string    `json:"key" gorm:"uniqueIndex;type:varchar(100);not null"`
    Value     string    `json:"value" gorm:"type:longtext"`
    Type      string    `json:"type" gorm:"type:varchar(50)"` // string, int, bool, json
    Desc      string    `json:"desc" gorm:"type:varchar(255)"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

func (GeneralConfig) TableName() string {
    return "admin_general_configs"
}

// 请求模型 (在 request.go 中添加)
type GeneralConfigCreateRequest struct {
    Key   string `json:"key" binding:"required,min=1,max=100"`
    Value string `json:"value" binding:"required"`
    Type  string `json:"type"`
    Desc  string `json:"desc" binding:"max=255"`
}

type GeneralConfigUpdateRequest struct {
    Value string `json:"value" binding:"required"`
    Desc  string `json:"desc" binding:"max=255"`
}

type GeneralConfigListRequest struct {
    Key   string `form:"key"`
    Type  string `form:"type"`
    Page  int    `form:"page,default=1"`
    Size  int    `form:"size,default=20"`
}

步骤 2: 创建存储层

文件: /internal/storage/general_config_storage.go

package storage

import (
    "goalfymax-admin/internal/models"
    "gorm.io/gorm"
)

type GeneralConfigStorage interface {
    Create(config *models.GeneralConfig) error
    GetByID(id uint) (*models.GeneralConfig, error)
    GetByKey(key string) (*models.GeneralConfig, error)
    Update(config *models.GeneralConfig) error
    Delete(id uint) error
    List(req *models.GeneralConfigListRequest) ([]models.GeneralConfig, int64, error)
    GetAll() ([]models.GeneralConfig, error)
}

type generalConfigStorage struct {
    db *gorm.DB
}

func NewGeneralConfigStorage() GeneralConfigStorage {
    return &generalConfigStorage{db: DB}
}

func (s *generalConfigStorage) Create(config *models.GeneralConfig) error {
    return s.db.Create(config).Error
}

func (s *generalConfigStorage) GetByKey(key string) (*models.GeneralConfig, error) {
    var config models.GeneralConfig
    err := s.db.Where("key = ?", key).First(&config).Error
    return &config, err
}

func (s *generalConfigStorage) List(req *models.GeneralConfigListRequest) ([]models.GeneralConfig, int64, error) {
    var configs []models.GeneralConfig
    var total int64

    query := s.db.Model(&models.GeneralConfig{})

    if req.Key != "" {
        query = query.Where("key LIKE ?", "%"+req.Key+"%")
    }
    if req.Type != "" {
        query = query.Where("type = ?", req.Type)
    }

    query.Count(&total)

    offset := (req.Page - 1) * req.Size
    err := query.Order("id DESC").Offset(offset).Limit(req.Size).Find(&configs).Error
    return configs, total, err
}

步骤 3: 创建服务层

文件: /internal/services/general_config_service.go

package services

import (
    "errors"
    "goalfymax-admin/internal/models"
    "goalfymax-admin/internal/storage"
    "goalfymax-admin/pkg/utils"
    "go.uber.org/zap"
)

type GeneralConfigService interface {
    Create(req *models.GeneralConfigCreateRequest) (*models.GeneralConfig, error)
    GetByID(id uint) (*models.GeneralConfig, error)
    GetByKey(key string) (*models.GeneralConfig, error)
    Update(id uint, req *models.GeneralConfigUpdateRequest) (*models.GeneralConfig, error)
    Delete(id uint) error
    List(req *models.GeneralConfigListRequest) ([]models.GeneralConfig, int64, error)
    GetAll() ([]models.GeneralConfig, error)
}

type generalConfigService struct {
    storage storage.GeneralConfigStorage
    logger  *utils.Logger
}

func NewGeneralConfigService(storage storage.GeneralConfigStorage, logger *utils.Logger) GeneralConfigService {
    return &generalConfigService{
        storage: storage,
        logger:  logger,
    }
}

func (s *generalConfigService) Create(req *models.GeneralConfigCreateRequest) (*models.GeneralConfig, error) {
    // 检查 Key 是否已存在
    _, err := s.storage.GetByKey(req.Key)
    if err == nil {
        return nil, errors.New("配置 Key 已存在")
    }

    config := &models.GeneralConfig{
        Key:   req.Key,
        Value: req.Value,
        Type:  req.Type,
        Desc:  req.Desc,
    }

    if err := s.storage.Create(config); err != nil {
        s.logger.Error("创建通用配置失败", zap.Error(err))
        return nil, errors.New("创建配置失败")
    }

    s.logger.Info("通用配置创建成功", zap.String("key", config.Key))
    return config, nil
}

// ... 其他方法

步骤 4: 创建 Handler

文件: /internal/api/handlers/general_config_handler.go

package handlers

import (
    "goalfymax-admin/internal/models"
    "goalfymax-admin/internal/services"
    "goalfymax-admin/pkg/utils"
    "strconv"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

type GeneralConfigHandler struct {
    service  services.GeneralConfigService
    response *utils.Response
    logger   *utils.Logger
}

func NewGeneralConfigHandler(service services.GeneralConfigService, logger *utils.Logger) *GeneralConfigHandler {
    return &GeneralConfigHandler{
        service:  service,
        response: utils.NewResponse(),
        logger:   logger,
    }
}

func (h *GeneralConfigHandler) Create(c *gin.Context) {
    var req models.GeneralConfigCreateRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }

    config, err := h.service.Create(&req)
    if err != nil {
        h.logger.Error("创建通用配置失败", zap.Error(err))
        h.response.InternalServerError(c, err.Error())
        return
    }

    h.response.Success(c, config)
}

func (h *GeneralConfigHandler) List(c *gin.Context) {
    var req models.GeneralConfigListRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        h.response.ValidateError(c, err)
        return
    }

    if req.Page <= 0 {
        req.Page = 1
    }
    if req.Size <= 0 {
        req.Size = 20
    }

    configs, total, err := h.service.List(&req)
    if err != nil {
        h.response.InternalServerError(c, "获取列表失败")
        return
    }

    h.response.Page(c, configs, total, req.Page, req.Size)
}

// ... 其他方法 GetByID, Update, Delete

步骤 5: 注册路由

/internal/api/routes/routes.goSetupRoutes 函数中:

func SetupRoutes(
    // ... 其他参数
    generalConfigService services.GeneralConfigService,
    // ... 其他参数
) *gin.Engine {
    // ... 其他代码
    
    admin := api.Group("/admin")
    admin.Use(authMiddleware.RequireAuth())
    {
        // ... 其他路由组
        
        // 通用配置管理
        generalConfigs := admin.Group("/general-configs")
        {
            generalConfigs.GET("", generalConfigHandler.List)
            generalConfigs.POST("", generalConfigHandler.Create)
            generalConfigs.GET("/:id", generalConfigHandler.GetByID)
            generalConfigs.PUT("/:id", generalConfigHandler.Update)
            generalConfigs.DELETE("/:id", generalConfigHandler.Delete)
        }
    }
    
    return r
}

步骤 6: 在 main.go 中注册服务

文件: /cmd/server/main.go

// 创建通用配置服务
generalConfigService := services.NewGeneralConfigService(
    storage.NewGeneralConfigStorage(),
    logger,
)

// 设置路由
router := routes.SetupRoutes(
    userService,
    roleService,
    pageService,
    quotaService,
    ssoService,
    rbacService,
    userLevelConfigService,
    generalConfigService,  // 新增
    logger,
    cfg,
)

步骤 7: 数据库迁移

/internal/storage/database.goAutoMigrate 函数中:

func AutoMigrate() error {
    err := DB.AutoMigrate(
        &models.UserLevelConfig{},
        &models.GeneralConfig{},  // 新增
    )
    
    if err != nil {
        return fmt.Errorf("数据库迁移失败: %w", err)
    }
    
    // ... 初始化默认数据
    return nil
}

9. 最佳实践总结

9.1 代码组织

  • 遵循三层架构: Handler (API) -> Service (业务逻辑) -> Storage (数据访问)
  • 每个资源类型对应独立的文件组
  • 使用接口定义 Service 和 Storage便于测试和扩展

9.2 错误处理

  • 在 Service 层捕获错误,返回有意义的错误信息
  • 在 Handler 层根据错误类型返回适当的 HTTP 状态码
  • 使用结构化日志记录所有错误

9.3 验证

  • 在请求模型中使用 Gin 的 binding tag 进行字段验证
  • 在 Service 层进行业务逻辑验证(如唯一性检查)
  • 统一使用 ValidateError 返回验证错误

9.4 数据库

  • 使用 GORM tag 定义表结构和约束
  • 为经常查询的字段添加索引
  • 在初始化时创建默认数据
  • 遵循命名约定: 表名为 admin_[resource]s

9.5 路由设计

  • RESTful 设计: GET/POST/PUT/DELETE
  • 使用路由组织逻辑相关的端点
  • 在路由级别应用认证中间件
  • 提供 .../all 端点用于不分页的列表查询

9.6 日志记录

  • 在每个操作前后记录日志
  • 使用结构化日志的字段记录上下文信息
  • 记录所有错误及其堆栈信息

10. 相关文件快速查找表

功能 文件路径
模型定义 /internal/models/
请求/响应 /internal/models/request.go, response.go
存储接口 /internal/storage/
数据库迁移 /internal/storage/database.go
业务逻辑 /internal/services/
HTTP 处理 /internal/api/handlers/
路由配置 /internal/api/routes/routes.go
应用入口 /cmd/server/main.go
配置管理 /internal/config/config.go
YAML 配置 /etc/config.yaml
中间件 /pkg/middleware/, /internal/api/middlewares/
工具函数 /pkg/utils/