1183 lines
33 KiB
Markdown
1183 lines
33 KiB
Markdown
# 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/` |
|
||
|