33 KiB
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.go 的 SetupRoutes 函数中:
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.go 的 AutoMigrate 函数中:
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/ |