3429 lines
88 KiB
Markdown
3429 lines
88 KiB
Markdown
# GoalfyMax Admin 后端管理系统 - 架构文档
|
|
|
|
## 目录
|
|
- [项目概述](#项目概述)
|
|
- [技术栈](#技术栈)
|
|
- [项目结构](#项目结构)
|
|
- [架构设计](#架构设计)
|
|
- [核心功能模块](#核心功能模块)
|
|
- [数据库设计](#数据库设计)
|
|
- [API接口说明](#api接口说明)
|
|
- [认证与权限](#认证与权限)
|
|
- [中间件系统](#中间件系统)
|
|
- [服务层设计](#服务层设计)
|
|
- [配置管理](#配置管理)
|
|
- [部署方案](#部署方案)
|
|
- [开发指南](#开发指南)
|
|
|
|
---
|
|
|
|
## 项目概述
|
|
|
|
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`
|
|
|
|
**核心字段**:
|
|
```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):
|
|
```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`: 角色-页面关联表
|
|
|
|
**核心模型**:
|
|
|
|
```go
|
|
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/*`
|
|
|
|
**权限检查中间件**:
|
|
```go
|
|
// 检查用户是否有访问某个页面的权限
|
|
rbacMiddleware.RequirePagePermission("/users")
|
|
```
|
|
|
|
**使用示例**:
|
|
```go
|
|
// 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`
|
|
|
|
**配置内容**:
|
|
```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`
|
|
|
|
**核心模型**:
|
|
```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`
|
|
|
|
**核心模型**:
|
|
```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):
|
|
|
|
**通过审批**:
|
|
```go
|
|
1. 验证申请状态 (必须是 pending)
|
|
2. 生成邀请码
|
|
- 关联申请邮箱
|
|
- 设置默认用户等级 (normal)
|
|
- 设置过期时间 (30天)
|
|
3. 更新申请状态为 approved
|
|
4. 记录审核人和审核时间
|
|
5. 发送邮件通知 (根据语言选择模板)
|
|
6. 在事务中完成所有操作
|
|
```
|
|
|
|
**拒绝审批**:
|
|
```go
|
|
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`
|
|
|
|
**核心模型**:
|
|
```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 使用余额
|
|
- 从外部服务获取余额数据
|
|
- 更新到本地数据库
|
|
|
|
**配置**:
|
|
```yaml
|
|
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 使用情况
|
|
- 更新余额数据
|
|
|
|
**配置**:
|
|
```yaml
|
|
jobs:
|
|
model_token_balance:
|
|
enabled: true
|
|
run_on_startup: true
|
|
delay_minutes: 5
|
|
```
|
|
|
|
**执行频率**: 每小时
|
|
|
|
---
|
|
|
|
## 数据库设计
|
|
|
|
### 数据库架构
|
|
|
|
系统使用**三个数据库**:
|
|
|
|
1. **MySQL**: 主数据库,存储所有业务数据
|
|
2. **PostgreSQL**: MCP 配置和相关数据
|
|
3. **Redis**: 缓存、会话、余额等临时数据
|
|
|
|
### 核心数据表
|
|
|
|
#### 1. BaseModel (所有表的基础)
|
|
|
|
```go
|
|
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 (管理员用户表)
|
|
|
|
```go
|
|
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 用户表)
|
|
|
|
```go
|
|
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 (用户等级配置表)
|
|
|
|
```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"`
|
|
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 (邀请码表)
|
|
|
|
```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"`
|
|
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`
|
|
|
|
```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 (角色表)
|
|
|
|
```go
|
|
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 (页面/菜单表)
|
|
|
|
```go
|
|
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 (角色-页面权限关联表)
|
|
|
|
```go
|
|
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 (审计日志表)
|
|
|
|
```go
|
|
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 (余额操作日志表)
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
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 字段到申请表
|
|
|
|
**执行迁移**:
|
|
```bash
|
|
./scripts/migrate.sh
|
|
```
|
|
|
|
---
|
|
|
|
## API 接口说明
|
|
|
|
### 接口规范
|
|
|
|
#### 请求格式
|
|
|
|
**认证**:
|
|
```
|
|
Authorization: Bearer {access_token}
|
|
```
|
|
|
|
**请求体** (POST/PUT):
|
|
```json
|
|
{
|
|
"field1": "value1",
|
|
"field2": "value2"
|
|
}
|
|
```
|
|
|
|
#### 响应格式
|
|
|
|
**成功响应**:
|
|
```json
|
|
{
|
|
"code": 200,
|
|
"message": "success",
|
|
"data": {
|
|
// 响应数据
|
|
}
|
|
}
|
|
```
|
|
|
|
**错误响应**:
|
|
```json
|
|
{
|
|
"code": 400,
|
|
"message": "错误信息"
|
|
}
|
|
```
|
|
|
|
**分页响应**:
|
|
```json
|
|
{
|
|
"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):
|
|
- http://localhost:5173
|
|
- http://localhost:5174
|
|
- http://localhost:3000
|
|
- http://localhost:3003
|
|
- http://localhost:3004
|
|
|
|
**允许的方法**:
|
|
- 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** (随机字符串)
|
|
```go
|
|
codeVerifier := generateRandomString(128)
|
|
```
|
|
|
|
2. **计算 code_challenge**
|
|
```go
|
|
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`
|
|
|
|
**逻辑**:
|
|
|
|
```go
|
|
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`
|
|
|
|
```go
|
|
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`
|
|
|
|
```sql
|
|
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
|
|
```
|
|
|
|
#### 使用示例
|
|
|
|
**在路由中使用**:
|
|
|
|
```go
|
|
// 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 权限
|
|
}
|
|
```
|
|
|
|
**灵活的权限粒度**:
|
|
|
|
```go
|
|
// 整个路由组共享权限
|
|
usersGroup.Use(rbacMiddleware.RequirePagePermission("/users"))
|
|
|
|
// 或者单个路由单独设置权限
|
|
users.GET("",
|
|
rbacMiddleware.RequirePagePermission("/users/view"),
|
|
userHandler.List,
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
### 会话管理
|
|
|
|
#### MemorySessionManager
|
|
|
|
**位置**: `pkg/middleware/auth.go:24-58`
|
|
|
|
**数据结构**:
|
|
|
|
```go
|
|
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. **创建会话**
|
|
```go
|
|
func (m *MemorySessionManager) Create(session *Session) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.sessions[session.AccessToken] = session
|
|
}
|
|
```
|
|
|
|
2. **获取会话**
|
|
```go
|
|
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. **删除会话 (登出)**
|
|
```go
|
|
func (m *MemorySessionManager) Delete(accessToken string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.sessions, accessToken)
|
|
}
|
|
```
|
|
|
|
4. **获取所有在线用户**
|
|
```go
|
|
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行)
|
|
|
|
**功能**: 验证用户身份
|
|
|
|
**流程**:
|
|
|
|
```go
|
|
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行)
|
|
|
|
**功能**: 检查用户是否有访问某个页面的权限
|
|
|
|
```go
|
|
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行)
|
|
|
|
**功能**: 检查用户是否具有特定角色
|
|
|
|
```go
|
|
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 请求
|
|
|
|
```go
|
|
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`
|
|
|
|
**功能**: 记录数据修改接口的详细信息
|
|
|
|
```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`
|
|
|
|
```go
|
|
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`
|
|
|
|
**主要方法**:
|
|
|
|
```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`
|
|
|
|
**主要方法**:
|
|
|
|
```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`
|
|
|
|
**主要方法**:
|
|
|
|
```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
|
|
```
|
|
|
|
**余额操作逻辑**:
|
|
|
|
```go
|
|
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`
|
|
|
|
**主要方法**:
|
|
|
|
```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`
|
|
|
|
**主要方法**:
|
|
|
|
```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
|
|
```
|
|
|
|
**审批通过逻辑**:
|
|
|
|
```go
|
|
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`
|
|
|
|
**主要方法**:
|
|
|
|
```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):
|
|
```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):
|
|
```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):
|
|
```html
|
|
<h2>您的邀请码申请未通过</h2>
|
|
<p>拒绝原因: {{reason}}</p>
|
|
<p>您可以重新申请或联系我们的客服。</p>
|
|
```
|
|
|
|
---
|
|
|
|
#### 7. 财务服务
|
|
|
|
**位置**: `internal/services/finance_service.go`
|
|
|
|
**主要方法**:
|
|
|
|
```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`
|
|
|
|
**主要方法**:
|
|
|
|
```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
|
|
|
|
```go
|
|
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)
|
|
```
|
|
|
|
**配置**:
|
|
```yaml
|
|
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`
|
|
|
|
**主要方法**:
|
|
|
|
```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)
|
|
```
|
|
|
|
**日志记录示例**:
|
|
|
|
```go
|
|
// 更新用户时记录日志
|
|
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`
|
|
|
|
```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)
|
|
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
### 环境变量支持
|
|
|
|
**使用环境变量覆盖配置**:
|
|
|
|
```bash
|
|
export SERVER_PORT=8088
|
|
export DATABASE_DSN="root:newpassword@tcp(localhost:3306)/db"
|
|
export REDIS_ADDR="redis.example.com:6379"
|
|
```
|
|
|
|
**在代码中启用**:
|
|
|
|
```go
|
|
viper.AutomaticEnv()
|
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
```
|
|
|
|
---
|
|
|
|
## 部署方案
|
|
|
|
### Docker 部署
|
|
|
|
#### Dockerfile
|
|
|
|
**位置**: `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"]
|
|
```
|
|
|
|
**构建镜像**:
|
|
|
|
```bash
|
|
./build.sh
|
|
```
|
|
|
|
**推送到 ECR**:
|
|
|
|
```bash
|
|
./build-and-push.sh
|
|
```
|
|
|
|
---
|
|
|
|
### Kubernetes 部署
|
|
|
|
#### Deployment (k8s/deployment.yaml)
|
|
|
|
```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)
|
|
|
|
```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)
|
|
|
|
```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. 本地开发
|
|
|
|
```bash
|
|
# 启动服务
|
|
./start.sh
|
|
|
|
# 启动 SSO 服务
|
|
./start_sso.sh
|
|
|
|
# 启动带 CORS 的服务
|
|
./start_with_cors.sh
|
|
```
|
|
|
|
#### 2. 构建 Docker 镜像
|
|
|
|
```bash
|
|
# 本地构建
|
|
./build.sh
|
|
|
|
# 构建并推送到 ECR
|
|
./build-and-push.sh
|
|
```
|
|
|
|
#### 3. 部署到 Kubernetes
|
|
|
|
```bash
|
|
# 创建 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. 数据库迁移
|
|
|
|
```bash
|
|
# 执行迁移脚本
|
|
./scripts/migrate.sh
|
|
```
|
|
|
|
#### 5. 健康检查
|
|
|
|
```bash
|
|
# 检查服务健康状态
|
|
curl http://localhost:8087/health
|
|
|
|
# 输出: {"status":"ok"}
|
|
```
|
|
|
|
---
|
|
|
|
### 监控与告警
|
|
|
|
#### 健康检查端点
|
|
|
|
```
|
|
GET /health
|
|
```
|
|
|
|
**响应**:
|
|
```json
|
|
{
|
|
"status": "ok"
|
|
}
|
|
```
|
|
|
|
#### 钉钉告警
|
|
|
|
**配置**:
|
|
```yaml
|
|
alert:
|
|
dingtalk:
|
|
enabled: true
|
|
webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
|
|
secret: "your-secret"
|
|
```
|
|
|
|
**使用**:
|
|
|
|
```go
|
|
// 发送告警
|
|
notifier.SendDingTalk("服务异常", "数据库连接失败")
|
|
```
|
|
|
|
---
|
|
|
|
## 开发指南
|
|
|
|
### 本地开发环境搭建
|
|
|
|
#### 1. 环境要求
|
|
|
|
- Go 1.25+
|
|
- MySQL 8.0+
|
|
- Redis 6.0+
|
|
- PostgreSQL 13+
|
|
|
|
#### 2. 安装依赖
|
|
|
|
```bash
|
|
go mod download
|
|
```
|
|
|
|
#### 3. 配置数据库
|
|
|
|
**创建数据库**:
|
|
|
|
```sql
|
|
CREATE DATABASE goalfymax_admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
```
|
|
|
|
**配置文件** (etc/config.yaml):
|
|
|
|
```yaml
|
|
database:
|
|
dsn: "root:password@tcp(localhost:3306)/goalfymax_admin?charset=utf8mb4&parseTime=True&loc=Local"
|
|
```
|
|
|
|
#### 4. 启动服务
|
|
|
|
```bash
|
|
./start.sh
|
|
```
|
|
|
|
或者:
|
|
|
|
```bash
|
|
go run cmd/server/main.go --env dev
|
|
```
|
|
|
|
#### 5. 访问接口
|
|
|
|
- **健康检查**: http://localhost:8087/health
|
|
- **API 文档**: http://localhost:8087/swagger (如果有)
|
|
|
|
---
|
|
|
|
### 代码规范
|
|
|
|
#### 1. 项目结构规范
|
|
|
|
- **Handler**: 只负责 HTTP 请求处理,不包含业务逻辑
|
|
- **Service**: 封装所有业务逻辑,调用 Storage
|
|
- **Storage**: 封装所有数据库操作
|
|
|
|
#### 2. 命名规范
|
|
|
|
- **文件名**: 小写蛇形命名 (user_service.go)
|
|
- **变量名**: 驼峰命名 (userService)
|
|
- **常量名**: 大写蛇形命名 (MAX_PAGE_SIZE)
|
|
- **接口名**: I 开头 (IUserService)
|
|
|
|
#### 3. 错误处理
|
|
|
|
**统一错误响应**:
|
|
|
|
```go
|
|
utils.ErrorResponse(c, http.StatusBadRequest, "参数错误")
|
|
```
|
|
|
|
**统一成功响应**:
|
|
|
|
```go
|
|
utils.SuccessResponse(c, data)
|
|
```
|
|
|
|
#### 4. 日志规范
|
|
|
|
**使用结构化日志**:
|
|
|
|
```go
|
|
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.go` 的 `autoMigrate()` 中注册
|
|
|
|
**示例**:
|
|
|
|
```go
|
|
// 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"`
|
|
}
|
|
```
|
|
|
|
```go
|
|
// storage/database.go
|
|
func (d *Database) autoMigrate() error {
|
|
return d.DB.AutoMigrate(
|
|
// ... 其他模型
|
|
&models.Product{},
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### 2. 添加新的 Storage
|
|
|
|
**步骤**:
|
|
|
|
1. 在 `internal/storage/` 创建 storage 文件
|
|
2. 实现 CRUD 方法
|
|
3. 依赖 Database 对象
|
|
|
|
**示例**:
|
|
|
|
```go
|
|
// 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 方法
|
|
|
|
**示例**:
|
|
|
|
```go
|
|
// 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 中注册路由
|
|
|
|
**示例**:
|
|
|
|
```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 中添加**:
|
|
|
|
```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. 单元测试
|
|
|
|
**创建测试文件**:
|
|
|
|
```go
|
|
// 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(), "价格不能为负数")
|
|
}
|
|
```
|
|
|
|
**运行测试**:
|
|
|
|
```bash
|
|
go test ./internal/services/...
|
|
```
|
|
|
|
---
|
|
|
|
#### 2. API 测试
|
|
|
|
**使用 curl 测试**:
|
|
|
|
```bash
|
|
# 登录
|
|
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}"
|
|
```
|
|
|
|
**使用测试脚本**:
|
|
|
|
```bash
|
|
./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
|
|
- **监控告警**: 健康检查 + 钉钉告警
|
|
- **高可用**: 多副本部署 + 负载均衡
|
|
|
|
---
|
|
|
|
## 附录
|
|
|
|
### 相关文档
|
|
|
|
- [快速开始指南](discuss/QUICK_START.md)
|
|
- [架构指南](discuss/architecture_guide.md)
|
|
- [用户等级配置实现总结](discuss/user-level-config-implementation-summary.md)
|
|
- [部署和测试文档](docs/deployment_and_testing.md)
|
|
- [邀请码功能文档](docs/invite_code_application_feature.md)
|
|
- [邮件模板预览](docs/email_templates_preview.html)
|
|
|
|
### 脚本工具
|
|
|
|
- **启动脚本**: `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 |