Files
goalfylearning-admin/ARCHITECTURE.md

88 KiB

GoalfyMax Admin 后端管理系统 - 架构文档

目录


项目概述

GoalfyMax Admin 是一个企业级的后端管理系统,基于 Go 语言开发,采用分层架构设计。系统提供了完整的用户管理、权限控制、财务管理、邀请码系统等功能,支持 SSO 单点登录和 RBAC 权限控制。 go run cmd/server/main.go --env dev

主要特性

  • 企业级架构: 严格的三层架构,职责分离明确
  • SSO单点登录: 基于 OAuth2/OIDC 协议的认证系统
  • RBAC权限控制: 灵活的角色-页面权限管理
  • 完整的审计日志: 记录所有数据变更操作
  • 多数据库支持: MySQL + PostgreSQL + Redis
  • 容器化部署: Docker + Kubernetes 生产就绪
  • 国际化支持: 中英文邮件模板
  • 可扩展性: 模块化设计,易于扩展

代码规模

  • 总代码量: 约 1.5 万行 Go 代码
  • 服务层: 20 个 Service,共 7305 行代码
  • API层: 18 个 Handler
  • 存储层: 17 个 Storage
  • 数据模型: 15 个 Model

技术栈

核心框架

技术 版本 用途
Go 1.25+ 编程语言
Gin v1.11.0 Web 框架
GORM v1.30.1 ORM 框架

数据库

数据库 用途
MySQL 主数据库
PostgreSQL MCP 配置存储
Redis v9.16.0 缓存和会话管理

认证与安全

技术 版本 用途
JWT v5.3.0 Token 管理
OAuth2 - SSO 单点登录
PKCE - OAuth2 安全增强

基础设施

组件 版本 用途
Viper v1.20.1 配置管理
Zap v1.27.0 结构化日志
CORS v1.7.0 跨域支持
AWS SDK v2 S3 对象存储

其他依赖

  • UUID v1.6.0: 唯一标识符生成
  • Decimal v1.3.1: 精确数值计算
  • Email v4.0: 邮件发送

项目结构

goalfylearning-admin/
├── cmd/
│   └── server/
│       └── main.go                    # 应用程序入口
├── internal/                          # 内部私有代码
│   ├── api/                          # API 层
│   │   ├── handlers/                 # HTTP 处理器 (18个)
│   │   │   ├── user_handler.go
│   │   │   ├── goalfymax_user_handler.go
│   │   │   ├── sso_handler.go
│   │   │   ├── rbac_handler.go
│   │   │   ├── role_handler.go
│   │   │   ├── page_handler.go
│   │   │   ├── invite_code_handler.go
│   │   │   ├── invite_code_application_handler.go
│   │   │   ├── user_level_config_handler.go
│   │   │   ├── finance_handler.go
│   │   │   ├── quota_handler.go
│   │   │   ├── message_push_handler.go
│   │   │   ├── user_feedback_handler.go
│   │   │   ├── audit_log_handler.go
│   │   │   ├── system_config_handler.go
│   │   │   ├── user_project_quota_handler.go
│   │   │   ├── vendor_model_pricing_handler.go
│   │   │   ├── vm_pricing_handler.go
│   │   │   └── mcp_provider_handler.go
│   │   ├── middlewares/              # 中间件
│   │   │   ├── logging.go            # 请求日志
│   │   │   └── api_log_middleware.go # API 调用日志
│   │   └── routes/
│   │       └── routes.go             # 路由配置
│   ├── config/
│   │   └── config.go                 # 配置结构定义
│   ├── jobs/                         # 定时任务
│   │   ├── mcp_usage_job.go         # MCP 使用余额同步
│   │   └── model_token_job.go       # 模型 Token 余额同步
│   ├── models/                       # 数据模型 (15个)
│   │   ├── common.go                # 基础模型
│   │   ├── user.go
│   │   ├── goalfymax_user.go
│   │   ├── user_level_config.go
│   │   ├── invite_code.go
│   │   ├── invite_code_application.go
│   │   ├── rbac.go
│   │   ├── audit_log.go
│   │   ├── balance_operation_log.go
│   │   ├── message_push.go
│   │   ├── quota_models.go
│   │   ├── user_feedback.go
│   │   ├── user_project_quota.go
│   │   ├── sso.go
│   │   ├── request.go
│   │   └── response.go
│   ├── notifier/
│   │   └── notifier.go              # 钉钉通知服务
│   ├── oss/
│   │   └── s3.go                    # S3 对象存储
│   ├── services/                     # 业务逻辑层 (20个,7305行)
│   │   ├── user_service.go
│   │   ├── goalfymax_user_service.go
│   │   ├── sso_service.go
│   │   ├── sso_admin_service.go
│   │   ├── rbac_service.go
│   │   ├── role_service.go
│   │   ├── page_service.go
│   │   ├── invite_code_service.go
│   │   ├── invite_code_application_service.go
│   │   ├── user_level_config_service.go
│   │   ├── finance_service.go
│   │   ├── quota_service.go
│   │   ├── message_push_service.go
│   │   ├── user_feedback_service.go
│   │   ├── audit_log_service.go
│   │   ├── system_config_service.go
│   │   ├── user_project_quota_service.go
│   │   ├── email_service.go
│   │   ├── gateway_client.go
│   │   └── log_service.go
│   └── storage/                      # 数据访问层 (17个)
│       ├── database.go              # 数据库初始化
│       ├── postgres.go              # PostgreSQL 连接
│       ├── user_storage.go
│       ├── goalfymax_user_storage.go
│       ├── rbac_storage.go
│       ├── role_storage.go
│       ├── page_storage.go
│       ├── invite_code.go
│       ├── user_level_config_storage.go
│       ├── audit_log_storage.go
│       ├── balance_operation_log_storage.go
│       ├── message_push_storage.go
│       ├── user_feedback_storage.go
│       ├── system_config_storage.go
│       ├── user_project_quota_storage.go
│       ├── sso_storage.go
│       └── log_storage.go
├── pkg/                              # 公共可复用包
│   ├── middleware/
│   │   ├── auth.go                  # 认证中间件
│   │   ├── rbac.go                  # 权限中间件
│   │   └── sso_client.go            # SSO 客户端
│   ├── redis/
│   │   └── redis.go                 # Redis 客户端
│   └── utils/
│       ├── crypto.go                # 加密工具
│       ├── jwt.go                   # JWT 工具
│       ├── logger.go                # 日志工具
│       ├── response.go              # 响应封装
│       └── validator.go             # 验证工具
├── etc/                              # 配置文件
│   ├── config.yaml                  # 开发配置
│   └── config-prod.yaml             # 生产配置
├── k8s/                              # Kubernetes 部署配置
│   ├── deployment.yaml
│   ├── service.yaml
│   └── configmap.yaml
├── docs/                             # 文档
│   ├── deployment_and_testing.md
│   ├── email_templates_preview.html
│   └── *.html                       # 邮件模板
├── migrations/                       # 数据库迁移脚本
│   ├── 20250129_add_client_id_to_invite_codes.sql
│   ├── 20250131_add_invite_code_applications_table.sql
│   └── 20250204_add_language_to_invite_code_applications.sql
├── scripts/                          # 脚本工具
│   ├── start.sh
│   ├── stop.sh
│   ├── status.sh
│   ├── migrate.sh
│   ├── invite_code_api.py
│   └── test_*.sh
├── test/                             # 测试文件
│   ├── test_email.go
│   ├── send_test_email.go
│   └── preview_email.go
├── Dockerfile                        # Docker 构建文件
├── build.sh                          # 构建脚本
├── build-and-push.sh                # 构建并推送到 ECR
├── go.mod                            # Go 模块定义
└── go.sum                            # 依赖校验

架构设计

整体架构

系统采用分层架构设计,从上到下分为:

┌─────────────────────────────────────────────┐
│            HTTP Client (前端)                │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│         Gin Router & Middlewares            │
│    (认证、权限、日志、CORS、错误处理)         │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│           Handler Layer (API层)             │
│   (参数验证、请求解析、响应封装)              │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│          Service Layer (业务逻辑层)          │
│   (业务规则、事务处理、外部服务调用)          │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│         Storage Layer (数据访问层)           │
│        (数据库操作、缓存操作)                 │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│      Database (MySQL/PostgreSQL/Redis)      │
└─────────────────────────────────────────────┘

架构特点

  1. 严格的层次分离

    • Handler 只负责 HTTP 请求处理
    • Service 封装所有业务逻辑
    • Storage 封装所有数据访问
  2. 依赖注入

    • Service 依赖 Storage 接口
    • Handler 依赖 Service 接口
    • 便于单元测试和模块替换
  3. 统一错误处理

    • 使用 utils.ErrorResponse 统一错误响应
    • 使用 utils.SuccessResponse 统一成功响应
  4. 中间件链

    • 认证 -> 权限 -> 日志 -> 业务处理

数据流向

Request → Middleware Chain → Handler → Service → Storage → Database
                                          ↓
                                    External API
                                          ↓
                                    Redis Cache

外部依赖

系统依赖以下外部服务:

  • SSO Server: passport.goalfy.ai (OAuth2/OIDC 认证)
  • AI Gateway: 44.247.156.94:8080 (配额查询、网关服务)
  • SMTP Server: smtp.mxhichina.com (邮件发送)
  • DingTalk: 钉钉机器人 (告警通知)
  • AWS S3: 对象存储

核心功能模块

1. 用户管理系统

位置: internal/api/handlers/user_handler.go, internal/services/user_service.go

功能:

  • 用户 CRUD 操作
  • 用户状态管理 (启用/禁用)
  • 角色分配
  • 权限查询
  • 系统角色变更

API 路由: /api/admin/users/*

主要接口:

  • GET /api/admin/users - 用户列表 (支持分页、搜索)
  • POST /api/admin/users - 创建用户
  • PUT /api/admin/users/:id - 更新用户
  • DELETE /api/admin/users/:id - 删除用户
  • PUT /api/admin/users/:id/status - 更新状态
  • PUT /api/admin/users/:id/roles - 更新角色
  • POST /api/admin/users/change-system-role - 变更系统角色

2. GoalfyMax 用户管理

位置: internal/models/goalfymax_user.go, internal/services/goalfymax_user_service.go

核心字段:

type GoalfyMaxUser struct {
    BaseModel
    UserID               uint    `json:"user_id" gorm:"uniqueIndex;not null"` // SSO 用户 ID
    Username             string  `json:"username"`
    Email                string  `json:"email"`
    Nickname             string  `json:"nickname"`
    Avatar               string  `json:"avatar"`
    UserLevelCode        string  `json:"user_level_code"`                     // 用户等级代码
    IsBanned             bool    `json:"is_banned" gorm:"default:false"`      // 是否封禁
    BanReason            string  `json:"ban_reason"`                          // 封禁原因
    BannedAt             *time.Time `json:"banned_at"`                        // 封禁时间
    BannedBy             *uint   `json:"banned_by"`                           // 封禁操作人
    GoalfyHubPermission  int     `json:"goalfy_hub_permission" gorm:"default:0"` // Hub权限 (0/1)
    Version              int     `json:"version" gorm:"default:1"`            // 版本 (1:用户版 2:观察版 3:双版)
    Balance              float64 `json:"balance" gorm:"-"`                    // 余额(从Redis查询,不存数据库)
}

功能:

  • 用户创建、编辑、删除
  • 封禁/解封操作
  • 余额增减操作
  • 版本管理 (用户版/观察版/双版)
  • Hub 权限管理

API 路由: /api/admin/goalfymax-users/*

余额操作:

  • 余额存储在 Redis: goalfymax:user:balance:{user_id}
  • 支持增加余额和扣减余额
  • 自动记录余额操作日志 (balance_operation_logs 表)

3. SSO 单点登录系统

位置: internal/services/sso_service.go, pkg/middleware/auth.go

认证流程:

1. 前端请求登录
   ↓
2. 后端生成 PKCE state 和 code_verifier
   ↓
3. 返回 SSO 授权 URL
   ↓
4. 用户在 SSO 服务器登录
   ↓
5. SSO 重定向到回调 URL (带 code 和 state)
   ↓
6. 后端验证 state,使用 code_verifier 交换 token
   ↓
7. 获取 access_token, refresh_token, id_token
   ↓
8. 调用 SSO userinfo 接口获取用户信息
   ↓
9. 自动创建/更新本地用户
   ↓
10. 返回 token 给前端

特性:

  • OAuth2/OpenID Connect 协议
  • PKCE 安全增强 (防止授权码拦截攻击)
  • 自动用户创建: 首次登录自动创建本地用户记录
  • 在线用户管理: 内存存储在线用户会话
  • 批量登出: 支持批量踢出用户
  • Token 刷新: 自动刷新过期的 access_token

API 路由: /api/sso/*

配置 (etc/config.yaml):

sso:
  sso_server_url: "https://passport.goalfy.ai"
  client_id: "xv5Xesd4ry1_I3hP3xYXNw"
  redirect_uri: "http://localhost:3003"
  scope: "openid profile email"
  resource_aud: "api://admin"

关键代码:

  • 认证中间件: pkg/middleware/auth.go:84-159 (RequireAuth)
  • 自动用户创建: pkg/middleware/auth.go:230-290 (findOrCreateUser)

4. RBAC 权限系统

位置: internal/models/rbac.go, pkg/middleware/rbac.go

权限模型:

User (用户)
  ↓ (role_id)
Role (角色)
  ↓ (role_page_permissions)
Page (页面/菜单)

数据表:

  • admin_users: 用户表 (包含 role_id 外键)
  • admin_roles: 角色表 (如: 超级管理员、普通管理员)
  • admin_pages: 页面/菜单表 (如: /users, /roles)
  • admin_role_page_permissions: 角色-页面关联表

核心模型:

type Role struct {
    BaseModel
    Name        string `json:"name" gorm:"uniqueIndex;size:100;not null"`
    Level       int    `json:"level" gorm:"default:1"`              // 角色级别 (1-5)
    Description string `json:"description" gorm:"size:255"`
    IsDefault   bool   `json:"is_default" gorm:"default:false"`     // 是否默认角色
}

type Page struct {
    BaseModel
    Name     string `json:"name" gorm:"size:100;not null"`
    Path     string `json:"path" gorm:"uniqueIndex;size:255;not null"` // 页面路径
    Icon     string `json:"icon" gorm:"size:100"`
    SortOrder int   `json:"sort_order" gorm:"default:0"`
    IsActive bool   `json:"is_active" gorm:"default:true"`
}

type RolePagePermission struct {
    BaseModel
    RoleID uint `json:"role_id" gorm:"not null;index:idx_role_page"`
    PageID uint `json:"page_id" gorm:"not null;index:idx_role_page"`
}

功能:

  • 页面权限检查
  • 角色权限管理
  • 用户可访问页面查询
  • 权限继承 (通过角色级别)

API 路由: /api/admin/rbac/*

权限检查中间件:

// 检查用户是否有访问某个页面的权限
rbacMiddleware.RequirePagePermission("/users")

使用示例:

// routes.go
users := admin.Group("/users")
users.Use(rbacMiddleware.RequirePagePermission("/users"))
{
    users.GET("", userHandler.List)
    users.POST("", userHandler.Create)
}

5. 用户等级配置系统

位置: internal/models/user_level_config.go, internal/services/user_level_config_service.go

配置内容:

type UserLevelConfig struct {
    BaseModel
    LevelName        string `json:"level_name" gorm:"size:100;not null"`
    LevelCode        string `json:"level_code" gorm:"uniqueIndex;size:50;not null"` // 等级代码
    ProjectLimit     int    `json:"project_limit" gorm:"default:0"`                  // 项目数限制
    CoderVMLimit     int    `json:"coder_vm_limit" gorm:"default:0"`                 // Coder VM上限
    BrowserVMLimit   int    `json:"browser_vm_limit" gorm:"default:0"`               // Browser VM上限
    ProcessLimit     int    `json:"process_limit" gorm:"default:0"`                  // 进程上限
    Description      string `json:"description" gorm:"size:255"`
    SortOrder        int    `json:"sort_order" gorm:"default:0"`
    Status           int    `json:"status" gorm:"default:1"`                         // 1:启用 0:禁用
}

默认等级 (storage/database.go:181-206):

等级代码 等级名称 项目限制 Coder VM Browser VM 进程上限
normal 普通用户 2 0 0 0
vip VIP用户 10 0 0 0
internal 内部用户 0 (无限) 0 0 0

功能:

  • 等级配置 CRUD
  • 等级启用/禁用
  • 排序管理
  • 与 GoalfyMax 用户关联

API 路由: /api/admin/user-level-configs/*


6. 邀请码系统

位置: internal/models/invite_code.go, internal/services/invite_code_service.go

核心模型:

type InviteCode struct {
    BaseModel
    Code         string     `json:"code" gorm:"uniqueIndex;size:50;not null"`    // 邀请码
    IsUsed       bool       `json:"is_used" gorm:"default:false"`                // 是否已使用
    ClientID     *string    `json:"client_id" gorm:"size:255"`                   // 客户端ID
    Email        string     `json:"email" gorm:"size:255"`                       // 关联邮箱
    ExpiresAt    *time.Time `json:"expires_at"`                                  // 过期时间
    UserLevelID  uint       `json:"user_level_id"`                               // 用户等级ID
    UserLevel    *UserLevelConfig `json:"user_level,omitempty" gorm:"foreignKey:UserLevelID"`
}

功能:

  • 邀请码生成 (支持批量生成)
  • 过期时间管理
  • 使用状态追踪
  • 邮箱关联
  • 用户等级绑定
  • 邀请码验证 (检查是否过期、是否已使用)

申请审批流程:

1. 用户在官网提交申请 (公开接口)
   ↓
2. 管理员查看待审批列表
   ↓
3. 审批通过
   ├─ 自动生成邀请码
   ├─ 关联申请邮箱
   └─ 发送邮件通知 (中英文)

或

3. 审批拒绝
   └─ 发送拒绝邮件 (中英文)

API 路由:

  • 管理端: /api/admin/invite-codes/*
  • 公开接口: /api/public/invite-code/apply

统计功能:

  • 总邀请码数
  • 已使用数量
  • 未使用数量
  • 过期数量

7. 邀请码申请系统

位置: internal/models/invite_code_application.go, internal/services/invite_code_application_service.go

核心模型:

type InviteCodeApplication struct {
    BaseModel
    Email          string     `json:"email" gorm:"size:255;not null;index"`   // 申请人邮箱
    Purpose        string     `json:"purpose" gorm:"type:text"`               // 申请目的
    Language       string     `json:"language" gorm:"size:10;default:'zh'"`   // 语言 (zh/en)
    Status         string     `json:"status" gorm:"size:20;default:'pending'"` // pending/approved/rejected
    ReviewedBy     *uint      `json:"reviewed_by"`                            // 审核人ID
    ReviewedAt     *time.Time `json:"reviewed_at"`                            // 审核时间
    RejectReason   string     `json:"reject_reason" gorm:"type:text"`         // 拒绝原因
    InviteCodeID   *uint      `json:"invite_code_id"`                         // 关联的邀请码ID
}

功能:

  • 公开申请接口 (官网接入)
  • 申请列表查询 (支持状态筛选)
  • 待处理数量统计
  • 审批通过 (自动生成邀请码并发邮件)
  • 审批拒绝 (发送拒绝邮件)
  • 批量审批 (批量通过/拒绝)

API 路由: /api/admin/invite-applications/*

邮件模板:

  • 通过模板: docs/email_preview_approval.html (中文), docs/email_preview_en.html (英文)
  • 拒绝模板: docs/email_preview_rejection.html (中文)

审批逻辑 (invite_code_application_service.go):

通过审批:

1. 验证申请状态 (必须是 pending)
2. 生成邀请码
   - 关联申请邮箱
   - 设置默认用户等级 (normal)
   - 设置过期时间 (30)
3. 更新申请状态为 approved
4. 记录审核人和审核时间
5. 发送邮件通知 (根据语言选择模板)
6. 在事务中完成所有操作

拒绝审批:

1. 验证申请状态 (必须是 pending)
2. 更新申请状态为 rejected
3. 记录拒绝原因审核人和审核时间
4. 发送拒绝邮件 (根据语言选择模板)
5. 在事务中完成所有操作

8. 财务管理系统

位置: internal/api/handlers/finance_handler.go, internal/services/finance_service.go

功能模块:

8.1 使用记录查询

  • Sandbox 使用记录: 查询用户的 Sandbox 使用历史
  • Token 使用统计: 模型 Token 消耗统计
  • MCP 使用记录: MCP 服务使用记录
  • 交易日志: 所有交易流水
  • 支付记录: 支付订单和退款记录

8.2 MCP 账户管理

  • 余额管理: 各 MCP Provider 的账户余额
  • 充值记录: MCP 账户充值历史
  • 余额调整: 增加或减少账户余额

8.3 模型账户管理

  • 余额查询: 各模型供应商的账户余额
  • 使用统计: Token 消耗统计

API 路由: /api/finance/*

主要接口:

  • GET /api/finance/sandbox-records - Sandbox 记录
  • GET /api/finance/token-usages - Token 使用
  • GET /api/finance/mcp-usages - MCP 使用
  • GET /api/finance/transaction-logs - 交易日志
  • GET /api/finance/payment-records - 支付记录
  • POST /api/finance/payment-records/refund - 退款
  • GET /api/finance/mcp-account-balances - MCP 余额
  • POST /api/finance/mcp-account-balances - 创建余额
  • PUT /api/finance/mcp-account-balances/:provider_id - 调整余额

9. 配额管理系统

位置: internal/services/quota_service.go, internal/api/handlers/quota_handler.go

功能:

9.1 配额历史查询

  • 转发到 AI Gateway
  • 查询用户的配额使用历史

9.2 配额规则管理

  • 配额规则 CRUD
  • 规则启用/禁用

9.3 用户项目配额

  • 用户项目配额查询
  • 配额创建和更新

API 路由: /api/quotas/*

主要接口:

  • POST /api/quotas/history - 配额历史 (转发到 Gateway)
  • GET /api/quotas/health - 健康检查
  • GET /api/quotas/rules - 规则列表
  • POST /api/quotas/rules - 创建规则
  • PUT /api/quotas/rules/:id - 更新规则
  • DELETE /api/quotas/rules/:id - 删除规则
  • GET /api/quotas/user-project - 用户项目配额
  • POST /api/quotas/user-project - 创建配额

10. 消息推送系统

位置: internal/services/message_push_service.go, internal/api/handlers/message_push_handler.go

功能:

  • 向 GoalfyMax 后端发送消息推送
  • 推送日志记录
  • 用户搜索

API 路由: /api/admin/message-push/*


11. 用户反馈系统

位置: internal/api/handlers/user_feedback_handler.go, internal/services/user_feedback_service.go

功能:

  • 反馈列表查询
  • 标记已处理
  • 反馈统计信息

API 路由: /api/admin/user-feedback/*


12. 审计日志系统

位置: internal/models/audit_log.go, internal/services/audit_log_service.go

核心模型:

type AuditLog struct {
    BaseModel
    UserID         uint      `json:"user_id" gorm:"index;not null"`
    OperationType  string    `json:"operation_type" gorm:"size:50;not null"`  // create/update/delete
    TargetTable    string    `json:"target_table" gorm:"size:100;not null"`   // 目标表名
    TargetID       uint      `json:"target_id" gorm:"index"`                  // 目标记录ID
    OldValue       string    `json:"old_value" gorm:"type:text"`              // 修改前的值(JSON)
    NewValue       string    `json:"new_value" gorm:"type:text"`              // 修改后的值(JSON)
    IP             string    `json:"ip" gorm:"size:50"`
    UserAgent      string    `json:"user_agent" gorm:"size:255"`
}

功能:

  • 记录所有数据修改操作
  • 记录 API 调用日志
  • 变更前后值对比
  • IP 和 UserAgent 追踪

自动日志记录:

  • 通过中间件自动记录 POST/PUT/DELETE 请求
  • 中间件位置: internal/api/middlewares/api_log_middleware.go

13. 定时任务系统

位置: internal/jobs/

13.1 MCP Usage Balance Job

文件: internal/jobs/mcp_usage_job.go

功能:

  • 定时同步 MCP 使用余额
  • 从外部服务获取余额数据
  • 更新到本地数据库

配置:

jobs:
  mcp_usage_balance:
    enabled: true
    run_on_startup: true
    delay_minutes: 5

执行频率: 每小时

13.2 Model Token Balance Job

文件: internal/jobs/model_token_job.go

功能:

  • 定时同步模型 Token 余额
  • 统计 Token 使用情况
  • 更新余额数据

配置:

jobs:
  model_token_balance:
    enabled: true
    run_on_startup: true
    delay_minutes: 5

执行频率: 每小时


数据库设计

数据库架构

系统使用三个数据库:

  1. MySQL: 主数据库,存储所有业务数据
  2. PostgreSQL: MCP 配置和相关数据
  3. Redis: 缓存、会话、余额等临时数据

核心数据表

1. BaseModel (所有表的基础)

type BaseModel struct {
    ID        uint           `json:"id" gorm:"primarykey"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
}

特点:

  • 自动管理 ID、创建时间、更新时间
  • 支持软删除 (DeletedAt)

2. admin_users (管理员用户表)

type User struct {
    BaseModel
    Username      string    `json:"username" gorm:"uniqueIndex;size:100;not null"`
    Email         string    `json:"email" gorm:"uniqueIndex;size:255"`
    Nickname      string    `json:"nickname" gorm:"size:100"`
    Avatar        string    `json:"avatar" gorm:"size:500"`
    Status        int       `json:"status" gorm:"default:1"`           // 1:正常 0:禁用
    SSOProvider   string    `json:"sso_provider" gorm:"size:50"`
    RoleID        *uint     `json:"role_id" gorm:"index"`
    Role          *Role     `json:"role,omitempty" gorm:"foreignKey:RoleID"`
    LastLoginAt   *time.Time `json:"last_login_at"`
    LoginCount    int       `json:"login_count" gorm:"default:0"`
}

索引:

  • uniqueIndex: username, email
  • index: role_id, deleted_at

说明:

  • 管理后台的用户表
  • 通过 SSO 登录自动创建
  • 关联角色表实现权限控制

3. admin_goalfymax_users (GoalfyMax 用户表)

type GoalfyMaxUser struct {
    BaseModel
    UserID               uint    `json:"user_id" gorm:"uniqueIndex;not null"`
    Username             string  `json:"username" gorm:"size:100"`
    Email                string  `json:"email" gorm:"size:255"`
    Nickname             string  `json:"nickname" gorm:"size:100"`
    Avatar               string  `json:"avatar" gorm:"size:500"`
    UserLevelCode        string  `json:"user_level_code" gorm:"size:50"`
    IsBanned             bool    `json:"is_banned" gorm:"default:false"`
    BanReason            string  `json:"ban_reason" gorm:"type:text"`
    BannedAt             *time.Time `json:"banned_at"`
    BannedBy             *uint   `json:"banned_by"`
    GoalfyHubPermission  int     `json:"goalfy_hub_permission" gorm:"default:0"`
    Version              int     `json:"version" gorm:"default:1"`
    Balance              float64 `json:"balance" gorm:"-"`  // 虚拟字段,从Redis查询
}

索引:

  • uniqueIndex: user_id
  • index: deleted_at

说明:

  • GoalfyMax 产品的用户表
  • user_id 关联 SSO 系统的用户 ID
  • Balance 字段不存数据库,从 Redis 实时查询

余额存储 (Redis):

key: goalfymax:user:balance:{user_id}
value: float64 (余额)

4. admin_user_level_configs (用户等级配置表)

type UserLevelConfig struct {
    BaseModel
    LevelName        string `json:"level_name" gorm:"size:100;not null"`
    LevelCode        string `json:"level_code" gorm:"uniqueIndex;size:50;not null"`
    ProjectLimit     int    `json:"project_limit" gorm:"default:0"`
    CoderVMLimit     int    `json:"coder_vm_limit" gorm:"default:0"`
    BrowserVMLimit   int    `json:"browser_vm_limit" gorm:"default:0"`
    ProcessLimit     int    `json:"process_limit" gorm:"default:0"`
    Description      string `json:"description" gorm:"size:255"`
    SortOrder        int    `json:"sort_order" gorm:"default:0"`
    Status           int    `json:"status" gorm:"default:1"`  // 1:启用 0:禁用
}

索引:

  • uniqueIndex: level_code
  • index: deleted_at

默认数据:

level_code level_name project_limit
normal 普通用户 2
vip VIP用户 10
internal 内部用户 0 (无限)

5. admin_invite_codes (邀请码表)

type InviteCode struct {
    BaseModel
    Code         string     `json:"code" gorm:"uniqueIndex;size:50;not null"`
    IsUsed       bool       `json:"is_used" gorm:"default:false"`
    ClientID     *string    `json:"client_id" gorm:"size:255"`
    Email        string     `json:"email" gorm:"size:255"`
    ExpiresAt    *time.Time `json:"expires_at"`
    UserLevelID  uint       `json:"user_level_id"`
    UserLevel    *UserLevelConfig `json:"user_level,omitempty" gorm:"foreignKey:UserLevelID"`
}

索引:

  • uniqueIndex: code
  • index: deleted_at

6. admin_invite_code_applications (邀请码申请表)

SQL 迁移: migrations/20250131_add_invite_code_applications_table.sql

CREATE TABLE admin_invite_code_applications (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    purpose TEXT,
    language VARCHAR(10) DEFAULT 'zh',
    status VARCHAR(20) DEFAULT 'pending',  -- pending/approved/rejected
    reviewed_by BIGINT,
    reviewed_at TIMESTAMP NULL,
    reject_reason TEXT,
    invite_code_id BIGINT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    INDEX idx_email (email),
    INDEX idx_status (status),
    INDEX idx_deleted_at (deleted_at)
);

7. admin_roles (角色表)

type Role struct {
    BaseModel
    Name        string `json:"name" gorm:"uniqueIndex;size:100;not null"`
    Level       int    `json:"level" gorm:"default:1"`
    Description string `json:"description" gorm:"size:255"`
    IsDefault   bool   `json:"is_default" gorm:"default:false"`
}

索引:

  • uniqueIndex: name
  • index: deleted_at

8. admin_pages (页面/菜单表)

type Page struct {
    BaseModel
    Name      string `json:"name" gorm:"size:100;not null"`
    Path      string `json:"path" gorm:"uniqueIndex;size:255;not null"`
    Icon      string `json:"icon" gorm:"size:100"`
    SortOrder int    `json:"sort_order" gorm:"default:0"`
    IsActive  bool   `json:"is_active" gorm:"default:true"`
}

索引:

  • uniqueIndex: path
  • index: deleted_at

9. admin_role_page_permissions (角色-页面权限关联表)

type RolePagePermission struct {
    BaseModel
    RoleID uint `json:"role_id" gorm:"not null;index:idx_role_page"`
    PageID uint `json:"page_id" gorm:"not null;index:idx_role_page"`
}

索引:

  • index: idx_role_page (role_id, page_id 组合索引)
  • index: deleted_at

10. admin_audit_logs (审计日志表)

type AuditLog struct {
    BaseModel
    UserID         uint   `json:"user_id" gorm:"index;not null"`
    OperationType  string `json:"operation_type" gorm:"size:50;not null"`
    TargetTable    string `json:"target_table" gorm:"size:100;not null"`
    TargetID       uint   `json:"target_id" gorm:"index"`
    OldValue       string `json:"old_value" gorm:"type:text"`
    NewValue       string `json:"new_value" gorm:"type:text"`
    IP             string `json:"ip" gorm:"size:50"`
    UserAgent      string `json:"user_agent" gorm:"size:255"`
}

索引:

  • index: user_id, target_id, deleted_at

11. admin_balance_operation_logs (余额操作日志表)

type BalanceOperationLog struct {
    BaseModel
    UserID         uint    `json:"user_id" gorm:"index;not null"`
    OperationType  string  `json:"operation_type" gorm:"size:50;not null"`  // add/deduct
    Amount         float64 `json:"amount" gorm:"type:decimal(20,2);not null"`
    BalanceBefore  float64 `json:"balance_before" gorm:"type:decimal(20,2)"`
    BalanceAfter   float64 `json:"balance_after" gorm:"type:decimal(20,2)"`
    OperatorID     uint    `json:"operator_id" gorm:"index"`
    Remark         string  `json:"remark" gorm:"type:text"`
}

索引:

  • index: user_id, operator_id, deleted_at

数据库初始化

位置: internal/storage/database.go

自动迁移 (database.go:119-165)

func (d *Database) autoMigrate() error {
    return d.DB.AutoMigrate(
        &models.User{},
        &models.Role{},
        &models.Page{},
        &models.RolePagePermission{},
        &models.UserLevelConfig{},
        &models.InviteCode{},
        &models.AuditLog{},
        &models.BalanceOperationLog{},
        &models.GoalfyMaxUser{},
        // ... 其他模型
    )
}

初始化默认数据 (database.go:167-260)

默认角色:

  • 超级管理员 (level: 5)
  • 普通管理员 (level: 3)
  • 观察者 (level: 1)

默认用户等级:

  • normal (普通用户): 2个项目
  • vip (VIP用户): 10个项目
  • internal (内部用户): 无限制

默认页面:

  • /dashboard (仪表盘)
  • /users (用户管理)
  • /roles (角色管理)
  • /pages (页面管理)
  • /invite-codes (邀请码管理)
  • /user-level-configs (用户等级配置)

SQL 迁移脚本

位置: migrations/

  1. 20250129_add_client_id_to_invite_codes.sql

    • 添加 client_id 字段到 invite_codes 表
  2. 20250131_add_invite_code_applications_table.sql

    • 创建邀请码申请表
  3. 20250204_add_language_to_invite_code_applications.sql

    • 添加 language 字段到申请表

执行迁移:

./scripts/migrate.sh

API 接口说明

接口规范

请求格式

认证:

Authorization: Bearer {access_token}

请求体 (POST/PUT):

{
  "field1": "value1",
  "field2": "value2"
}

响应格式

成功响应:

{
  "code": 200,
  "message": "success",
  "data": {
    // 响应数据
  }
}

错误响应:

{
  "code": 400,
  "message": "错误信息"
}

分页响应:

{
  "code": 200,
  "message": "success",
  "data": {
    "list": [...],
    "total": 100,
    "page": 1,
    "page_size": 20
  }
}

API 路由列表

公开接口 (无需认证)

POST /api/public/invite-code/apply  # 提交邀请码申请

SSO 认证接口

POST /api/sso/login          # SSO 登录
POST /api/sso/callback       # SSO 回调
POST /api/sso/refresh        # 刷新令牌
POST /api/sso/logout         # 登出
GET  /api/sso/userinfo       # 获取用户信息
GET  /api/sso/online-users   # 在线用户列表
GET  /api/sso/online-count   # 在线用户数量
POST /api/sso/batch-logout   # 批量登出

用户管理 (需认证)

GET    /api/admin/users                         # 用户列表
POST   /api/admin/users                         # 创建用户
GET    /api/admin/users/:id                     # 用户详情
PUT    /api/admin/users/:id                     # 更新用户
DELETE /api/admin/users/:id                     # 删除用户
PUT    /api/admin/users/:id/status              # 更新状态
PUT    /api/admin/users/:id/roles               # 更新角色
GET    /api/admin/users/:id/roles               # 获取角色
GET    /api/admin/users/:id/permissions         # 获取权限
GET    /api/admin/users/check-role/:user_id     # 检查系统角色
POST   /api/admin/users/change-system-role      # 变更系统角色

GoalfyMax 用户管理 (需认证)

GET    /api/admin/goalfymax-users              # 用户列表
POST   /api/admin/goalfymax-users              # 新增用户
GET    /api/admin/goalfymax-users/:id          # 用户详情
PUT    /api/admin/goalfymax-users/:id          # 编辑用户
DELETE /api/admin/goalfymax-users/:id          # 删除用户
POST   /api/admin/goalfymax-users/:id/ban      # 封禁用户
POST   /api/admin/goalfymax-users/:id/unban    # 解封用户
POST   /api/admin/goalfymax-users/:id/add-balance    # 增加余额
POST   /api/admin/goalfymax-users/:id/deduct-balance # 减少余额

角色管理 (需认证)

GET    /api/admin/roles                    # 角色列表
POST   /api/admin/roles                    # 创建角色
GET    /api/admin/roles/:id                # 角色详情
PUT    /api/admin/roles/:id                # 更新角色
DELETE /api/admin/roles/:id                # 删除角色
PUT    /api/admin/roles/:id/status         # 更新状态
PUT    /api/admin/roles/:id/permissions    # 更新权限
GET    /api/admin/roles/:id/permissions    # 获取权限

页面管理 (需认证)

GET    /api/admin/pages        # 页面列表
POST   /api/admin/pages        # 创建页面
GET    /api/admin/pages/:id    # 页面详情
PUT    /api/admin/pages/:id    # 更新页面
DELETE /api/admin/pages/:id    # 删除页面

RBAC 权限管理 (需认证)

POST   /api/admin/rbac/role-page-permissions           # 分配角色页面权限
DELETE /api/admin/rbac/roles/:id/page-permissions      # 移除角色页面权限
GET    /api/admin/rbac/roles/:id/page-permissions      # 获取角色页面权限
GET    /api/admin/rbac/users/:id/permissions           # 获取用户权限
GET    /api/admin/rbac/check-page-permission           # 检查页面权限
GET    /api/admin/rbac/users/:id/accessible-pages      # 获取用户可访问页面

用户等级配置 (需认证)

GET    /api/admin/user-level-configs              # 等级列表
GET    /api/admin/user-level-configs/all          # 获取所有(不分页)
POST   /api/admin/user-level-configs              # 创建等级
GET    /api/admin/user-level-configs/:id          # 等级详情
PUT    /api/admin/user-level-configs/:id          # 更新等级
DELETE /api/admin/user-level-configs/:id          # 删除等级
PUT    /api/admin/user-level-configs/:id/status   # 更新状态

邀请码管理 (需认证)

GET    /api/admin/invite-codes                    # 邀请码列表
POST   /api/admin/invite-codes                    # 创建邀请码
GET    /api/admin/invite-codes/client-options     # 客户端选项
GET    /api/admin/invite-codes/statistics         # 统计信息
GET    /api/admin/invite-codes/:id                # 邀请码详情
PUT    /api/admin/invite-codes/:id                # 更新邀请码
DELETE /api/admin/invite-codes/:id                # 删除邀请码
POST   /api/admin/invite-codes/mark-used          # 标记为已使用
POST   /api/admin/invite-codes/validate           # 验证有效性

邀请码申请管理 (需认证)

GET    /api/admin/invite-applications                    # 申请列表
GET    /api/admin/invite-applications/statistics         # 统计信息
GET    /api/admin/invite-applications/pending-count      # 待处理数量
POST   /api/admin/invite-applications/approve            # 审批通过
POST   /api/admin/invite-applications/reject             # 审批拒绝
POST   /api/admin/invite-applications/batch-approve      # 批量通过
POST   /api/admin/invite-applications/batch-reject       # 批量拒绝

财务管理 (需认证)

GET    /api/finance/sandbox-records                      # Sandbox 记录
GET    /api/finance/token-usages                         # Token 使用
GET    /api/finance/mcp-usages                           # MCP 使用
GET    /api/finance/transaction-logs                     # 交易日志
GET    /api/finance/payment-records                      # 支付记录
POST   /api/finance/payment-records/refund               # 退款
GET    /api/finance/mcp-account-recharge-records         # MCP 充值记录
POST   /api/finance/mcp-account-recharge-records         # 创建充值
GET    /api/finance/mcp-account-balances                 # MCP 余额
POST   /api/finance/mcp-account-balances                 # 创建余额
PUT    /api/finance/mcp-account-balances/:provider_id    # 调整余额
GET    /api/finance/model-account-balances               # 模型余额

配额管理 (需认证)

POST   /api/quotas/history           # 配额历史
GET    /api/quotas/health            # 健康检查
GET    /api/quotas/rules             # 规则列表
POST   /api/quotas/rules             # 创建规则
PUT    /api/quotas/rules/:id         # 更新规则
DELETE /api/quotas/rules/:id         # 删除规则
GET    /api/quotas/user-project      # 用户项目配额
POST   /api/quotas/user-project      # 创建配额

消息推送 (需认证)

POST   /api/admin/message-push/send      # 发送消息
GET    /api/admin/message-push/logs      # 推送日志
POST   /api/admin/message-push/search    # 用户搜索

用户反馈 (需认证)

GET    /api/admin/user-feedback              # 反馈列表
PUT    /api/admin/user-feedback/:id/process  # 标记已处理
GET    /api/admin/user-feedback/statistics   # 统计信息

审计日志 (需认证)

GET    /api/admin/audit-logs    # 审计日志列表

CORS 配置

允许的源 (routes.go:37-44):

允许的方法:

  • GET, POST, PUT, DELETE, OPTIONS, PATCH

允许的头:

  • Origin, Content-Type, Accept, Authorization, X-Requested-With, Cookie

认证与权限

SSO 单点登录

OAuth2/OIDC 流程

┌─────────┐                                  ┌──────────┐
│  前端   │                                  │ SSO 服务器│
└────┬────┘                                  └─────┬────┘
     │                                             │
     │ 1. 请求登录                                  │
     ├──────────────────────────────────────────►  │
     │                                             │
     │ 2. 返回授权 URL (带 state, code_challenge)   │
     │◄──────────────────────────────────────────┤
     │                                             │
     │ 3. 跳转到 SSO 登录页面                       │
     ├────────────────────────────────────────────►
     │                                             │
     │ 4. 用户登录授权                              │
     │                                             │
     │ 5. 回调 (带 code 和 state)                  │
     │◄────────────────────────────────────────────┤
     │                                             │
     │ 6. 交换 token (使用 code_verifier)          │
     ├──────────────────────────────────────────►  │
     │                                             │
     │ 7. 返回 access_token, refresh_token, id_token│
     │◄──────────────────────────────────────────┤
     │                                             │
     │ 8. 获取用户信息                              │
     ├──────────────────────────────────────────►  │
     │                                             │
     │ 9. 返回用户信息 (email, name, etc.)         │
     │◄──────────────────────────────────────────┤
     │                                             │

PKCE 安全增强

什么是 PKCE?

PKCE (Proof Key for Code Exchange) 是 OAuth2 的安全扩展,用于防止授权码拦截攻击。

流程:

  1. 生成 code_verifier (随机字符串)

    codeVerifier := generateRandomString(128)
    
  2. 计算 code_challenge

    hash := sha256.Sum256([]byte(codeVerifier))
    codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
    
  3. 授权请求携带 code_challenge

    GET /authorize?
        client_id=xxx
        &redirect_uri=xxx
        &code_challenge=xxx
        &code_challenge_method=S256
    
  4. Token 交换时携带 code_verifier

    POST /token
    {
        "code": "xxx",
        "code_verifier": "xxx"
    }
    
  5. 服务器验证

    hash(code_verifier) == code_challenge
    

自动用户创建

位置: pkg/middleware/auth.go:230-290

逻辑:

func findOrCreateUser(ssoUserID, username, email string, db *gorm.DB) (*models.User, error) {
    var user models.User

    // 1. 查找现有用户
    err := db.Where("username = ?", username).First(&user).Error

    if err == gorm.ErrRecordNotFound {
        // 2. 用户不存在,创建新用户
        user = models.User{
            Username:    username,
            Email:       email,
            Nickname:    username,
            Status:      1,  // 默认启用
            SSOProvider: "goalfy-sso",
            LoginCount:  1,
            LastLoginAt: &now,
        }
        if err := db.Create(&user).Error; err != nil {
            return nil, err
        }
    } else {
        // 3. 用户已存在,更新登录信息
        updates := map[string]interface{}{
            "login_count":   user.LoginCount + 1,
            "last_login_at": &now,
        }

        // 同步 SSO 的姓名和邮箱
        if user.Email != email {
            updates["email"] = email
        }

        db.Model(&user).Updates(updates)
    }

    return &user, nil
}

特性:

  • 首次登录自动创建用户
  • 每次登录更新登录时间和次数
  • 自动同步 SSO 的邮箱信息
  • 默认启用状态

RBAC 权限系统

权限模型

User (用户)
  ↓ (role_id)
Role (角色)
  ↓ (role_page_permissions 中间表)
Page (页面/菜单)

权限检查流程

中间件: pkg/middleware/rbac.go:31-70

func RequirePagePermission(pagePath string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Context 获取 user_id
        userID, exists := c.Get("user_id")
        if !exists {
            c.JSON(401, gin.H{"error": "Unauthorized"})
            c.Abort()
            return
        }

        // 2. 调用 Storage 检查权限
        hasPermission, err := storage.CheckUserRolePagePermission(userID, pagePath)
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to check permission"})
            c.Abort()
            return
        }

        // 3. 验证权限
        if !hasPermission {
            c.JSON(403, gin.H{"error": "Forbidden: No permission to access this page"})
            c.Abort()
            return
        }

        // 4. 继续处理请求
        c.Next()
    }
}

权限查询 SQL

位置: internal/storage/rbac_storage.go

SELECT COUNT(*) > 0
FROM admin_users u
JOIN admin_roles r ON u.role_id = r.id
JOIN admin_role_page_permissions rpp ON r.id = rpp.role_id
JOIN admin_pages p ON rpp.page_id = p.id
WHERE u.id = ? AND p.path = ?
  AND u.deleted_at IS NULL
  AND r.deleted_at IS NULL
  AND rpp.deleted_at IS NULL
  AND p.deleted_at IS NULL

使用示例

在路由中使用:

// routes.go
users := admin.Group("/users")
users.Use(rbacMiddleware.RequirePagePermission("/users"))
{
    users.GET("", userHandler.List)       // 需要 /users 权限
    users.POST("", userHandler.Create)    // 需要 /users 权限
    users.PUT("/:id", userHandler.Update) // 需要 /users 权限
}

灵活的权限粒度:

// 整个路由组共享权限
usersGroup.Use(rbacMiddleware.RequirePagePermission("/users"))

// 或者单个路由单独设置权限
users.GET("",
    rbacMiddleware.RequirePagePermission("/users/view"),
    userHandler.List,
)

会话管理

MemorySessionManager

位置: pkg/middleware/auth.go:24-58

数据结构:

type Session struct {
    UserID      uint
    Username    string
    AccessToken string
    ExpiresAt   time.Time
}

type MemorySessionManager struct {
    sessions map[string]*Session  // key: access_token
    mu       sync.RWMutex
}

功能:

  1. 创建会话

    func (m *MemorySessionManager) Create(session *Session) {
        m.mu.Lock()
        defer m.mu.Unlock()
        m.sessions[session.AccessToken] = session
    }
    
  2. 获取会话

    func (m *MemorySessionManager) Get(accessToken string) (*Session, bool) {
        m.mu.RLock()
        defer m.mu.RUnlock()
    
        session, exists := m.sessions[accessToken]
        if !exists {
            return nil, false
        }
    
        // 检查是否过期
        if time.Now().After(session.ExpiresAt) {
            return nil, false
        }
    
        return session, true
    }
    
  3. 删除会话 (登出)

    func (m *MemorySessionManager) Delete(accessToken string) {
        m.mu.Lock()
        defer m.mu.Unlock()
        delete(m.sessions, accessToken)
    }
    
  4. 获取所有在线用户

    func (m *MemorySessionManager) GetAllSessions() []*Session {
        m.mu.RLock()
        defer m.mu.RUnlock()
    
        var sessions []*Session
        now := time.Now()
        for _, session := range m.sessions {
            if now.Before(session.ExpiresAt) {
                sessions = append(sessions, session)
            }
        }
        return sessions
    }
    

特点:

  • 内存存储,重启丢失
  • 线程安全 (使用 RWMutex)
  • 自动过期检查
  • 支持批量登出

中间件系统

中间件链

Request
  ↓
CORS Middleware
  ↓
Logging Middleware
  ↓
Auth Middleware (RequireAuth)
  ↓
RBAC Middleware (RequirePagePermission)
  ↓
API Log Middleware
  ↓
Handler
  ↓
Response

1. 认证中间件

位置: pkg/middleware/auth.go

RequireAuth() (第84-159行)

功能: 验证用户身份

流程:

func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Header 或 Query 获取 Token
        token := c.GetHeader("Authorization")
        if token == "" {
            token = c.Query("token")
        }

        if token == "" {
            c.JSON(401, gin.H{"error": "Missing token"})
            c.Abort()
            return
        }

        // 移除 "Bearer " 前缀
        token = strings.TrimPrefix(token, "Bearer ")

        // 2. 调用 SSO 服务验证 Token
        userInfo, err := m.ssoService.ValidateToken(token)
        if err != nil {
            c.JSON(401, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        // 3. 解析用户 ID
        userID := parseUserID(userInfo)
        if userID == 0 {
            c.JSON(401, gin.H{"error": "Invalid user ID"})
            c.Abort()
            return
        }

        // 4. 查找或创建本地用户
        user, err := findOrCreateUser(userID, userInfo.Username, userInfo.Email, m.db)
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to get user"})
            c.Abort()
            return
        }

        // 5. 将用户信息存入 Context
        c.Set("user_id", user.ID)
        c.Set("username", user.Username)
        c.Set("user", user)

        c.Next()
    }
}

Token 获取方式:

  • Header: Authorization: Bearer {token}
  • Query: ?token={token}

2. RBAC 权限中间件

位置: pkg/middleware/rbac.go

RequirePagePermission(pagePath) (第31-70行)

功能: 检查用户是否有访问某个页面的权限

func RequirePagePermission(pagePath string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Context 获取 user_id (由 AuthMiddleware 设置)
        userID, exists := c.Get("user_id")
        if !exists {
            c.JSON(401, gin.H{"error": "Unauthorized"})
            c.Abort()
            return
        }

        // 2. 调用 Storage 检查权限
        hasPermission, err := storage.CheckUserRolePagePermission(userID.(uint), pagePath)
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to check permission"})
            c.Abort()
            return
        }

        // 3. 验证权限
        if !hasPermission {
            c.JSON(403, gin.H{"error": "Forbidden: No permission"})
            c.Abort()
            return
        }

        c.Next()
    }
}

RequireRole(roleName) (第73-126行)

功能: 检查用户是否具有特定角色

func RequireRole(roleName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID, _ := c.Get("user_id")

        // 查询用户角色
        var user models.User
        db.Preload("Role").First(&user, userID)

        // 检查角色
        if user.Role == nil || user.Role.Name != roleName {
            c.JSON(403, gin.H{"error": "Insufficient role"})
            c.Abort()
            return
        }

        c.Next()
    }
}

3. 日志中间件

RequestLogMiddleware

位置: internal/api/middlewares/logging.go

功能: 记录所有 HTTP 请求

func RequestLogMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        startTime := time.Now()

        // 处理请求
        c.Next()

        // 计算耗时
        duration := time.Since(startTime)

        // 记录日志
        logger.Info("HTTP Request",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("duration", duration),
            zap.String("client_ip", c.ClientIP()),
        )
    }
}

日志输出示例:

2025-01-15T10:30:45.123Z INFO HTTP Request
  method: GET
  path: /api/admin/users
  status: 200
  duration: 45ms
  client_ip: 192.168.1.100

APILogMiddleware

位置: internal/api/middlewares/api_log_middleware.go

功能: 记录数据修改接口的详细信息

func APILogMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 只记录 POST/PUT/DELETE 请求
        if !shouldLogRequest(c.Request.Method) {
            c.Next()
            return
        }

        // 读取请求体
        bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

        // 创建响应写入器
        blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
        c.Writer = blw

        // 处理请求
        c.Next()

        // 记录日志
        logger.Info("API Call",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.String("request_body", string(bodyBytes)),
            zap.String("response_body", blw.body.String()),
            zap.Int("status", c.Writer.Status()),
        )
    }
}

日志包含:

  • 请求方法和路径
  • 请求体 (JSON)
  • 响应体 (JSON)
  • 状态码

4. CORS 中间件

位置: internal/api/routes/routes.go:37-44

config := cors.Config{
    AllowOrigins: []string{
        "http://localhost:5173",
        "http://localhost:5174",
        "http://localhost:3000",
        "http://localhost:3003",
        "http://localhost:3004",
    },
    AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
    AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "Cookie"},
    AllowCredentials: true,
}
router.Use(cors.New(config))

服务层设计

服务层职责

Service Layer 负责:

  • 业务逻辑封装: 所有业务规则都在 Service 中实现
  • 事务管理: 跨表操作的事务控制
  • 外部服务调用: 调用 SSO、Gateway、Email 等外部服务
  • 数据转换: 将 Storage 层的数据转换为业务对象
  • 错误处理: 统一的错误处理和日志记录

核心服务

1. SSO 服务

位置: internal/services/sso_service.go

主要方法:

type SSOService struct {
    config *config.SSOConfig
    db     *gorm.DB
}

// Login 生成 SSO 授权 URL
func (s *SSOService) Login() (string, error)

// HandleCallback 处理 SSO 回调,交换 token
func (s *SSOService) HandleCallback(code, state string) (*TokenResponse, error)

// RefreshToken 刷新访问令牌
func (s *SSOService) RefreshToken(refreshToken string) (*TokenResponse, error)

// ValidateToken 验证 token 有效性
func (s *SSOService) ValidateToken(accessToken string) (*UserInfo, error)

// Logout 用户登出
func (s *SSOService) Logout(accessToken string) error

// GetOnlineUsers 获取在线用户列表
func (s *SSOService) GetOnlineUsers() []*Session

2. 用户服务

位置: internal/services/user_service.go

主要方法:

type UserService struct {
    storage *storage.UserStorage
}

// List 用户列表(分页+搜索)
func (s *UserService) List(page, pageSize int, search string) ([]models.User, int64, error)

// Create 创建用户
func (s *UserService) Create(user *models.User) error

// Update 更新用户
func (s *UserService) Update(id uint, user *models.User) error

// Delete 删除用户(软删除)
func (s *UserService) Delete(id uint) error

// UpdateStatus 更新状态
func (s *UserService) UpdateStatus(id uint, status int) error

// UpdateRoles 更新用户角色
func (s *UserService) UpdateRoles(userID uint, roleIDs []uint) error

// GetPermissions 获取用户权限
func (s *UserService) GetPermissions(userID uint) ([]string, error)

// ChangeUserSystemRole 变更用户系统角色
func (s *UserService) ChangeUserSystemRole(userID uint, newRoleID uint) error

3. GoalfyMax 用户服务

位置: internal/services/goalfymax_user_service.go

主要方法:

type GoalfyMaxUserService struct {
    storage      *storage.GoalfyMaxUserStorage
    balanceLogStorage *storage.BalanceOperationLogStorage
    redis        *redis.Client
}

// Create 创建用户
func (s *GoalfyMaxUserService) Create(user *models.GoalfyMaxUser) error

// Update 更新用户
func (s *GoalfyMaxUserService) Update(id uint, user *models.GoalfyMaxUser) error

// Delete 删除用户
func (s *GoalfyMaxUserService) Delete(id uint) error

// Ban 封禁用户
func (s *GoalfyMaxUserService) Ban(id uint, reason string, bannedBy uint) error

// Unban 解封用户
func (s *GoalfyMaxUserService) Unban(id uint) error

// AddBalance 增加余额
func (s *GoalfyMaxUserService) AddBalance(id uint, amount float64, operatorID uint, remark string) error

// DeductBalance 扣减余额
func (s *GoalfyMaxUserService) DeductBalance(id uint, amount float64, operatorID uint, remark string) error

余额操作逻辑:

func (s *GoalfyMaxUserService) AddBalance(id, amount, operatorID, remark) error {
    // 1. 开启事务
    tx := s.storage.BeginTx()
    defer tx.Rollback()

    // 2. 查询用户
    user, err := s.storage.GetByID(tx, id)

    // 3. 从 Redis 获取当前余额
    balanceKey := fmt.Sprintf("goalfymax:user:balance:%d", id)
    currentBalance := redis.Get(balanceKey).Float64()

    // 4. 计算新余额
    newBalance := currentBalance + amount

    // 5. 更新 Redis 余额
    redis.Set(balanceKey, newBalance, 0)

    // 6. 记录余额操作日志
    log := &models.BalanceOperationLog{
        UserID:        id,
        OperationType: "add",
        Amount:        amount,
        BalanceBefore: currentBalance,
        BalanceAfter:  newBalance,
        OperatorID:    operatorID,
        Remark:        remark,
    }
    s.balanceLogStorage.Create(tx, log)

    // 7. 提交事务
    tx.Commit()

    return nil
}

4. 邀请码服务

位置: internal/services/invite_code_service.go

主要方法:

type InviteCodeService struct {
    storage *storage.InviteCodeStorage
}

// CreateInviteCode 创建邀请码(支持批量)
func (s *InviteCodeService) CreateInviteCode(req *CreateInviteCodeRequest) ([]models.InviteCode, error)

// ValidateInviteCode 验证邀请码有效性
func (s *InviteCodeService) ValidateInviteCode(code string) (bool, error)

// MarkAsUsed 标记邀请码为已使用
func (s *InviteCodeService) MarkAsUsed(code string, clientID string) error

// GetStatistics 获取邀请码统计
func (s *InviteCodeService) GetStatistics() (*InviteCodeStatistics, error)

5. 邀请码申请服务

位置: internal/services/invite_code_application_service.go

主要方法:

type InviteCodeApplicationService struct {
    storage            *storage.InviteCodeApplicationStorage
    inviteCodeService  *InviteCodeService
    emailService       *EmailService
}

// SubmitApplication 提交申请(公开接口)
func (s *InviteCodeApplicationService) SubmitApplication(req *SubmitApplicationRequest) error

// ApproveApplication 审批通过
func (s *InviteCodeApplicationService) ApproveApplication(id uint, reviewerID uint) error

// RejectApplication 审批拒绝
func (s *InviteCodeApplicationService) RejectApplication(id uint, reviewerID uint, reason string) error

// BatchApprove 批量审批通过
func (s *InviteCodeApplicationService) BatchApprove(ids []uint, reviewerID uint) error

// BatchReject 批量审批拒绝
func (s *InviteCodeApplicationService) BatchReject(ids []uint, reviewerID uint, reason string) error

审批通过逻辑:

func (s *InviteCodeApplicationService) ApproveApplication(id, reviewerID) error {
    // 1. 开启事务
    tx := s.storage.BeginTx()
    defer tx.Rollback()

    // 2. 查询申请
    app, err := s.storage.GetByID(tx, id)
    if app.Status != "pending" {
        return errors.New("申请状态不是待审批")
    }

    // 3. 生成邀请码
    inviteCode, err := s.inviteCodeService.CreateInviteCode(tx, &CreateInviteCodeRequest{
        Email:       app.Email,
        UserLevelID: defaultLevelID,  // normal
        ExpiresAt:   time.Now().Add(30 * 24 * time.Hour),  // 30天
        Count:       1,
    })

    // 4. 更新申请状态
    app.Status = "approved"
    app.ReviewedBy = &reviewerID
    now := time.Now()
    app.ReviewedAt = &now
    app.InviteCodeID = &inviteCode.ID
    s.storage.Update(tx, app)

    // 5. 提交事务
    tx.Commit()

    // 6. 发送邮件通知
    s.emailService.SendApprovalEmail(app.Email, inviteCode.Code, app.Language)

    return nil
}

6. 邮件服务

位置: internal/services/email_service.go

主要方法:

type EmailService struct {
    config *config.EmailConfig
}

// SendApprovalEmail 发送审批通过邮件
func (s *EmailService) SendApprovalEmail(email, code, language string) error

// SendRejectionEmail 发送审批拒绝邮件
func (s *EmailService) SendRejectionEmail(email, reason, language string) error

邮件模板:

中文通过模板 (docs/email_preview_approval.html):

<h2>您的邀请码申请已通过!</h2>
<p>您的邀请码是: <strong>{{code}}</strong></p>
<p>有效期: 30天</p>
<a href="https://passport.goalfy.ai/invite/{{code}}">立即使用</a>

英文通过模板 (docs/email_preview_en.html):

<h2>Your invite code application has been approved!</h2>
<p>Your invite code is: <strong>{{code}}</strong></p>
<p>Valid for: 30 days</p>
<a href="https://passport.goalfy.ai/invite/{{code}}">Use Now</a>

中文拒绝模板 (docs/email_preview_rejection.html):

<h2>您的邀请码申请未通过</h2>
<p>拒绝原因: {{reason}}</p>
<p>您可以重新申请或联系我们的客服。</p>

7. 财务服务

位置: internal/services/finance_service.go

主要方法:

type FinanceService struct {
    db *gorm.DB
}

// GetSandboxRecords 获取 Sandbox 使用记录
func (s *FinanceService) GetSandboxRecords(req *SandboxRecordRequest) ([]SandboxRecord, int64, error)

// GetTokenUsages 获取 Token 使用统计
func (s *FinanceService) GetTokenUsages(req *TokenUsageRequest) ([]TokenUsage, int64, error)

// GetMCPUsages 获取 MCP 使用记录
func (s *FinanceService) GetMCPUsages(req *MCPUsageRequest) ([]MCPUsage, int64, error)

// GetMCPAccountBalances 获取 MCP 账户余额
func (s *FinanceService) GetMCPAccountBalances() ([]MCPAccountBalance, error)

// AdjustMCPBalance 调整 MCP 账户余额
func (s *FinanceService) AdjustMCPBalance(providerID uint, amount float64, operatorID uint, remark string) error

// RefundPayment 退款
func (s *FinanceService) RefundPayment(paymentID uint, amount float64, reason string) error

8. 配额服务

位置: internal/services/quota_service.go

主要方法:

type QuotaService struct {
    gatewayClient *GatewayClient
    storage       *storage.QuotaStorage
}

// GetQuotaHistory 获取配额历史(转发到 Gateway)
func (s *QuotaService) GetQuotaHistory(req *QuotaHistoryRequest) (*QuotaHistoryResponse, error)

// CreateQuotaRule 创建配额规则
func (s *QuotaService) CreateQuotaRule(rule *models.QuotaRule) error

// UpdateQuotaRule 更新配额规则
func (s *QuotaService) UpdateQuotaRule(id uint, rule *models.QuotaRule) error

// DeleteQuotaRule 删除配额规则
func (s *QuotaService) DeleteQuotaRule(id uint) error

9. 网关客户端

位置: internal/services/gateway_client.go

功能: 转发请求到 AI Gateway

type GatewayClient struct {
    baseURL    string
    httpClient *http.Client
    authToken  string
}

// Login 登录 Gateway 获取 token
func (c *GatewayClient) Login() error

// ForwardRequest 转发请求到 Gateway
func (c *GatewayClient) ForwardRequest(method, path string, body interface{}) ([]byte, error)

配置:

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

10. 审计日志服务

位置: internal/services/audit_log_service.go

主要方法:

type AuditLogService struct {
    storage *storage.AuditLogStorage
}

// CreateLog 创建审计日志
func (s *AuditLogService) CreateLog(log *models.AuditLog) error

// ListLogs 查询审计日志(分页)
func (s *AuditLogService) ListLogs(req *ListAuditLogsRequest) ([]models.AuditLog, int64, error)

日志记录示例:

// 更新用户时记录日志
func (s *UserService) Update(id uint, newUser *models.User) error {
    // 1. 查询旧值
    oldUser, _ := s.storage.GetByID(id)

    // 2. 更新用户
    s.storage.Update(id, newUser)

    // 3. 记录审计日志
    s.auditLogService.CreateLog(&models.AuditLog{
        UserID:        currentUserID,
        OperationType: "update",
        TargetTable:   "admin_users",
        TargetID:      id,
        OldValue:      toJSON(oldUser),
        NewValue:      toJSON(newUser),
        IP:            clientIP,
        UserAgent:     userAgent,
    })

    return nil
}

配置管理

配置文件位置

  • 开发环境: etc/config.yaml
  • 生产环境: etc/config-prod.yaml

配置加载

位置: internal/config/config.go

type Config struct {
    Server     ServerConfig     `mapstructure:"server"`
    Database   DatabaseConfig   `mapstructure:"database"`
    Redis      RedisConfig      `mapstructure:"redis"`
    PostgreSQL PostgreSQLConfig `mapstructure:"postgresql"`
    SSO        SSOConfig        `mapstructure:"sso"`
    Gateway    GatewayConfig    `mapstructure:"gateway"`
    OSS        OSSConfig        `mapstructure:"oss"`
    Email      EmailConfig      `mapstructure:"email"`
    Alert      AlertConfig      `mapstructure:"alert"`
    Jobs       JobsConfig       `mapstructure:"jobs"`
}

func LoadConfig(env string) (*Config, error) {
    viper.SetConfigName(fmt.Sprintf("config-%s", env))
    viper.SetConfigType("yaml")
    viper.AddConfigPath("./etc")

    if err := viper.ReadInConfig(); err != nil {
        return nil, err
    }

    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return nil, err
    }

    return &config, nil
}

配置示例

完整配置文件 (config.yaml)

# 服务器配置
server:
  addr: "0.0.0.0"
  port: 8087

# MySQL 数据库配置
database:
  dsn: "root:password@tcp(localhost:3306)/goalfymax_admin?charset=utf8mb4&parseTime=True&loc=Local"
  maxIdleConns: 10
  maxOpenConns: 100
  logLevel: "info"

# Redis 配置
redis:
  addr: "localhost:6379"
  password: ""
  db: 0
  poolSize: 10

# PostgreSQL 配置 (MCP 数据)
postgresql:
  host: "localhost"
  port: 5432
  user: "postgres"
  password: "password"
  dbname: "mcp_gateway"
  sslmode: "disable"
  max_open_conns: 100
  max_idle_conns: 20

# SSO 配置
sso:
  sso_server_url: "https://passport.goalfy.ai"
  client_id: "xv5Xesd4ry1_I3hP3xYXNw"
  client_secret: "your-secret"
  redirect_uri: "http://localhost:3003"
  scope: "openid profile email"
  resource_aud: "api://admin"
  admin_token: "your-admin-token"

# Gateway 配置
gateway:
  base_url: "http://44.247.156.94:8080"
  timeout: 30
  auth:
    login_url: "/api/auth/login"
    key: "your-gateway-key"

# OSS (S3) 配置
oss:
  endpoint: "https://goalfyagent-data-test.s3.us-west-2.amazonaws.com/"
  region: "us-west-2"
  access_key_id: "your-access-key"
  secret_access_key: "your-secret-key"
  bucket: "goalfyagent-data-test"
  presign_url_expire: 30m

# 邮件配置
email:
  sender: "invite_goalfymax@goalfyai.com"
  sender_name: "GoalfyMax"
  host: "smtp.mxhichina.com"
  port: 465
  username: "invite_goalfymax@goalfyai.com"
  password: "your-email-password"
  invite_url_prefix: "https://passport.goalfy.ai/invite/"

# 告警配置
alert:
  dingtalk:
    enabled: true
    webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
    secret: "your-dingtalk-secret"

# 定时任务配置
jobs:
  mcp_usage_balance:
    enabled: true
    run_on_startup: true
    delay_minutes: 5
  model_token_balance:
    enabled: true
    run_on_startup: true
    delay_minutes: 5

环境变量支持

使用环境变量覆盖配置:

export SERVER_PORT=8088
export DATABASE_DSN="root:newpassword@tcp(localhost:3306)/db"
export REDIS_ADDR="redis.example.com:6379"

在代码中启用:

viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

部署方案

Docker 部署

Dockerfile

位置: Dockerfile

多阶段构建:

# Stage 1: 构建
FROM golang:1.25-alpine AS builder

WORKDIR /app

# 安装依赖
RUN apk add --no-cache git

# 复制 go.mod 和 go.sum
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY . .

# 构建二进制文件
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s" \
    -o goalfymax-admin \
    ./cmd/server

# Stage 2: 运行
FROM alpine:latest

WORKDIR /app

# 安装 ca-certificates (用于 HTTPS 请求)
RUN apk --no-cache add ca-certificates

# 创建非 root 用户
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser

# 复制二进制文件和配置文件
COPY --from=builder /app/goalfymax-admin .
COPY --from=builder /app/etc ./etc
COPY --from=builder /app/docs ./docs

# 设置权限
RUN chown -R appuser:appuser /app

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8087

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8087/health || exit 1

# 启动应用
CMD ["./goalfymax-admin", "--env", "prod"]

构建镜像:

./build.sh

推送到 ECR:

./build-and-push.sh

Kubernetes 部署

Deployment (k8s/deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: goalfymax-admin
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: goalfymax-admin
  template:
    metadata:
      labels:
        app: goalfymax-admin
    spec:
      containers:
      - name: goalfymax-admin
        image: 177603749739.dkr.ecr.us-west-2.amazonaws.com/goalfy/goalfymax-admin:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8087
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 8087
          initialDelaySeconds: 30
          periodSeconds: 30
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health
            port: 8087
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: config
          mountPath: /app/etc
          readOnly: true
        env:
        - name: ENV
          value: "prod"
      volumes:
      - name: config
        configMap:
          name: goalfymax-admin-config

Service (k8s/service.yaml)

apiVersion: v1
kind: Service
metadata:
  name: goalfymax-admin
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: goalfymax-admin
  ports:
  - protocol: TCP
    port: 8087
    targetPort: 8087

ConfigMap (k8s/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: goalfymax-admin-config
  namespace: default
data:
  config-prod.yaml: |
    server:
      addr: "0.0.0.0"
      port: 8087
    database:
      dsn: "root:password@tcp(mysql:3306)/goalfymax_admin"
    redis:
      addr: "redis:6379"
    # ... 其他配置

部署流程

1. 本地开发

# 启动服务
./start.sh

# 启动 SSO 服务
./start_sso.sh

# 启动带 CORS 的服务
./start_with_cors.sh

2. 构建 Docker 镜像

# 本地构建
./build.sh

# 构建并推送到 ECR
./build-and-push.sh

3. 部署到 Kubernetes

# 创建 ConfigMap
kubectl apply -f k8s/configmap.yaml

# 部署应用
kubectl apply -f k8s/deployment.yaml

# 创建 Service
kubectl apply -f k8s/service.yaml

# 查看部署状态
kubectl get pods -l app=goalfymax-admin
kubectl logs -f <pod-name>

4. 数据库迁移

# 执行迁移脚本
./scripts/migrate.sh

5. 健康检查

# 检查服务健康状态
curl http://localhost:8087/health

# 输出: {"status":"ok"}

监控与告警

健康检查端点

GET /health

响应:

{
  "status": "ok"
}

钉钉告警

配置:

alert:
  dingtalk:
    enabled: true
    webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
    secret: "your-secret"

使用:

// 发送告警
notifier.SendDingTalk("服务异常", "数据库连接失败")

开发指南

本地开发环境搭建

1. 环境要求

  • Go 1.25+
  • MySQL 8.0+
  • Redis 6.0+
  • PostgreSQL 13+

2. 安装依赖

go mod download

3. 配置数据库

创建数据库:

CREATE DATABASE goalfymax_admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

配置文件 (etc/config.yaml):

database:
  dsn: "root:password@tcp(localhost:3306)/goalfymax_admin?charset=utf8mb4&parseTime=True&loc=Local"

4. 启动服务

./start.sh

或者:

go run cmd/server/main.go --env dev

5. 访问接口


代码规范

1. 项目结构规范

  • Handler: 只负责 HTTP 请求处理,不包含业务逻辑
  • Service: 封装所有业务逻辑,调用 Storage
  • Storage: 封装所有数据库操作

2. 命名规范

  • 文件名: 小写蛇形命名 (user_service.go)
  • 变量名: 驼峰命名 (userService)
  • 常量名: 大写蛇形命名 (MAX_PAGE_SIZE)
  • 接口名: I 开头 (IUserService)

3. 错误处理

统一错误响应:

utils.ErrorResponse(c, http.StatusBadRequest, "参数错误")

统一成功响应:

utils.SuccessResponse(c, data)

4. 日志规范

使用结构化日志:

logger.Info("创建用户",
    zap.String("username", username),
    zap.Uint("user_id", userID),
)

logger.Error("数据库错误",
    zap.Error(err),
    zap.String("operation", "create user"),
)

添加新功能

1. 添加新的数据模型

步骤:

  1. internal/models/ 创建模型文件
  2. 定义模型结构
  3. storage/database.goautoMigrate() 中注册

示例:

// internal/models/product.go
type Product struct {
    BaseModel
    Name        string  `json:"name" gorm:"size:100;not null"`
    Price       float64 `json:"price" gorm:"type:decimal(10,2)"`
    Description string  `json:"description" gorm:"type:text"`
}
// storage/database.go
func (d *Database) autoMigrate() error {
    return d.DB.AutoMigrate(
        // ... 其他模型
        &models.Product{},
    )
}

2. 添加新的 Storage

步骤:

  1. internal/storage/ 创建 storage 文件
  2. 实现 CRUD 方法
  3. 依赖 Database 对象

示例:

// internal/storage/product_storage.go
type ProductStorage struct {
    db *Database
}

func NewProductStorage(db *Database) *ProductStorage {
    return &ProductStorage{db: db}
}

func (s *ProductStorage) Create(product *models.Product) error {
    return s.db.DB.Create(product).Error
}

func (s *ProductStorage) GetByID(id uint) (*models.Product, error) {
    var product models.Product
    err := s.db.DB.First(&product, id).Error
    return &product, err
}

func (s *ProductStorage) List(page, pageSize int) ([]models.Product, int64, error) {
    var products []models.Product
    var total int64

    offset := (page - 1) * pageSize

    s.db.DB.Model(&models.Product{}).Count(&total)
    err := s.db.DB.Offset(offset).Limit(pageSize).Find(&products).Error

    return products, total, err
}

func (s *ProductStorage) Update(id uint, product *models.Product) error {
    return s.db.DB.Model(&models.Product{}).Where("id = ?", id).Updates(product).Error
}

func (s *ProductStorage) Delete(id uint) error {
    return s.db.DB.Delete(&models.Product{}, id).Error
}

3. 添加新的 Service

步骤:

  1. internal/services/ 创建 service 文件
  2. 实现业务逻辑
  3. 调用 Storage 方法

示例:

// internal/services/product_service.go
type ProductService struct {
    storage *storage.ProductStorage
}

func NewProductService(storage *storage.ProductStorage) *ProductService {
    return &ProductService{storage: storage}
}

func (s *ProductService) CreateProduct(product *models.Product) error {
    // 业务验证
    if product.Price < 0 {
        return errors.New("价格不能为负数")
    }

    // 调用 Storage
    return s.storage.Create(product)
}

func (s *ProductService) GetProduct(id uint) (*models.Product, error) {
    return s.storage.GetByID(id)
}

func (s *ProductService) ListProducts(page, pageSize int) ([]models.Product, int64, error) {
    return s.storage.List(page, pageSize)
}

4. 添加新的 Handler

步骤:

  1. internal/api/handlers/ 创建 handler 文件
  2. 实现 HTTP 处理方法
  3. 在 routes.go 中注册路由

示例:

// internal/api/handlers/product_handler.go
type ProductHandler struct {
    service *services.ProductService
}

func NewProductHandler(service *services.ProductService) *ProductHandler {
    return &ProductHandler{service: service}
}

// Create 创建产品
func (h *ProductHandler) Create(c *gin.Context) {
    var req models.Product
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "参数错误")
        return
    }

    if err := h.service.CreateProduct(&req); err != nil {
        utils.ErrorResponse(c, http.StatusInternalServerError, err.Error())
        return
    }

    utils.SuccessResponse(c, req)
}

// List 产品列表
func (h *ProductHandler) List(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

    products, total, err := h.service.ListProducts(page, pageSize)
    if err != nil {
        utils.ErrorResponse(c, http.StatusInternalServerError, err.Error())
        return
    }

    utils.SuccessResponse(c, gin.H{
        "list":      products,
        "total":     total,
        "page":      page,
        "page_size": pageSize,
    })
}

5. 注册路由

在 routes.go 中添加:

// internal/api/routes/routes.go
func SetupRoutes(r *gin.Engine, deps *Dependencies) {
    // ... 初始化 handler
    productHandler := handlers.NewProductHandler(deps.ProductService)

    // 添加路由
    admin := r.Group("/api/admin")
    admin.Use(authMiddleware.RequireAuth())
    {
        products := admin.Group("/products")
        products.Use(rbacMiddleware.RequirePagePermission("/products"))
        {
            products.GET("", productHandler.List)
            products.POST("", productHandler.Create)
            products.GET("/:id", productHandler.GetByID)
            products.PUT("/:id", productHandler.Update)
            products.DELETE("/:id", productHandler.Delete)
        }
    }
}

测试

1. 单元测试

创建测试文件:

// internal/services/product_service_test.go
package services

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestProductService_CreateProduct(t *testing.T) {
    // 创建 mock storage
    mockStorage := &MockProductStorage{}
    service := NewProductService(mockStorage)

    // 测试用例
    product := &models.Product{
        Name:  "Test Product",
        Price: 99.99,
    }

    err := service.CreateProduct(product)
    assert.NoError(t, err)
    assert.NotZero(t, product.ID)
}

func TestProductService_CreateProduct_NegativePrice(t *testing.T) {
    mockStorage := &MockProductStorage{}
    service := NewProductService(mockStorage)

    product := &models.Product{
        Name:  "Test Product",
        Price: -10,  // 负数价格
    }

    err := service.CreateProduct(product)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "价格不能为负数")
}

运行测试:

go test ./internal/services/...

2. API 测试

使用 curl 测试:

# 登录
curl -X POST http://localhost:8087/api/sso/login

# 创建用户
curl -X POST http://localhost:8087/api/admin/users \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "email": "test@example.com",
    "nickname": "Test User"
  }'

# 查询用户列表
curl -X GET "http://localhost:8087/api/admin/users?page=1&page_size=20" \
  -H "Authorization: Bearer {token}"

使用测试脚本:

./scripts/test_api.sh
./test_sso_api.sh
./test_quota_api.sh

常见问题

1. 数据库连接失败

问题:

Error 1045: Access denied for user 'root'@'localhost'

解决:

  • 检查 MySQL 用户名和密码
  • 检查 MySQL 是否启动
  • 检查配置文件中的 DSN

2. Redis 连接失败

问题:

dial tcp 127.0.0.1:6379: connect: connection refused

解决:

  • 检查 Redis 是否启动
  • 检查 Redis 端口是否正确
  • 检查 Redis 密码配置

3. SSO 登录失败

问题:

invalid_grant: PKCE validation failed

解决:

  • 检查 code_verifier 是否正确存储
  • 检查 SSO 服务器配置
  • 检查 redirect_uri 是否匹配

4. 权限被拒绝

问题:

403 Forbidden: No permission to access this page

解决:

  • 检查用户是否分配角色
  • 检查角色是否有对应页面权限
  • 检查页面路径是否匹配

总结

GoalfyMax Admin 是一个功能完善、架构清晰的企业级后端管理系统:

核心优势

  1. 架构清晰: 严格的三层架构,职责分离明确
  2. 功能完善: 用户管理、权限控制、财务管理、邀请码系统等
  3. 安全可靠: SSO 单点登录、RBAC 权限、审计日志、PKCE 安全增强
  4. 生产就绪: Docker + Kubernetes 部署、健康检查、日志监控、钉钉告警
  5. 代码规范: 符合 Go 最佳实践,模块化设计,接口抽象
  6. 可扩展性: 易于添加新功能,支持多数据库

技术亮点

  • SSO 单点登录: 完整的 OAuth2/OIDC 实现,支持 PKCE
  • RBAC 权限: 灵活的角色-页面权限管理
  • 邀请码系统: 完整的申请-审批-发邮件流程
  • 财务管理: MCP 账户、模型账户、余额管理
  • 审计日志: 记录所有数据变更操作
  • 定时任务: 余额同步、统计任务
  • 多数据库: MySQL + PostgreSQL + Redis
  • 容器化部署: Docker 多阶段构建 + Kubernetes

部署环境

  • 容器化: Docker + Kubernetes
  • 云平台: AWS EKS + RDS + S3 + Redis
  • 监控告警: 健康检查 + 钉钉告警
  • 高可用: 多副本部署 + 负载均衡

附录

相关文档

脚本工具

  • 启动脚本: start.sh, start_sso.sh, start_with_cors.sh
  • 部署脚本: build.sh, build-and-push.sh
  • 数据库迁移: scripts/migrate.sh
  • 测试脚本: scripts/test_api.sh, test_sso_api.sh
  • 邀请码 API: scripts/invite_code_api.py

联系方式

  • 项目地址: /Users/ricardo/Documents/加和科技/goalfylearning-admin
  • Git 仓库: (待补充)
  • 问题反馈: (待补充)

文档版本: v1.0 更新时间: 2025-12-04 文档作者: Claude AI