Files
goalfylearning-admin/discuss/architecture_guide.md

1183 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```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行)
```go
// 页面管理 - 所有路由通过页面权限检查
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`):
```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`):
```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`):
```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`):
```go
func (s *pageStorage) Create(page *models.Page) error {
return s.db.Create(page).Error
}
```
#### b) 分配角色的页面权限
**Handler** (`/internal/api/handlers/rbac_handler.go`):
```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`)
```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`)
```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`)
```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`)
```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 都遵循相同的模式:
```go
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` 提供统一的响应:
```go
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 错误处理
```go
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`
```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 中)
```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`
```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 自动生成表:
```go
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 中**:
```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`
```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`
```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`
```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`
```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`
```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`
```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` 函数中:
```go
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`
```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` 函数中:
```go
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/` |