feat():learning后台管理项目初始化

This commit is contained in:
yuj
2025-12-04 16:23:46 +08:00
parent 39886d50d2
commit 88e048f4d1
154 changed files with 28966 additions and 6 deletions

179
internal/api/README.md Normal file
View File

@@ -0,0 +1,179 @@
# API 层
本模块负责HTTP API的实现提供RESTful接口。
## 功能特性
- RESTful API设计
- JWT认证中间件
- 跨域支持
- 请求日志记录
- 统一错误处理
- 参数验证
## 模块结构
```
api/
├── middlewares/ # 中间件
│ ├── auth.go # 认证中间件
│ ├── cors.go # 跨域中间件
│ └── logging.go # 日志中间件
├── handlers/ # 请求处理器
│ ├── auth_handler.go # 认证处理器
│ ├── user_handler.go # 用户处理器
│ ├── role_handler.go # 角色处理器
│ └── menu_handler.go # 菜单处理器
├── routes/ # 路由配置
│ └── routes.go # 路由设置
└── README.md # 说明文档
```
## API 接口
### 认证接口
```
POST /api/auth/login # 用户登录
POST /api/auth/logout # 用户登出
GET /api/profile # 获取用户信息
PUT /api/profile # 更新用户信息
PUT /api/change-password # 修改密码
```
### 用户管理接口(管理员)
```
POST /api/admin/users # 创建用户
GET /api/admin/users # 获取用户列表
GET /api/admin/users/:id # 获取用户详情
PUT /api/admin/users/:id # 更新用户
DELETE /api/admin/users/:id # 删除用户
PUT /api/admin/users/:id/status # 更新用户状态
```
### 角色管理接口(管理员)
```
POST /api/admin/roles # 创建角色
GET /api/admin/roles # 获取角色列表
GET /api/admin/roles/:id # 获取角色详情
PUT /api/admin/roles/:id # 更新角色
DELETE /api/admin/roles/:id # 删除角色
PUT /api/admin/roles/:id/status # 更新角色状态
```
### 菜单管理接口(管理员)
```
POST /api/admin/menus # 创建菜单
GET /api/admin/menus # 获取菜单列表
GET /api/admin/menus/tree # 获取菜单树
GET /api/admin/menus/:id # 获取菜单详情
PUT /api/admin/menus/:id # 更新菜单
DELETE /api/admin/menus/:id # 删除菜单
PUT /api/admin/menus/:id/status # 更新菜单状态
PUT /api/admin/menus/:id/sort # 更新菜单排序
```
## 请求示例
### 用户登录
```bash
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "password"
}'
```
### 获取用户列表
```bash
curl -X GET "http://localhost:8080/api/admin/users?page=1&size=10&username=admin" \
-H "Authorization: Bearer your-jwt-token"
```
### 创建用户
```bash
curl -X POST http://localhost:8080/api/admin/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{
"username": "newuser",
"email": "newuser@example.com",
"password": "password",
"nickname": "新用户",
"role": "user"
}'
```
## 响应格式
### 成功响应
```json
{
"code": 200,
"message": "操作成功",
"data": {
"id": 1,
"username": "admin",
"email": "admin@example.com"
}
}
```
### 分页响应
```json
{
"code": 200,
"message": "操作成功",
"data": [...],
"total": 100,
"page": 1,
"size": 10
}
```
### 错误响应
```json
{
"code": 400,
"message": "参数错误"
}
```
## 中间件
### 认证中间件
验证JWT token将用户信息存储到上下文中。
### 管理员中间件
验证用户是否具有管理员权限。
### 跨域中间件
处理跨域请求,支持预检请求。
### 日志中间件
记录HTTP请求日志包括请求方法、路径、状态码、响应时间等。
## 错误处理
所有API都遵循统一的错误处理模式
- 参数验证错误400 Bad Request
- 认证失败401 Unauthorized
- 权限不足403 Forbidden
- 资源不存在404 Not Found
- 服务器错误500 Internal Server Error

View File

@@ -0,0 +1,73 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
)
// AuditLogHandler 审计日志处理器
type AuditLogHandler struct {
service services.AuditLogService
response *utils.Response
}
// NewAuditLogHandler 创建审计日志处理器
func NewAuditLogHandler(service services.AuditLogService) *AuditLogHandler {
return &AuditLogHandler{
service: service,
response: utils.NewResponse(),
}
}
// List 获取审计日志列表
func (h *AuditLogHandler) List(c *gin.Context) {
var req models.AuditLogListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 设置默认值
if req.Page < 1 {
req.Page = 1
}
if req.Size < 1 {
req.Size = 20
}
if req.SortBy == "" {
req.SortBy = "operation_time"
}
if req.SortOrder == "" {
req.SortOrder = "desc"
}
result, err := h.service.List(&req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, result)
}
// GetByID 获取审计日志详情
func (h *AuditLogHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
log, err := h.service.GetByID(uint(id))
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, log)
}

View File

@@ -0,0 +1,633 @@
package handlers
import (
"reflect"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
)
type FinanceHandler struct {
response *utils.Response
}
func NewFinanceHandler() *FinanceHandler {
return &FinanceHandler{response: utils.NewResponse()}
}
func getPageParams(c *gin.Context) (int, int) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
if page < 1 {
page = 1
}
if size < 1 {
size = 20
}
offset := (page - 1) * size
return offset, size
}
func (h *FinanceHandler) ListSandboxRecords(c *gin.Context) {
offset, size := getPageParams(c)
// 兼容多种参数命名user/user_idproject/project_id
userID := c.DefaultQuery("user_id", c.Query("user"))
project := c.DefaultQuery("project", c.Query("project_id"))
start := c.Query("start")
end := c.Query("end")
res, err := services.ListSandboxRecords(offset, size, userID, project, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) ListTokenUsages(c *gin.Context) {
offset, size := getPageParams(c)
userID := c.DefaultQuery("user_id", c.Query("user"))
project := c.DefaultQuery("project", c.Query("project_id"))
start := c.Query("start") // 对于token使用按day进行过滤
end := c.Query("end")
res, err := services.ListTokenUsages(offset, size, userID, project, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) ListMCPUsages(c *gin.Context) {
offset, size := getPageParams(c)
userID := c.DefaultQuery("user_id", c.Query("user"))
project := c.DefaultQuery("project", c.Query("project_id"))
start := c.Query("start")
end := c.Query("end")
res, err := services.ListMCPUsages(offset, size, userID, project, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) ListTransactionLogs(c *gin.Context) {
offset, size := getPageParams(c)
userID := c.Query("user_id")
orderID := c.Query("order_id")
txType := c.Query("type")
status := c.Query("status")
start := c.Query("start")
end := c.Query("end")
res, err := services.ListTransactionLogs(offset, size, userID, orderID, txType, status, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) ListPaymentRecords(c *gin.Context) {
offset, size := getPageParams(c)
userID := c.Query("user_id")
orderID := c.Query("order_id")
paypalOrderID := c.Query("paypal_order_id")
status := c.Query("status")
refundStatus := c.Query("refund_status")
payerEmail := c.Query("payer_email")
start := c.Query("start")
end := c.Query("end")
res, err := services.ListPaymentRecords(offset, size, userID, orderID, paypalOrderID, status, refundStatus, payerEmail, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) RefundPaymentRecord(c *gin.Context) {
var req struct {
OrderID string `json:"order_id"`
PayPalCaptureID string `json:"paypal_capture_id"`
Amount *int64 `json:"amount"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err := services.RefundPaymentRecord(req.OrderID, req.PayPalCaptureID, req.Amount)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "退款请求已提交"})
}
func (h *FinanceHandler) ListMcpAccountRechargeRecords(c *gin.Context) {
offset, size := getPageParams(c)
provider := c.Query("provider")
account := c.Query("account")
start := c.Query("start")
end := c.Query("end")
res, err := services.ListMcpAccountRechargeRecords(offset, size, provider, account, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) CreateMcpAccountRechargeRecord(c *gin.Context) {
var req struct {
ProviderID string `json:"provider_id" binding:"required"`
Amount float64 `json:"amount" binding:"required,gt=0"`
RechargeDate string `json:"recharge_date" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 获取当前操作人信息
var operatorID interface{}
operatorName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
// 尝试从userInfo中获取sub字段可能是UUID格式
if sub, ok := userInfo["sub"].(string); ok {
operatorID = sub
}
if name, ok := userInfo["name"].(string); ok {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok {
operatorName = email
}
}
}
// 如果user中没有sub尝试从user_id获取
if operatorID == nil {
operatorID, _ = c.Get("user_id")
}
err := services.CreateMcpAccountRechargeRecord(
req.ProviderID,
req.Amount,
req.RechargeDate,
operatorID,
operatorName,
req.Remark,
)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "创建成功"})
}
func (h *FinanceHandler) UpdateMcpAccountRechargeRecord(c *gin.Context) {
id := c.Param("id")
var req struct {
Amount *float64 `json:"amount"`
RechargeDate *string `json:"recharge_date"`
Remark *string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err := services.UpdateMcpAccountRechargeRecord(id, req.Amount, req.RechargeDate, req.Remark)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "更新成功"})
}
func (h *FinanceHandler) DeleteMcpAccountRechargeRecord(c *gin.Context) {
id := c.Param("id")
err := services.DeleteMcpAccountRechargeRecord(id)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "删除成功"})
}
func (h *FinanceHandler) GetMcpProviderAccounts(c *gin.Context) {
status := c.Query("status")
var isUsed *bool
if v := c.Query("is_used"); v != "" {
if v == "true" {
val := true
isUsed = &val
} else if v == "false" {
val := false
isUsed = &val
}
}
list, err := services.GetMcpProviderAccounts(status, isUsed)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
func (h *FinanceHandler) GetMcpAccountBalances(c *gin.Context) {
list, err := services.GetMcpAccountBalances()
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
func (h *FinanceHandler) GetMcpAccountBalanceHistory(c *gin.Context) {
providerID := c.Param("provider_id")
start := c.Query("start")
end := c.Query("end")
list, err := services.GetMcpAccountBalanceHistory(providerID, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
// ========== 模型账号充值记录和余额管理 ==========
func (h *FinanceHandler) ListModelAccountRechargeRecords(c *gin.Context) {
offset, size := getPageParams(c)
provider := c.Query("provider")
modelName := c.Query("model_name")
start := c.Query("start")
end := c.Query("end")
res, err := services.ListModelAccountRechargeRecords(offset, size, provider, modelName, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, res.List, res.Total, offset/size+1, size)
}
func (h *FinanceHandler) CreateModelAccountRechargeRecord(c *gin.Context) {
var req struct {
Account string `json:"account" binding:"required"`
Amount float64 `json:"amount" binding:"required,gt=0"`
RechargeDate string `json:"recharge_date" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 获取当前操作人信息
var operatorID interface{}
operatorName := "系统管理员"
// 先尝试从 user_id 获取
if userID, exists := c.Get("user_id"); exists {
operatorID = userID
}
// 从 user 信息中获取名称
if user, exists := c.Get("user"); exists {
// user 可能是 *models.UserInfo 类型
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok && name != "" {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok && email != "" {
operatorName = email
}
} else {
// 尝试使用反射获取结构体字段
userVal := reflect.ValueOf(user)
if userVal.Kind() == reflect.Ptr {
userVal = userVal.Elem()
}
if userVal.Kind() == reflect.Struct {
if nameField := userVal.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
if name := nameField.String(); name != "" {
operatorName = name
}
}
if operatorName == "系统管理员" {
if emailField := userVal.FieldByName("Email"); emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
operatorName = email
}
}
}
}
}
}
err := services.CreateModelAccountRechargeRecord(
req.Account,
req.Amount,
req.RechargeDate,
operatorID,
operatorName,
req.Remark,
)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "创建成功"})
}
func (h *FinanceHandler) UpdateModelAccountRechargeRecord(c *gin.Context) {
id := c.Param("id")
var req struct {
Amount *float64 `json:"amount"`
RechargeDate *string `json:"recharge_date"`
Remark *string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err := services.UpdateModelAccountRechargeRecord(id, req.Amount, req.RechargeDate, req.Remark)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "更新成功"})
}
func (h *FinanceHandler) DeleteModelAccountRechargeRecord(c *gin.Context) {
id := c.Param("id")
err := services.DeleteModelAccountRechargeRecord(id)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "删除成功"})
}
func (h *FinanceHandler) GetModelConfigAccounts(c *gin.Context) {
var enabled *bool
if v := c.Query("enabled"); v != "" {
if v == "true" {
val := true
enabled = &val
} else if v == "false" {
val := false
enabled = &val
}
}
list, err := services.GetModelConfigAccounts(enabled)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
func (h *FinanceHandler) GetModelAccountBalances(c *gin.Context) {
list, err := services.GetModelAccountBalances()
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
func (h *FinanceHandler) GetModelAccountBalanceHistory(c *gin.Context) {
account := c.Param("account")
start := c.Query("start")
end := c.Query("end")
list, err := services.GetModelAccountBalanceHistory(account, start, end)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, list)
}
func (h *FinanceHandler) AdjustMcpAccountBalance(c *gin.Context) {
providerID := c.Param("provider_id")
var req struct {
Balance float64 `json:"balance" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
operatorName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok && name != "" {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok && email != "" {
operatorName = email
}
} else {
userVal := reflect.ValueOf(user)
if userVal.Kind() == reflect.Ptr {
userVal = userVal.Elem()
}
if userVal.Kind() == reflect.Struct {
if nameField := userVal.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
if name := nameField.String(); name != "" {
operatorName = name
}
}
if operatorName == "系统管理员" {
if emailField := userVal.FieldByName("Email"); emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
operatorName = email
}
}
}
}
}
}
if err := services.AdjustMcpAccountBalance(providerID, req.Balance, operatorName, req.Remark); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "更新成功"})
}
func (h *FinanceHandler) AdjustModelAccountBalance(c *gin.Context) {
account := c.Param("account")
var req struct {
Balance float64 `json:"balance" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
operatorName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok && name != "" {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok && email != "" {
operatorName = email
}
} else {
userVal := reflect.ValueOf(user)
if userVal.Kind() == reflect.Ptr {
userVal = userVal.Elem()
}
if userVal.Kind() == reflect.Struct {
if nameField := userVal.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
if name := nameField.String(); name != "" {
operatorName = name
}
}
if operatorName == "系统管理员" {
if emailField := userVal.FieldByName("Email"); emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
operatorName = email
}
}
}
}
}
}
if err := services.AdjustModelAccountBalance(account, req.Balance, operatorName, req.Remark); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "更新成功"})
}
func (h *FinanceHandler) CreateModelAccountBalance(c *gin.Context) {
var req struct {
Account string `json:"account" binding:"required"`
Balance float64 `json:"balance" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
operatorName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok && name != "" {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok && email != "" {
operatorName = email
}
} else {
userVal := reflect.ValueOf(user)
if userVal.Kind() == reflect.Ptr {
userVal = userVal.Elem()
}
if userVal.Kind() == reflect.Struct {
if nameField := userVal.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
if name := nameField.String(); name != "" {
operatorName = name
}
}
if operatorName == "系统管理员" {
if emailField := userVal.FieldByName("Email"); emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
operatorName = email
}
}
}
}
}
}
if err := services.CreateModelAccountBalanceRecord(req.Account, req.Balance, operatorName, req.Remark); err != nil {
if strings.Contains(err.Error(), "已存在") {
h.response.BadRequest(c, err.Error())
} else {
h.response.InternalServerError(c, err.Error())
}
return
}
h.response.Success(c, gin.H{"message": "创建成功"})
}
func (h *FinanceHandler) CreateMcpAccountBalance(c *gin.Context) {
var req struct {
ProviderID string `json:"provider_id" binding:"required"`
Balance float64 `json:"balance" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
operatorName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok && name != "" {
operatorName = name
} else if email, ok := userInfo["email"].(string); ok && email != "" {
operatorName = email
}
} else {
userVal := reflect.ValueOf(user)
if userVal.Kind() == reflect.Ptr {
userVal = userVal.Elem()
}
if userVal.Kind() == reflect.Struct {
if nameField := userVal.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
if name := nameField.String(); name != "" {
operatorName = name
}
}
if operatorName == "系统管理员" {
if emailField := userVal.FieldByName("Email"); emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
operatorName = email
}
}
}
}
}
}
if err := services.CreateMcpAccountBalanceRecord(req.ProviderID, req.Balance, operatorName, req.Remark); err != nil {
if strings.Contains(err.Error(), "已存在") {
h.response.BadRequest(c, err.Error())
} else {
h.response.InternalServerError(c, err.Error())
}
return
}
h.response.Success(c, gin.H{"message": "创建成功"})
}

View File

@@ -0,0 +1,245 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
)
type GoalfyMaxUserHandler struct {
service services.GoalfyMaxUserService
response *utils.Response
}
func NewGoalfyMaxUserHandler(s services.GoalfyMaxUserService) *GoalfyMaxUserHandler {
return &GoalfyMaxUserHandler{service: s, response: utils.NewResponse()}
}
func (h *GoalfyMaxUserHandler) List(c *gin.Context) {
var req models.GoalfyMaxUserListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
users, total, err := h.service.List(&req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"users": users, "total": total, "page": req.Page, "size": req.Size})
}
func (h *GoalfyMaxUserHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
user, err := h.service.GetByID(uint(id64))
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
func (h *GoalfyMaxUserHandler) Create(c *gin.Context) {
var req models.GoalfyMaxUserCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
user, err := h.service.Create(&req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
func (h *GoalfyMaxUserHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.GoalfyMaxUserUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
user, err := h.service.Update(uint(id64), &req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
func (h *GoalfyMaxUserHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
if err := h.service.Delete(uint(id64)); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "删除成功"})
}
func (h *GoalfyMaxUserHandler) Ban(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.GoalfyMaxUserBanRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 从上下文获取当前管理员ID
adminID := 0
if userID, exists := c.Get("user_id"); exists {
switch v := userID.(type) {
case int:
adminID = v
case uint:
adminID = int(v)
case string:
if parsedID, err := strconv.Atoi(v); err == nil {
adminID = parsedID
}
}
}
if err := h.service.Ban(uint(id64), &req, adminID); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "封禁成功"})
}
func (h *GoalfyMaxUserHandler) Unban(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
if err := h.service.Unban(uint(id64)); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "解封成功"})
}
func (h *GoalfyMaxUserHandler) AddBalance(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.GoalfyMaxUserAddBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 从上下文获取当前管理员信息
operatorID := 0
operatorEmail := "system@goalfy.com"
if userID, exists := c.Get("user_id"); exists {
switch v := userID.(type) {
case int:
operatorID = v
case uint:
operatorID = int(v)
case string:
if parsedID, err := strconv.Atoi(v); err == nil {
operatorID = parsedID
}
}
}
// 尝试获取用户邮箱从userInfo中获取
if userInfo, exists := c.Get("user"); exists {
if user, ok := userInfo.(*models.UserInfo); ok && user != nil {
if user.Email != "" {
operatorEmail = user.Email
} else if user.PreferredUsername != "" {
operatorEmail = user.PreferredUsername + "@goalfy.com"
}
}
}
// 获取IP地址和UserAgent
ipAddress := c.ClientIP()
userAgent := c.Request.UserAgent()
if err := h.service.AddBalance(uint(id64), &req, operatorID, operatorEmail, ipAddress, userAgent); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "增加余额成功"})
}
func (h *GoalfyMaxUserHandler) DeductBalance(c *gin.Context) {
idStr := c.Param("id")
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.GoalfyMaxUserAddBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 从上下文获取当前管理员信息
operatorID := 0
operatorEmail := "system@goalfy.com"
if userID, exists := c.Get("user_id"); exists {
switch v := userID.(type) {
case int:
operatorID = v
case uint:
operatorID = int(v)
case string:
if parsedID, err := strconv.Atoi(v); err == nil {
operatorID = parsedID
}
}
}
// 尝试获取用户邮箱从userInfo中获取
if userInfo, exists := c.Get("user"); exists {
if user, ok := userInfo.(*models.UserInfo); ok && user != nil {
if user.Email != "" {
operatorEmail = user.Email
} else if user.PreferredUsername != "" {
operatorEmail = user.PreferredUsername + "@goalfy.com"
}
}
}
// 获取IP地址和UserAgent
ipAddress := c.ClientIP()
userAgent := c.Request.UserAgent()
if err := h.service.DeductBalance(uint(id64), &req, operatorID, operatorEmail, ipAddress, userAgent); err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "减少余额成功"})
}

View File

@@ -0,0 +1,254 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
)
type InviteCodeApplicationHandler struct {
service *services.InviteCodeApplicationService
}
// NewInviteCodeApplicationHandler 创建邀请码申请处理器
func NewInviteCodeApplicationHandler(db *gorm.DB) *InviteCodeApplicationHandler {
return &InviteCodeApplicationHandler{
service: services.NewInviteCodeApplicationService(db),
}
}
// SubmitApplication 提交邀请码申请(公开接口,官网使用)
func (h *InviteCodeApplicationHandler) SubmitApplication(c *gin.Context) {
var req models.InviteCodeApplicationCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
application, err := h.service.SubmitApplication(&req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "申请已提交我们将在1-2个工作日内处理您的申请",
"data": application,
})
}
// GetApplicationList 获取申请列表(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) GetApplicationList(c *gin.Context) {
var req models.InviteCodeApplicationListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.Size <= 0 {
req.Size = 20
}
response, err := h.service.GetApplicationList(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取申请列表失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": response,
})
}
// GetStatistics 获取申请统计(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) GetStatistics(c *gin.Context) {
stats, err := h.service.GetStatistics()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取统计信息失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": stats,
})
}
// ApproveApplication 审批通过申请(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) ApproveApplication(c *gin.Context) {
var req models.InviteCodeApplicationApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
// 从上下文获取操作人信息(需要在中间件中设置)
approvedBy := c.GetString("username")
if approvedBy == "" {
approvedBy = "admin"
}
if err := h.service.ApproveApplication(&req, approvedBy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "审批通过成功",
})
}
// RejectApplication 审批拒绝申请(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) RejectApplication(c *gin.Context) {
var req models.InviteCodeApplicationRejectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
// 从上下文获取操作人信息(需要在中间件中设置)
approvedBy := c.GetString("username")
if approvedBy == "" {
approvedBy = "admin"
}
if err := h.service.RejectApplication(&req, approvedBy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "审批拒绝成功",
})
}
// BatchApproveApplications 批量审批通过(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) BatchApproveApplications(c *gin.Context) {
var req models.InviteCodeApplicationBatchApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
// 从上下文获取操作人信息(需要在中间件中设置)
approvedBy := c.GetString("username")
if approvedBy == "" {
approvedBy = "admin"
}
if err := h.service.BatchApproveApplications(&req, approvedBy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "批量审批通过成功",
})
}
// BatchRejectApplications 批量审批拒绝(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) BatchRejectApplications(c *gin.Context) {
var req models.InviteCodeApplicationBatchRejectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数无效",
})
return
}
// 从上下文获取操作人信息(需要在中间件中设置)
approvedBy := c.GetString("username")
if approvedBy == "" {
approvedBy = "admin"
}
if err := h.service.BatchRejectApplications(&req, approvedBy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "批量审批拒绝成功",
})
}
// GetPendingCount 获取待处理申请数量(后台管理接口,需要权限)
func (h *InviteCodeApplicationHandler) GetPendingCount(c *gin.Context) {
count, err := h.service.GetPendingApplicationsCount()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取待处理数量失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": gin.H{
"count": count,
},
})
}
// RegisterRoutes 注册路由
func (h *InviteCodeApplicationHandler) RegisterRoutes(router *gin.RouterGroup, authMiddleware gin.HandlerFunc) {
// 公开接口(官网提交申请)
public := router.Group("/public")
{
public.POST("/invite-code/apply", h.SubmitApplication)
}
// 需要认证的接口(后台管理)
protected := router.Group("/invite-code/applications")
protected.Use(authMiddleware)
{
protected.GET("", h.GetApplicationList)
protected.GET("/statistics", h.GetStatistics)
protected.GET("/pending-count", h.GetPendingCount)
protected.POST("/approve", h.ApproveApplication)
protected.POST("/reject", h.RejectApplication)
protected.POST("/batch-approve", h.BatchApproveApplications)
protected.POST("/batch-reject", h.BatchRejectApplications)
}
}

View File

@@ -0,0 +1,458 @@
package handlers
import (
"net/http"
"regexp"
"strconv"
"strings"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"github.com/gin-gonic/gin"
)
type InviteCodeHandler struct {
service services.InviteCodeService
userLevelService services.UserLevelConfigService
}
func NewInviteCodeHandler(service services.InviteCodeService, userLevelService services.UserLevelConfigService) *InviteCodeHandler {
return &InviteCodeHandler{
service: service,
userLevelService: userLevelService,
}
}
// GetInviteCodeList 获取邀请码列表
func (h *InviteCodeHandler) GetInviteCodeList(c *gin.Context) {
var req models.InviteCodeListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误",
"data": nil,
})
return
}
// 设置默认分页参数
if req.Page <= 0 {
req.Page = 1
}
if req.Size <= 0 {
req.Size = 20
}
response, err := h.service.List(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
"data": nil,
})
return
}
// enrich invite_url
base := config.GetConfig().SSO.SSOServerURL
base = strings.TrimRight(base, "/")
enriched := make([]gin.H, 0, len(response.List))
for _, item := range response.List {
// 获取用户等级信息
var userLevelName string
if item.UserLevelID != nil {
if level, err := h.userLevelService.GetByID(*item.UserLevelID); err == nil {
userLevelName = level.LevelName
}
}
enriched = append(enriched, gin.H{
"id": item.ID,
"code": item.Code,
"is_used": item.IsUsed,
"client_id": item.ClientID,
"client_label": config.GetClientValue(item.ClientID),
"email": item.Email,
"user_level_id": item.UserLevelID,
"user_level_name": userLevelName,
"expires_at": item.ExpiresAt,
"created_at": item.CreatedAt,
"invite_url": base + "/invite/" + item.Code,
})
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": gin.H{
"list": enriched,
"total": response.Total,
},
})
}
// GetInviteCodeDetail 获取邀请码详情
func (h *InviteCodeHandler) GetInviteCodeDetail(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "ID格式错误",
"data": nil,
})
return
}
inviteCode, err := h.service.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "邀请码不存在",
"data": nil,
})
return
}
base := config.GetConfig().SSO.SSOServerURL
base = strings.TrimRight(base, "/")
// 获取用户等级信息
var userLevelName string
if inviteCode.UserLevelID != nil {
if level, err := h.userLevelService.GetByID(*inviteCode.UserLevelID); err == nil {
userLevelName = level.LevelName
}
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": gin.H{
"id": inviteCode.ID,
"code": inviteCode.Code,
"is_used": inviteCode.IsUsed,
"client_id": inviteCode.ClientID,
"client_label": config.GetClientValue(inviteCode.ClientID),
"email": inviteCode.Email,
"user_level_id": inviteCode.UserLevelID,
"user_level_name": userLevelName,
"expires_at": inviteCode.ExpiresAt,
"created_at": inviteCode.CreatedAt,
"invite_url": base + "/invite/" + inviteCode.Code,
},
})
}
// validateEmail 验证邮箱格式
func validateEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// CreateInviteCode 创建邀请码(支持设置过期时间和邮箱列表)
func (h *InviteCodeHandler) CreateInviteCode(c *gin.Context) {
var req models.InviteCodeCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误:" + err.Error(),
"data": nil,
})
return
}
// 验证邮箱格式
if len(req.Emails) > 0 {
for _, email := range req.Emails {
if email != "" && !validateEmail(email) {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "邮箱格式不正确: " + email,
"data": nil,
})
return
}
}
}
// 验证用户等级ID是否存在且启用
if req.UserLevelID != nil {
userLevel, err := h.userLevelService.GetByID(*req.UserLevelID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "用户等级不存在",
"data": nil,
})
return
}
if userLevel.Status != 1 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "用户等级已禁用",
"data": nil,
})
return
}
}
inviteCodes, err := h.service.Create(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
"data": nil,
})
return
}
base := config.GetConfig().SSO.SSOServerURL
base = strings.TrimRight(base, "/")
// 如果只创建了一个邀请码,返回单个对象(向后兼容)
if len(inviteCodes) == 1 {
inviteCode := inviteCodes[0]
var userLevelName string
if inviteCode.UserLevelID != nil {
if level, err := h.userLevelService.GetByID(*inviteCode.UserLevelID); err == nil {
userLevelName = level.LevelName
}
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "创建成功",
"data": gin.H{
"id": inviteCode.ID,
"code": inviteCode.Code,
"is_used": inviteCode.IsUsed,
"client_id": inviteCode.ClientID,
"client_label": config.GetClientValue(inviteCode.ClientID),
"email": inviteCode.Email,
"user_level_id": inviteCode.UserLevelID,
"user_level_name": userLevelName,
"expires_at": inviteCode.ExpiresAt,
"created_at": inviteCode.CreatedAt,
"invite_url": base + "/invite/" + inviteCode.Code,
},
})
return
}
// 如果创建了多个邀请码,返回数组
enriched := make([]gin.H, 0, len(inviteCodes))
for _, inviteCode := range inviteCodes {
var userLevelName string
if inviteCode.UserLevelID != nil {
if level, err := h.userLevelService.GetByID(*inviteCode.UserLevelID); err == nil {
userLevelName = level.LevelName
}
}
enriched = append(enriched, gin.H{
"id": inviteCode.ID,
"code": inviteCode.Code,
"is_used": inviteCode.IsUsed,
"client_id": inviteCode.ClientID,
"client_label": config.GetClientValue(inviteCode.ClientID),
"email": inviteCode.Email,
"user_level_id": inviteCode.UserLevelID,
"user_level_name": userLevelName,
"expires_at": inviteCode.ExpiresAt,
"created_at": inviteCode.CreatedAt,
"invite_url": base + "/invite/" + inviteCode.Code,
})
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "创建成功",
"data": enriched,
})
}
// UpdateInviteCode 更新邀请码(支持更新过期时间)
func (h *InviteCodeHandler) UpdateInviteCode(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "ID格式错误",
"data": nil,
})
return
}
var req models.InviteCodeUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误",
"data": nil,
})
return
}
inviteCode, err := h.service.Update(uint(id), &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
"data": nil,
})
return
}
base := config.GetConfig().SSO.SSOServerURL
base = strings.TrimRight(base, "/")
// 获取用户等级信息
var userLevelName string
if inviteCode.UserLevelID != nil {
if level, err := h.userLevelService.GetByID(*inviteCode.UserLevelID); err == nil {
userLevelName = level.LevelName
}
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "更新成功",
"data": gin.H{
"id": inviteCode.ID,
"code": inviteCode.Code,
"is_used": inviteCode.IsUsed,
"client_id": inviteCode.ClientID,
"client_label": config.GetClientValue(inviteCode.ClientID),
"email": inviteCode.Email,
"user_level_id": inviteCode.UserLevelID,
"user_level_name": userLevelName,
"expires_at": inviteCode.ExpiresAt,
"created_at": inviteCode.CreatedAt,
"invite_url": base + "/invite/" + inviteCode.Code,
},
})
}
// DeleteInviteCode 删除邀请码
func (h *InviteCodeHandler) DeleteInviteCode(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "ID格式错误",
"data": nil,
})
return
}
err = h.service.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "删除成功",
"data": nil,
})
}
// GetInviteCodeStatistics 获取统计信息
func (h *InviteCodeHandler) GetInviteCodeStatistics(c *gin.Context) {
stats, err := h.service.GetStatistics()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": stats,
})
}
// MarkInviteCodeAsUsed 标记邀请码为已使用
func (h *InviteCodeHandler) MarkInviteCodeAsUsed(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误",
"data": nil,
})
return
}
err := h.service.MarkAsUsed(req.Code)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "标记成功",
"data": nil,
})
}
// ValidateInviteCode 验证邀请码是否有效
func (h *InviteCodeHandler) ValidateInviteCode(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误",
"data": nil,
})
return
}
err := h.service.ValidateInviteCode(req.Code)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "邀请码有效",
"data": nil,
})
}
// GetClientOptions 获取客户端选项列表
func (h *InviteCodeHandler) GetClientOptions(c *gin.Context) {
options := config.GetClientOptions()
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": options,
})
}

View File

@@ -0,0 +1,186 @@
package handlers
import (
"net/http"
"strconv"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type MCPProvider struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
Provider string `json:"provider"`
BaseURL string `json:"base_url"`
Auth *string `json:"auth"`
Account *string `json:"account"`
PriceType string `json:"price_type"`
Price float64 `json:"price"`
FloatingRatio float64 `json:"floating_ratio"`
IsUsed bool `json:"is_used"`
Status string `json:"status"`
Description *string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type MCPProviderHandler struct {
response *utils.Response
}
func NewMCPProviderHandler() *MCPProviderHandler {
return &MCPProviderHandler{response: utils.NewResponse()}
}
func (h *MCPProviderHandler) List(c *gin.Context) {
db := storage.GetPG()
if db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "PostgreSQL未初始化"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
if page <= 0 {
page = 1
}
if size <= 0 {
size = 10
}
offset := (page - 1) * size
q := db.Table("mcp_providers")
if v := c.Query("provider"); v != "" {
q = q.Where("provider ILIKE ?", "%"+v+"%")
}
if v := c.Query("status"); v != "" {
q = q.Where("status = ?", v)
}
if v := c.Query("is_used"); v != "" {
if v == "true" {
q = q.Where("is_used = ?", true)
} else if v == "false" {
q = q.Where("is_used = ?", false)
}
}
var total int64
if err := q.Count(&total).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
var list []MCPProvider
if err := q.Order("created_at DESC").Offset(offset).Limit(size).Find(&list).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, list, total, page, size)
}
func (h *MCPProviderHandler) GetByID(c *gin.Context) {
db := storage.GetPG()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
h.response.BadRequest(c, "无效ID")
return
}
var m MCPProvider
if err := db.Table("mcp_providers").Where("id = ?", id).First(&m).Error; err != nil {
h.response.NotFound(c, "记录不存在")
return
}
h.response.Success(c, m)
}
func (h *MCPProviderHandler) Create(c *gin.Context) {
db := storage.GetPG()
var req map[string]any
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
if err := db.Table("mcp_providers").Create(req).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "创建成功")
}
func (h *MCPProviderHandler) Update(c *gin.Context) {
db := storage.GetPG()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
h.response.BadRequest(c, "无效ID")
return
}
var req map[string]any
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
delete(req, "id")
if err := db.Table("mcp_providers").Where("id = ?", id).Updates(req).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "更新成功")
}
func (h *MCPProviderHandler) Delete(c *gin.Context) {
db := storage.GetPG()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
h.response.BadRequest(c, "无效ID")
return
}
if err := db.Table("mcp_providers").Where("id = ?", id).Delete(nil).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "删除成功")
}
func (h *MCPProviderHandler) UpdateStatus(c *gin.Context) {
db := storage.GetPG()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
h.response.BadRequest(c, "无效ID")
return
}
var body struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
h.response.BadRequest(c, err.Error())
return
}
if err := db.Table("mcp_providers").Where("id = ?", id).Update("status", body.Status).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "状态更新成功")
}
func (h *MCPProviderHandler) UpdateIsUsed(c *gin.Context) {
db := storage.GetPG()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
h.response.BadRequest(c, "无效ID")
return
}
var body struct {
IsUsed bool `json:"is_used" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
h.response.BadRequest(c, err.Error())
return
}
if err := db.Table("mcp_providers").Where("id = ?", id).Update("is_used", body.IsUsed).Error; err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "使用状态更新成功")
}

View File

@@ -0,0 +1,134 @@
package handlers
import (
"strconv"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"github.com/gin-gonic/gin"
)
// MessagePushHandler 消息推送处理器
type MessagePushHandler struct {
service services.MessagePushService
response *utils.Response
}
// NewMessagePushHandler 创建消息推送处理器
func NewMessagePushHandler(service services.MessagePushService) *MessagePushHandler {
return &MessagePushHandler{
service: service,
response: utils.NewResponse(),
}
}
// SendMessage 发送消息
func (h *MessagePushHandler) SendMessage(c *gin.Context) {
var req models.MessagePushRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 获取当前用户信息
userID, exists := c.Get("user_id")
if !exists {
h.response.Unauthorized(c, "无法获取用户信息")
return
}
// 处理用户ID类型
var senderID int
switch v := userID.(type) {
case int:
senderID = v
case uint:
senderID = int(v)
case string:
var err error
senderID, err = strconv.Atoi(v)
if err != nil {
h.response.InternalServerError(c, "用户ID格式错误")
return
}
default:
h.response.InternalServerError(c, "用户ID格式错误")
return
}
// 获取发送人姓名(从用户信息中获取)
senderName := "系统管理员"
if user, exists := c.Get("user"); exists {
if userInfo, ok := user.(map[string]interface{}); ok {
if name, ok := userInfo["name"].(string); ok {
senderName = name
}
}
}
result, err := h.service.SendMessage(c.Request.Context(), &req, senderID, senderName)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, result)
}
// GetPushLogs 获取推送记录列表
func (h *MessagePushHandler) GetPushLogs(c *gin.Context) {
var req models.MessagePushListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
result, err := h.service.GetPushLogs(c.Request.Context(), &req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, result)
}
// GetPushLogByID 根据ID获取推送记录
func (h *MessagePushHandler) GetPushLogByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.response.BadRequest(c, "无效的推送记录ID")
return
}
log, err := h.service.GetPushLogByID(c.Request.Context(), id)
if err != nil {
if err.Error() == "推送记录不存在" {
h.response.NotFound(c, "推送记录不存在")
return
}
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, log)
}
// SearchUsers 搜索用户
func (h *MessagePushHandler) SearchUsers(c *gin.Context) {
var req models.UserSearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
result, err := h.service.SearchUsers(c.Request.Context(), &req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, result)
}

View File

@@ -0,0 +1,144 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// PageHandler 页面处理器
type PageHandler struct {
pageService services.PageService
response *utils.Response
logger *utils.Logger
}
// NewPageHandler 创建页面处理器
func NewPageHandler(pageService services.PageService, logger *utils.Logger) *PageHandler {
return &PageHandler{
pageService: pageService,
response: utils.NewResponse(),
logger: logger,
}
}
// Create 创建页面
func (h *PageHandler) Create(c *gin.Context) {
var req models.PageCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.ValidateError(c, err)
return
}
page, err := h.pageService.Create(&req)
if err != nil {
h.logger.Error("创建页面失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, page)
}
// GetByID 根据ID获取页面
func (h *PageHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Error("页面ID格式错误", zap.Error(err))
h.response.BadRequest(c, "无效的页面ID")
return
}
page, err := h.pageService.GetByID(uint(id))
if err != nil {
h.logger.Error("获取页面失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, page)
}
// Update 更新页面
func (h *PageHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Error("页面ID格式错误", zap.Error(err))
h.response.BadRequest(c, "无效的页面ID")
return
}
var req models.PageUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.ValidateError(c, err)
return
}
page, err := h.pageService.Update(uint(id), &req)
if err != nil {
h.logger.Error("更新页面失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, page)
}
// Delete 删除页面
func (h *PageHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Error("页面ID格式错误", zap.Error(err))
h.response.BadRequest(c, "无效的页面ID")
return
}
err = h.pageService.Delete(uint(id))
if err != nil {
h.logger.Error("删除页面失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "页面删除成功"})
}
// List 获取页面列表
func (h *PageHandler) List(c *gin.Context) {
var req models.PageListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.ValidateError(c, err)
return
}
pages, total, err := h.pageService.List(&req)
if err != nil {
h.logger.Error("获取页面列表失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
// 如果没有分页参数,直接返回页面列表
if req.Page <= 0 || req.Size <= 0 {
h.response.Success(c, pages)
return
}
// 有分页参数时返回完整的分页信息
h.response.Success(c, gin.H{
"pages": pages,
"total": total,
"page": req.Page,
"size": req.Size,
})
}

View File

@@ -0,0 +1,117 @@
package handlers
import (
"github.com/gin-gonic/gin"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
)
// QuotaHandler 配额处理器
type QuotaHandler struct {
quotaService services.QuotaService
response *utils.Response
}
// NewQuotaHandler 创建配额处理器
func NewQuotaHandler(quotaService services.QuotaService) *QuotaHandler {
return &QuotaHandler{
quotaService: quotaService,
response: utils.NewResponse(),
}
}
// GetQuotaHistory 获取配额历史数据
func (h *QuotaHandler) GetQuotaHistory(c *gin.Context) {
var req models.QuotaHistoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 调用服务层
response, err := h.quotaService.GetQuotaHistory(&req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
// 返回响应
if response.Success {
h.response.Success(c, response.Data)
} else {
h.response.BadRequest(c, response.Message)
}
}
// HealthCheck 健康检查
func (h *QuotaHandler) HealthCheck(c *gin.Context) {
err := h.quotaService.HealthCheck()
if err != nil {
h.response.InternalServerError(c, "配额服务健康检查失败")
return
}
h.response.Success(c, gin.H{"status": "ok", "service": "quota"})
}
// GetQuotaRules 获取配额规则列表(透传网关)
func (h *QuotaHandler) GetQuotaRules(c *gin.Context) {
resp, err := h.quotaService.GetQuotaRules()
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if resp.Success {
// 透传 data
c.JSON(200, gin.H{
"code": 200,
"message": "操作成功",
"data": resp.Data,
})
return
}
h.response.BadRequest(c, resp.Message)
}
// CreateQuotaRule 创建配额规则(代理网关)
func (h *QuotaHandler) CreateQuotaRule(c *gin.Context) {
var body map[string]any
if err := c.ShouldBindJSON(&body); err != nil {
h.response.ValidateError(c, err)
return
}
resp, err := h.quotaService.CreateQuotaRule(body)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
c.JSON(200, gin.H{"code": 200, "message": "操作成功", "data": resp.Data})
}
// UpdateQuotaRule 更新配额规则(代理网关)
func (h *QuotaHandler) UpdateQuotaRule(c *gin.Context) {
id := c.Param("id")
var body map[string]any
if err := c.ShouldBindJSON(&body); err != nil {
h.response.ValidateError(c, err)
return
}
resp, err := h.quotaService.UpdateQuotaRule(id, body)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
c.JSON(200, gin.H{"code": 200, "message": "操作成功", "data": resp.Data})
}
// DeleteQuotaRule 删除配额规则(代理网关)
func (h *QuotaHandler) DeleteQuotaRule(c *gin.Context) {
id := c.Param("id")
resp, err := h.quotaService.DeleteQuotaRule(id)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
c.JSON(200, gin.H{"code": 200, "message": "操作成功", "data": resp.Data})
}

View File

@@ -0,0 +1,182 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// RBACHandler 简化的RBAC处理器
type RBACHandler struct {
rbacService services.RBACService
response *utils.Response
logger *utils.Logger
}
// NewRBACHandler 创建RBAC处理器
func NewRBACHandler(rbacService services.RBACService, logger *utils.Logger) *RBACHandler {
return &RBACHandler{
rbacService: rbacService,
response: utils.NewResponse(),
logger: logger,
}
}
// AssignRolePagePermissions 分配角色页面权限
func (h *RBACHandler) AssignRolePagePermissions(c *gin.Context) {
var req models.RolePagePermissionAssignRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.Error(c, 400, "参数错误")
return
}
err := h.rbacService.AssignRolePagePermissions(&req)
if err != nil {
h.logger.Error("分配角色页面权限失败", zap.Error(err))
h.response.Error(c, 500, "分配角色页面权限失败")
return
}
h.response.Success(c, "分配角色页面权限成功")
}
// RemoveRolePagePermissions 移除角色页面权限
func (h *RBACHandler) RemoveRolePagePermissions(c *gin.Context) {
roleIDStr := c.Param("id")
roleID, err := strconv.ParseUint(roleIDStr, 10, 32)
if err != nil {
h.logger.Error("角色ID格式错误", zap.Error(err))
h.response.Error(c, 400, "角色ID格式错误")
return
}
var req struct {
PageIDs []uint `json:"pageIds" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.Error(c, 400, "参数错误")
return
}
err = h.rbacService.RemoveRolePagePermissions(uint(roleID), req.PageIDs)
if err != nil {
h.logger.Error("移除角色页面权限失败", zap.Error(err))
h.response.Error(c, 500, "移除角色页面权限失败")
return
}
h.response.Success(c, "移除角色页面权限成功")
}
// GetRolePagePermissions 获取角色页面权限
func (h *RBACHandler) GetRolePagePermissions(c *gin.Context) {
roleIDStr := c.Param("id")
roleID, err := strconv.ParseUint(roleIDStr, 10, 32)
if err != nil {
h.logger.Error("角色ID格式错误", zap.Error(err))
h.response.Error(c, 400, "角色ID格式错误")
return
}
pages, err := h.rbacService.GetRolePagePermissions(uint(roleID))
if err != nil {
h.logger.Error("获取角色页面权限失败", zap.Error(err))
h.response.Error(c, 500, "获取角色页面权限失败")
return
}
h.response.Success(c, pages)
}
// GetUserPermissions 获取用户权限
func (h *RBACHandler) GetUserPermissions(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
h.logger.Error("用户ID格式错误", zap.Error(err))
h.response.Error(c, 400, "用户ID格式错误")
return
}
permissions, err := h.rbacService.GetUserPermissionsResponse(uint(userID))
if err != nil {
h.logger.Error("获取用户权限失败", zap.Error(err))
h.response.Error(c, 500, "获取用户权限失败")
return
}
h.response.Success(c, permissions)
}
// GetRolePermissions 获取角色权限
func (h *RBACHandler) GetRolePermissions(c *gin.Context) {
roleIDStr := c.Param("id")
roleID, err := strconv.ParseUint(roleIDStr, 10, 32)
if err != nil {
h.logger.Error("角色ID格式错误", zap.Error(err))
h.response.Error(c, 400, "角色ID格式错误")
return
}
permissions, err := h.rbacService.GetRolePagePermissionsResponse(uint(roleID))
if err != nil {
h.logger.Error("获取角色权限失败", zap.Error(err))
h.response.Error(c, 500, "获取角色权限失败")
return
}
h.response.Success(c, permissions)
}
// CheckPagePermission 检查页面权限
func (h *RBACHandler) CheckPagePermission(c *gin.Context) {
userIDStr := c.Query("user_id")
pagePath := c.Query("page_path")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
h.logger.Error("用户ID格式错误", zap.Error(err))
h.response.Error(c, 400, "用户ID格式错误")
return
}
hasPermission, err := h.rbacService.CheckUserPagePermission(uint(userID), pagePath)
if err != nil {
h.logger.Error("检查页面权限失败", zap.Error(err))
h.response.Error(c, 500, "检查页面权限失败")
return
}
h.response.Success(c, gin.H{
"hasPermission": hasPermission,
"pagePath": pagePath,
})
}
// GetUserAccessiblePages 获取用户可访问页面
func (h *RBACHandler) GetUserAccessiblePages(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
h.logger.Error("用户ID格式错误", zap.Error(err))
h.response.Error(c, 400, "用户ID格式错误")
return
}
pages, err := h.rbacService.GetUserAccessiblePages(uint(userID))
if err != nil {
h.logger.Error("获取用户可访问页面失败", zap.Error(err))
h.response.Error(c, 500, "获取用户可访问页面失败")
return
}
h.response.Success(c, gin.H{
"pages": pages,
})
}

View File

@@ -0,0 +1,228 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// RoleHandler 角色处理器
type RoleHandler struct {
roleService services.RoleService
rbacService services.RBACService
response *utils.Response
logger *utils.Logger
}
// NewRoleHandler 创建角色处理器
func NewRoleHandler(roleService services.RoleService, rbacService services.RBACService, logger *utils.Logger) *RoleHandler {
return &RoleHandler{
roleService: roleService,
rbacService: rbacService,
response: utils.NewResponse(),
logger: logger,
}
}
// Create 创建角色
func (h *RoleHandler) Create(c *gin.Context) {
var req models.RoleCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
role, err := h.roleService.Create(&req)
if err != nil {
h.logger.Error("创建角色失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, role)
}
// GetByID 获取角色详情
func (h *RoleHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
role, err := h.roleService.GetByID(uint(id))
if err != nil {
h.logger.Error("获取角色失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, role)
}
// Update 更新角色
func (h *RoleHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
var req models.RoleUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
role, err := h.roleService.Update(uint(id), &req)
if err != nil {
h.logger.Error("更新角色失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, role)
}
// Delete 删除角色
func (h *RoleHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
err = h.roleService.Delete(uint(id))
if err != nil {
h.logger.Error("删除角色失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "角色删除成功"})
}
// List 获取角色列表
func (h *RoleHandler) List(c *gin.Context) {
var req models.RoleListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
roles, total, err := h.roleService.List(&req)
if err != nil {
h.logger.Error("获取角色列表失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
// 为每个角色获取页面权限
rolesWithPermissions := make([]gin.H, len(roles))
for i, role := range roles {
// 获取角色的页面权限
pages, err := h.rbacService.GetRolePagePermissions(role.ID)
if err != nil {
h.logger.Warn("获取角色页面权限失败", zap.Uint("roleId", role.ID), zap.Error(err))
pages = []models.Page{} // 如果获取失败,返回空数组
}
rolesWithPermissions[i] = gin.H{
"id": role.ID,
"name": role.Name,
"level": role.Level,
"description": role.Description,
"isDefault": role.IsDefault,
"createdAt": role.CreatedAt,
"updatedAt": role.UpdatedAt,
"deletedAt": role.DeletedAt,
"pages": pages, // 添加页面权限信息
}
}
h.response.Success(c, gin.H{
"roles": rolesWithPermissions,
"total": total,
"page": req.Page,
"size": req.Size,
})
}
// UpdateStatus 更新角色状态
func (h *RoleHandler) UpdateStatus(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
var req struct {
Status int `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err = h.roleService.UpdateStatus(uint(id), req.Status)
if err != nil {
h.logger.Error("更新角色状态失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "角色状态更新成功"})
}
// UpdatePermissions 更新角色权限
func (h *RoleHandler) UpdatePermissions(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
var req models.RolePagePermissionAssignRequest
req.RoleID = uint(id)
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err = h.rbacService.AssignRolePagePermissions(&req)
if err != nil {
h.logger.Error("更新角色权限失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "角色权限更新成功"})
}
// GetRolePermissions 获取角色权限
func (h *RoleHandler) GetRolePermissions(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的角色ID")
return
}
permissions, err := h.rbacService.GetRolePagePermissions(uint(id))
if err != nil {
h.logger.Error("获取角色权限失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"permissions": permissions})
}

View File

@@ -0,0 +1,267 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SSOHandler SSO处理器
type SSOHandler struct {
ssoService services.SSOService
response *utils.Response
logger *utils.Logger
}
// NewSSOHandler 创建SSO处理器
func NewSSOHandler(ssoService services.SSOService, logger *utils.Logger) *SSOHandler {
return &SSOHandler{
ssoService: ssoService,
response: utils.NewResponse(),
logger: logger,
}
}
// HandleSSOLogin 处理SSO登录请求合并登录和回调逻辑
func (h *SSOHandler) HandleSSOLogin(c *gin.Context) {
if c.Request.Method != http.MethodPost {
h.response.BadRequest(c, "Method not allowed")
return
}
var req models.SSOCallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 解析失败时,走登录逻辑
h.handleLoginLogic(c, models.SSOLoginRequest{})
return
}
// 如果code为空走登录逻辑
if req.Code == "" {
h.handleLoginLogic(c, models.SSOLoginRequest{})
return
}
// 如果code不为空走回调逻辑
h.handleCallbackLogic(c, req)
}
// HandleSSOCallback 处理SSO回调
func (h *SSOHandler) HandleSSOCallback(c *gin.Context) {
if c.Request.Method != http.MethodPost {
h.response.BadRequest(c, "Method not allowed")
return
}
var req models.SSOCallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 验证参数
if req.Code == "" || req.State == "" {
h.response.BadRequest(c, "Code and state are required")
return
}
// 调用服务层处理回调
response, err := h.ssoService.HandleCallback(c.Request.Context(), &req)
if err != nil {
h.logger.Error("failed to handle SSO callback", zap.Error(err))
// 返回更具体的错误信息,避免前端重复尝试
if strings.Contains(err.Error(), "password") {
h.response.BadRequest(c, "数据库表结构错误,请联系管理员")
} else {
h.response.InternalServerError(c, "SSO登录处理失败请稍后重试")
}
return
}
h.response.Success(c, response)
}
// HandleRefreshToken 处理令牌刷新
func (h *SSOHandler) HandleRefreshToken(c *gin.Context) {
if c.Request.Method != http.MethodPost {
h.response.BadRequest(c, "Method not allowed")
return
}
var req models.RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
if req.RefreshToken == "" {
h.response.BadRequest(c, "Refresh token is required")
return
}
// 调用服务层刷新令牌
response, err := h.ssoService.RefreshToken(c.Request.Context(), &req)
if err != nil {
h.logger.Error("failed to refresh token", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, response)
}
// HandleLogout 处理登出请求
func (h *SSOHandler) HandleLogout(c *gin.Context) {
if c.Request.Method != http.MethodPost {
h.response.BadRequest(c, "Method not allowed")
return
}
// 从Authorization头获取访问令牌
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
h.response.Unauthorized(c, "Authorization header is required")
return
}
// 提取Bearer令牌
token := ""
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
token = authHeader[7:]
}
if token == "" {
h.response.Unauthorized(c, "Invalid authorization header")
return
}
// 调用服务层登出
response, err := h.ssoService.Logout(c.Request.Context(), token)
if err != nil {
h.logger.Error("failed to logout", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, response)
}
// HandleUserInfo 处理用户信息请求
func (h *SSOHandler) HandleUserInfo(c *gin.Context) {
if c.Request.Method != http.MethodGet {
h.response.BadRequest(c, "Method not allowed")
return
}
// 从Authorization头获取访问令牌
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
h.response.Unauthorized(c, "Authorization header is required")
return
}
// 提取Bearer令牌
token := ""
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
token = authHeader[7:]
}
if token == "" {
h.response.Unauthorized(c, "Invalid authorization header")
return
}
// 调用服务层获取用户信息
response, err := h.ssoService.GetUserInfo(c.Request.Context(), token)
if err != nil {
h.logger.Error("failed to get user info", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, response)
}
// handleLoginLogic 处理登录逻辑
func (h *SSOHandler) handleLoginLogic(c *gin.Context, req models.SSOLoginRequest) {
// 调用服务层初始化登录
response, err := h.ssoService.InitiateLogin(c.Request.Context())
if err != nil {
h.logger.Error("failed to initiate SSO login", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, response)
}
// handleCallbackLogic 处理回调逻辑
func (h *SSOHandler) handleCallbackLogic(c *gin.Context, req models.SSOCallbackRequest) {
// 验证参数
if req.State == "" {
// 参数缺失时,走登录逻辑
h.handleLoginLogic(c, models.SSOLoginRequest{})
return
}
// 调用服务层处理回调
response, err := h.ssoService.HandleCallback(c.Request.Context(), &req)
if err != nil {
h.logger.Error("failed to handle SSO callback", zap.Error(err))
// 回调失败时,走登录逻辑
h.handleLoginLogic(c, models.SSOLoginRequest{})
return
}
h.response.Success(c, response)
}
// GetOnlineUsers 获取在线用户列表
func (h *SSOHandler) GetOnlineUsers(c *gin.Context) {
users, err := h.ssoService.GetOnlineUsers(c.Request.Context())
if err != nil {
h.logger.Error("failed to get online users", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, users)
}
// GetOnlineUserCount 获取在线用户数量
func (h *SSOHandler) GetOnlineUserCount(c *gin.Context) {
count, err := h.ssoService.GetOnlineUserCount(c.Request.Context())
if err != nil {
h.logger.Error("failed to get online user count", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"count": count})
}
// BatchLogout 批量登出用户
func (h *SSOHandler) BatchLogout(c *gin.Context) {
var req struct {
UserIDs []int `json:"user_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err := h.ssoService.BatchUserLogout(c.Request.Context(), req.UserIDs)
if err != nil {
h.logger.Error("failed to batch logout users", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "批量登出成功")
}

View File

@@ -0,0 +1,189 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SystemConfigHandler 系统配置处理器
type SystemConfigHandler struct {
service services.SystemConfigService
response *utils.Response
logger *utils.Logger
}
// NewSystemConfigHandler 创建系统配置处理器
func NewSystemConfigHandler(service services.SystemConfigService, logger *utils.Logger) *SystemConfigHandler {
return &SystemConfigHandler{
service: service,
response: utils.NewResponse(),
logger: logger,
}
}
// Create 创建系统配置
func (h *SystemConfigHandler) Create(c *gin.Context) {
var req models.SystemConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
config, err := h.service.Create(&req)
if err != nil {
h.logger.Error("创建系统配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, config)
}
// GetByID 获取系统配置详情
func (h *SystemConfigHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
config, err := h.service.GetByID(uint(id))
if err != nil {
h.logger.Error("获取系统配置失败", zap.Error(err))
h.response.NotFound(c, "配置不存在")
return
}
h.response.Success(c, config)
}
// GetByKey 根据配置标识获取系统配置
func (h *SystemConfigHandler) GetByKey(c *gin.Context) {
key := c.Param("key")
if key == "" {
h.response.BadRequest(c, "配置标识不能为空")
return
}
config, err := h.service.GetByKey(key)
if err != nil {
h.logger.Error("获取系统配置失败", zap.Error(err))
h.response.NotFound(c, "配置不存在")
return
}
h.response.Success(c, config)
}
// List 获取系统配置列表
func (h *SystemConfigHandler) List(c *gin.Context) {
var req models.SystemConfigListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.Size <= 0 {
req.Size = 20
}
configs, total, err := h.service.List(&req)
if err != nil {
h.logger.Error("获取系统配置列表失败", zap.Error(err))
h.response.InternalServerError(c, "获取列表失败")
return
}
h.response.Page(c, configs, total, req.Page, req.Size)
}
// Update 更新系统配置
func (h *SystemConfigHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.SystemConfigUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
config, err := h.service.Update(uint(id), &req)
if err != nil {
h.logger.Error("更新系统配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, config)
}
// Delete 删除系统配置
func (h *SystemConfigHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
err = h.service.Delete(uint(id))
if err != nil {
h.logger.Error("删除系统配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, nil)
}
// UpdateStatus 更新状态
func (h *SystemConfigHandler) UpdateStatus(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.SystemConfigUpdateStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err = h.service.UpdateStatus(uint(id), req.Status)
if err != nil {
h.logger.Error("更新系统配置状态失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, nil)
}
// GetAll 获取所有系统配置
func (h *SystemConfigHandler) GetAll(c *gin.Context) {
configs, err := h.service.GetAll()
if err != nil {
h.logger.Error("获取所有系统配置失败", zap.Error(err))
h.response.InternalServerError(c, "获取配置失败")
return
}
h.response.Success(c, configs)
}

View File

@@ -0,0 +1,195 @@
package handlers
import (
"strconv"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"github.com/gin-gonic/gin"
)
// UserFeedbackHandler 用户反馈处理器
type UserFeedbackHandler struct {
service *services.UserFeedbackService
response *utils.Response
}
// NewUserFeedbackHandler 创建用户反馈处理器
func NewUserFeedbackHandler(service *services.UserFeedbackService) *UserFeedbackHandler {
return &UserFeedbackHandler{
service: service,
response: utils.NewResponse(),
}
}
// List 获取用户反馈列表
func (h *UserFeedbackHandler) List(c *gin.Context) {
var req models.UserFeedbackListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
result, err := h.service.List(c.Request.Context(), &req)
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, result)
}
// GetByID 根据ID获取用户反馈
func (h *UserFeedbackHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.response.BadRequest(c, "无效的反馈ID")
return
}
feedbackItem, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
if err.Error() == "反馈不存在" {
h.response.NotFound(c, "反馈不存在")
return
}
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, feedbackItem)
}
// MarkHandled 标记为已处理
func (h *UserFeedbackHandler) MarkHandled(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.response.BadRequest(c, "无效的反馈ID")
return
}
var req models.UserFeedbackMarkRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 获取当前用户ID从JWT token中解析
userID, exists := c.Get("user_id")
if !exists {
h.response.Unauthorized(c, "无法获取用户信息")
return
}
// 处理用户ID支持多种类型
var handledBy int
switch v := userID.(type) {
case int:
handledBy = v
case uint:
handledBy = int(v)
case string:
var err error
handledBy, err = strconv.Atoi(v)
if err != nil {
h.response.InternalServerError(c, "用户ID格式错误")
return
}
default:
h.response.InternalServerError(c, "用户ID格式错误")
return
}
// 获取当前状态以确定切换后的状态
feedback, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
if err.Error() == "反馈不存在" {
h.response.NotFound(c, "反馈不存在")
return
}
h.response.InternalServerError(c, err.Error())
return
}
newStatus := 0
message := "标记为未处理成功"
if feedback.Status == 0 {
// 当前是未处理,切换为已处理
newStatus = 1
message = "标记为已处理成功"
}
err = h.service.MarkHandled(c.Request.Context(), id, handledBy, req.Note)
if err != nil {
if err.Error() == "反馈不存在" {
h.response.NotFound(c, "反馈不存在")
return
}
if err.Error() == "处理人ID无效" {
h.response.BadRequest(c, err.Error())
return
}
h.response.InternalServerError(c, err.Error())
return
}
// 返回处理结果
result := map[string]interface{}{
"id": id,
"status": newStatus,
"message": message,
}
if newStatus == 1 {
result["handled_by"] = handledBy
}
h.response.Success(c, result)
}
// Delete 删除用户反馈
func (h *UserFeedbackHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.response.BadRequest(c, "无效的反馈ID")
return
}
err = h.service.Delete(c.Request.Context(), id)
if err != nil {
if err.Error() == "反馈不存在" {
h.response.NotFound(c, "反馈不存在")
return
}
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "删除成功"})
}
// GetStatistics 获取反馈统计信息
func (h *UserFeedbackHandler) GetStatistics(c *gin.Context) {
stats, err := h.service.GetStatistics(c.Request.Context())
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, stats)
}
// hasHTTPPrefix 判断字符串是否为 http/https URL
func hasHTTPPrefix(s string) bool {
if len(s) < 7 {
return false
}
if len(s) >= 8 && (s[:8] == "https://") {
return true
}
return len(s) >= 7 && (s[:7] == "http://")
}

View File

@@ -0,0 +1,285 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// UserHandler 用户处理器
type UserHandler struct {
userService services.UserService
rbacService services.RBACService
response *utils.Response
logger *utils.Logger
}
// NewUserHandler 创建用户处理器
func NewUserHandler(userService services.UserService, rbacService services.RBACService, logger *utils.Logger) *UserHandler {
return &UserHandler{
userService: userService,
rbacService: rbacService,
response: utils.NewResponse(),
logger: logger,
}
}
// Create 创建用户
func (h *UserHandler) Create(c *gin.Context) {
var req models.UserCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
user, err := h.userService.Create(&req)
if err != nil {
h.logger.Error("创建用户失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
// GetByID 获取用户详情
func (h *UserHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
user, err := h.userService.GetByID(uint(id))
if err != nil {
h.logger.Error("获取用户失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
// Update 更新用户
func (h *UserHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
var req models.UserUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
user, err := h.userService.Update(uint(id), &req)
if err != nil {
h.logger.Error("更新用户失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, user)
}
// Delete 删除用户
func (h *UserHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
err = h.userService.Delete(uint(id))
if err != nil {
h.logger.Error("删除用户失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "用户删除成功"})
}
// List 获取用户列表
func (h *UserHandler) List(c *gin.Context) {
var req models.UserListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
users, total, err := h.userService.ListWithRoles(&req)
if err != nil {
h.logger.Error("获取用户列表失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{
"users": users,
"total": total,
"page": req.Page,
"size": req.Size,
})
}
// UpdateStatus 更新用户状态
func (h *UserHandler) UpdateStatus(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
var req struct {
Status int `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err = h.userService.UpdateStatus(uint(id), req.Status)
if err != nil {
h.logger.Error("更新用户状态失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "用户状态更新成功"})
}
// UpdateRoles 更新用户角色
func (h *UserHandler) UpdateRoles(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
var req models.UserRoleAssignRequest
req.UserID = uint(id)
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 现在用户只有一个角色直接更新用户的role_id
err = h.userService.UpdateRole(uint(id), req.RoleIDs[0])
if err != nil {
h.logger.Error("更新用户角色失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "用户角色更新成功"})
}
// GetUserRoles 获取用户角色
func (h *UserHandler) GetUserRoles(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
// 现在用户只有一个角色,直接获取用户信息
user, err := h.userService.GetByID(uint(id))
if err != nil {
h.logger.Error("获取用户信息失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
// 获取角色信息
role, err := h.rbacService.GetRoleByID(user.RoleID)
if err != nil {
h.logger.Error("获取角色信息失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"roles": []models.Role{*role}})
}
// GetUserPermissions 获取用户权限
func (h *UserHandler) GetUserPermissions(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
permissions, err := h.rbacService.GetUserPermissionsResponse(uint(id))
if err != nil {
h.logger.Error("获取用户权限失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"permissions": permissions})
}
// CheckUserRole 检查用户当前系统角色
// GET /admin/users/check-role/:user_id
func (h *UserHandler) CheckUserRole(c *gin.Context) {
userIDStr := c.Param("user_id")
userID, err := strconv.Atoi(userIDStr)
if err != nil {
h.response.BadRequest(c, "无效的用户ID")
return
}
isSystemAdmin, err := h.userService.CheckUserSystemRole(userID)
if err != nil {
h.logger.Error("检查用户系统角色失败", zap.Int("user_id", userID), zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
currentRole := "custom"
if isSystemAdmin {
currentRole = "sys_admin"
}
h.response.Success(c, gin.H{
"is_system_admin": isSystemAdmin,
"current_role": currentRole,
})
}
// ChangeUserSystemRole 变更用户系统角色
// POST /admin/users/change-system-role
func (h *UserHandler) ChangeUserSystemRole(c *gin.Context) {
var req struct {
UserID int `json:"user_id" binding:"required"`
SystemRole string `json:"system_role" binding:"required,oneof=sys_admin custom"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err := h.userService.ChangeUserSystemRole(req.UserID, req.SystemRole)
if err != nil {
h.logger.Error("变更用户系统角色失败", zap.Int("user_id", req.UserID), zap.String("system_role", req.SystemRole), zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, gin.H{"message": "角色变更成功"})
}

View File

@@ -0,0 +1,171 @@
package handlers
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// UserLevelConfigHandler 用户等级配置处理器
type UserLevelConfigHandler struct {
service services.UserLevelConfigService
response *utils.Response
logger *utils.Logger
}
// NewUserLevelConfigHandler 创建用户等级配置处理器
func NewUserLevelConfigHandler(service services.UserLevelConfigService, logger *utils.Logger) *UserLevelConfigHandler {
return &UserLevelConfigHandler{
service: service,
response: utils.NewResponse(),
logger: logger,
}
}
// Create 创建用户等级配置
func (h *UserLevelConfigHandler) Create(c *gin.Context) {
var req models.UserLevelConfigCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
config, err := h.service.Create(&req)
if err != nil {
h.logger.Error("创建用户等级配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, config)
}
// GetByID 获取用户等级配置详情
func (h *UserLevelConfigHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
config, err := h.service.GetByID(uint(id))
if err != nil {
h.logger.Error("获取用户等级配置失败", zap.Error(err))
h.response.NotFound(c, "配置不存在")
return
}
h.response.Success(c, config)
}
// List 获取用户等级配置列表
func (h *UserLevelConfigHandler) List(c *gin.Context) {
var req models.UserLevelConfigListRequest
if err := c.ShouldBindQuery(&req); err != nil {
h.response.ValidateError(c, err)
return
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.Size <= 0 {
req.Size = 20
}
configs, total, err := h.service.List(&req)
if err != nil {
h.logger.Error("获取用户等级配置列表失败", zap.Error(err))
h.response.InternalServerError(c, "获取列表失败")
return
}
h.response.Page(c, configs, total, req.Page, req.Size)
}
// Update 更新用户等级配置
func (h *UserLevelConfigHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.UserLevelConfigUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
config, err := h.service.Update(uint(id), &req)
if err != nil {
h.logger.Error("更新用户等级配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, config)
}
// Delete 删除用户等级配置
func (h *UserLevelConfigHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
err = h.service.Delete(uint(id))
if err != nil {
h.logger.Error("删除用户等级配置失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, nil)
}
// UpdateStatus 更新状态
func (h *UserLevelConfigHandler) UpdateStatus(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.response.BadRequest(c, "无效的ID")
return
}
var req models.UserLevelConfigUpdateStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.ValidateError(c, err)
return
}
err = h.service.UpdateStatus(uint(id), req.Status)
if err != nil {
h.logger.Error("更新用户等级配置状态失败", zap.Error(err))
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, nil)
}
// GetAll 获取所有用户等级配置
func (h *UserLevelConfigHandler) GetAll(c *gin.Context) {
configs, err := h.service.GetAll()
if err != nil {
h.logger.Error("获取所有用户等级配置失败", zap.Error(err))
h.response.InternalServerError(c, "获取配置失败")
return
}
h.response.Success(c, configs)
}

View File

@@ -0,0 +1,97 @@
package handlers
import (
"github.com/gin-gonic/gin"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
"strconv"
)
type UserProjectQuotaHandler struct {
svc services.UserProjectQuotaService
resp *utils.Response
}
func NewUserProjectQuotaHandler(s services.UserProjectQuotaService) *UserProjectQuotaHandler {
return &UserProjectQuotaHandler{svc: s, resp: utils.NewResponse()}
}
func (h *UserProjectQuotaHandler) Create(c *gin.Context) {
var req models.UserProjectQuota
if err := c.ShouldBindJSON(&req); err != nil {
h.resp.ValidateError(c, err)
return
}
out, err := h.svc.Create(&req)
if err != nil {
h.resp.BadRequest(c, err.Error())
return
}
h.resp.Success(c, out)
}
func (h *UserProjectQuotaHandler) Update(c *gin.Context) {
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req models.UserProjectQuota
if err := c.ShouldBindJSON(&req); err != nil {
h.resp.ValidateError(c, err)
return
}
out, err := h.svc.Update(uint(id64), &req)
if err != nil {
h.resp.BadRequest(c, err.Error())
return
}
h.resp.Success(c, out)
}
func (h *UserProjectQuotaHandler) Delete(c *gin.Context) {
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.Delete(uint(id64)); err != nil {
h.resp.BadRequest(c, err.Error())
return
}
h.resp.Success(c, gin.H{"message": "deleted"})
}
func (h *UserProjectQuotaHandler) GetByID(c *gin.Context) {
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
out, err := h.svc.GetByID(uint(id64))
if err != nil {
h.resp.NotFound(c, "not found")
return
}
h.resp.Success(c, out)
}
func (h *UserProjectQuotaHandler) List(c *gin.Context) {
var filter storage.UserProjectQuotaFilter
filter.UserID = c.Query("user_id")
if v := c.Query("enabled"); v != "" {
if v == "true" {
b := true
filter.Enabled = &b
} else if v == "false" {
b := false
filter.Enabled = &b
}
}
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil {
filter.Page = v
}
}
if s := c.Query("size"); s != "" {
if v, err := strconv.Atoi(s); err == nil {
filter.Size = v
}
}
items, total, err := h.svc.List(filter)
if err != nil {
h.resp.InternalServerError(c, err.Error())
return
}
h.resp.Page(c, items, total, filter.Page, filter.Size)
}

View File

@@ -0,0 +1,170 @@
package handlers
import (
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// VendorModelPricingHandler 供应商模型价格配置处理器
type VendorModelPricingHandler struct {
db *gorm.DB
response *utils.Response
}
// NewVendorModelPricingHandler 创建处理器
func NewVendorModelPricingHandler(db *gorm.DB) *VendorModelPricingHandler {
return &VendorModelPricingHandler{
db: db,
response: utils.NewResponse(),
}
}
// VendorModelPricingResponse 供应商模型价格配置响应
type VendorModelPricingResponse struct {
ID uint `json:"id"`
Provider string `json:"provider"`
Account string `json:"account"`
ModelName string `json:"model_name"`
InputPrice float64 `json:"input_price"`
OutputPrice float64 `json:"output_price"`
CacheReadPrice float64 `json:"cache_read_price"`
CacheCreatePrice float64 `json:"cache_create_price"`
PriceRatio float64 `json:"price_ratio"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// PriceUpdateRequest 价格更新请求
type PriceUpdateRequest struct {
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
CacheCreatePrice *float64 `json:"cache_create_price"`
Enabled *bool `json:"enabled"`
PriceRatio *float64 `json:"price_ratio"`
}
// GetVendorModelPricing 获取供应商模型价格配置列表
func (h *VendorModelPricingHandler) GetVendorModelPricing(c *gin.Context) {
var models []VendorModelPricingResponse
// 构建查询条件
// 使用 v2 表gw_model_config_v2直接返回数据无需联查
// 为兼容前端字段类型account 字段返回空串
query := h.db.Table("gw_model_config_v2 mc").
Select("mc.id, mc.provider, '' as account, mc.model_name, " +
"mc.prompt_price as input_price, " +
"mc.output_price as output_price, " +
"mc.cache_read_price, mc.cache_create_price, " +
"mc.price_ratio, " +
"mc.enabled, mc.created_at, mc.updated_at")
// 添加筛选条件
if provider := c.Query("provider"); provider != "" {
query = query.Where("mc.provider = ?", provider)
}
if model := c.Query("model"); model != "" {
query = query.Where("mc.model_name LIKE ?", "%"+model+"%")
}
if status := c.Query("status"); status != "" {
if status == "enabled" {
query = query.Where("mc.enabled = ?", true)
} else if status == "disabled" {
query = query.Where("mc.enabled = ?", false)
}
}
// 分页
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
offset := (page - 1) * size
// 先获取总数(在应用分页之前)
var total int64
err := query.Count(&total).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
// 然后获取分页数据
err = query.Offset(offset).Limit(size).Find(&models).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, models, total, page, size)
}
// UpdateModelPricing 更新单个模型价格
func (h *VendorModelPricingHandler) UpdateModelPricing(c *gin.Context) {
id := c.Param("id")
var req PriceUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
// 构建更新字段v2 列)
updates := make(map[string]interface{})
if req.InputPrice != nil {
updates["prompt_price"] = *req.InputPrice
}
if req.OutputPrice != nil {
updates["output_price"] = *req.OutputPrice
}
if req.CacheReadPrice != nil {
updates["cache_read_price"] = *req.CacheReadPrice
}
if req.CacheCreatePrice != nil {
updates["cache_create_price"] = *req.CacheCreatePrice
}
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if req.PriceRatio != nil {
updates["price_ratio"] = *req.PriceRatio
}
// 检查模型是否存在v2 表)
var count int64
err := h.db.Table("gw_model_config_v2").Where("id = ?", id).Count(&count).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if count == 0 {
h.response.NotFound(c, "模型不存在")
return
}
// 更新模型价格v2 表)
err = h.db.Table("gw_model_config_v2").Where("id = ?", id).Updates(updates).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "价格更新成功")
}
// GetProviders 获取供应商列表(用于筛选)
func (h *VendorModelPricingHandler) GetProviders(c *gin.Context) {
var providers []string
err := h.db.Table("gw_providers").
Select("DISTINCT name").
Where("status = ?", "active").
Find(&providers).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, providers)
}

View File

@@ -0,0 +1,403 @@
package handlers
import (
"errors"
"goalfymax-admin/pkg/utils"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// VmPricingHandler 虚拟机价格配置处理器
type VmPricingHandler struct {
db *gorm.DB
response *utils.Response
}
// NewVmPricingHandler 创建处理器
func NewVmPricingHandler(db *gorm.DB) *VmPricingHandler {
return &VmPricingHandler{
db: db,
response: utils.NewResponse(),
}
}
// VmSpecResponse 虚拟机规格响应
type VmSpecResponse struct {
ID uint `json:"id"`
SpecType string `json:"spec_type"`
CPUCores int `json:"cpu_cores"`
MemoryGB int `json:"memory_gb"`
Description *string `json:"description"`
CostPricePerMinute float64 `json:"cost_price_per_minute"`
MarkupRate float64 `json:"markup_rate"`
IsActive bool `json:"is_active"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// VmTemplateResponse 虚拟机模板响应
type VmTemplateResponse struct {
ID uint `json:"id"`
SpecType string `json:"spec_type"`
TemplateID string `json:"template_id"`
IsDefault bool `json:"is_default"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// VmSpecCreateRequest 规格创建请求
type VmSpecCreateRequest struct {
SpecType string `json:"spec_type" binding:"required"`
CPUCores int `json:"cpu_cores" binding:"required"`
MemoryGB int `json:"memory_gb" binding:"required"`
Description *string `json:"description"`
CostPricePerMinute float64 `json:"cost_price_per_minute" binding:"required"`
MarkupRate *float64 `json:"markup_rate"`
IsActive *bool `json:"is_active"`
}
// VmSpecUpdateRequest 规格更新请求
type VmSpecUpdateRequest struct {
CostPricePerMinute *float64 `json:"cost_price_per_minute"`
MarkupRate *float64 `json:"markup_rate"`
IsActive *bool `json:"is_active"`
}
// VmTemplateCreateRequest 模板创建请求
type VmTemplateCreateRequest struct {
SpecType string `json:"spec_type" binding:"required"`
TemplateID string `json:"template_id" binding:"required"`
IsDefault bool `json:"is_default"`
}
// VmTemplateUpdateRequest 模板更新请求
type VmTemplateUpdateRequest struct {
IsDefault *bool `json:"is_default"`
}
// GetVmSpecs 获取虚拟机规格列表
func (h *VmPricingHandler) GetVmSpecs(c *gin.Context) {
var specs []VmSpecResponse
// 构建查询条件
query := h.db.Table("sb_sandbox_specs")
// 添加筛选条件
if specType := c.Query("spec_type"); specType != "" {
query = query.Where("spec_type LIKE ?", "%"+specType+"%")
}
if status := c.Query("status"); status != "" {
if status == "active" {
query = query.Where("is_active = ?", true)
} else if status == "inactive" {
query = query.Where("is_active = ?", false)
}
}
// 分页
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
offset := (page - 1) * size
// 先获取总数
var total int64
err := query.Count(&total).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
// 然后获取分页数据
err = query.Order("created_at DESC").Offset(offset).Limit(size).Find(&specs).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, specs, total, page, size)
}
// UpdateVmSpec 更新虚拟机规格
func (h *VmPricingHandler) UpdateVmSpec(c *gin.Context) {
id := c.Param("id")
var req VmSpecUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
// 构建更新字段
updates := make(map[string]interface{})
if req.CostPricePerMinute != nil {
updates["cost_price_per_minute"] = *req.CostPricePerMinute
}
if req.MarkupRate != nil {
updates["markup_rate"] = *req.MarkupRate
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) == 0 {
h.response.BadRequest(c, "没有需要更新的字段")
return
}
// 检查规格是否存在
var count int64
err := h.db.Table("sb_sandbox_specs").Where("id = ?", id).Count(&count).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if count == 0 {
h.response.NotFound(c, "规格不存在")
return
}
// 更新规格
err = h.db.Table("sb_sandbox_specs").Where("id = ?", id).Updates(updates).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "规格更新成功")
}
// DeleteVmSpec 删除虚拟机规格
func (h *VmPricingHandler) DeleteVmSpec(c *gin.Context) {
id := c.Param("id")
// 检查规格是否存在
var count int64
err := h.db.Table("sb_sandbox_specs").Where("id = ?", id).Count(&count).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if count == 0 {
h.response.NotFound(c, "规格不存在")
return
}
// 删除规格
err = h.db.Table("sb_sandbox_specs").Where("id = ?", id).Delete(nil).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "规格删除成功")
}
// CreateVmSpec 创建虚拟机规格
func (h *VmPricingHandler) CreateVmSpec(c *gin.Context) {
var req VmSpecCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
// 检查规格类型是否已存在
var count int64
err := h.db.Table("sb_sandbox_specs").Where("spec_type = ?", req.SpecType).Count(&count).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if count > 0 {
h.response.BadRequest(c, "该配置类型已存在")
return
}
// 设置默认值
markupRate := 0.3000 // 默认30%
if req.MarkupRate != nil {
markupRate = *req.MarkupRate
}
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
// 创建规格
insertData := map[string]interface{}{
"spec_type": req.SpecType,
"cpu_cores": req.CPUCores,
"memory_gb": req.MemoryGB,
"cost_price_per_minute": req.CostPricePerMinute,
"markup_rate": markupRate,
"is_active": isActive,
}
if req.Description != nil {
insertData["description"] = *req.Description
}
err = h.db.Table("sb_sandbox_specs").Create(insertData).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "规格创建成功")
}
// GetVmTemplates 获取虚拟机模板列表
func (h *VmPricingHandler) GetVmTemplates(c *gin.Context) {
var templates []VmTemplateResponse
// 构建查询条件
query := h.db.Table("sb_sandbox_templates")
// 添加筛选条件
if specType := c.Query("spec_type"); specType != "" {
query = query.Where("spec_type = ?", specType)
}
// 分页
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
offset := (page - 1) * size
// 先获取总数
var total int64
err := query.Count(&total).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
// 然后获取分页数据
err = query.Order("spec_type ASC, created_at DESC").Offset(offset).Limit(size).Find(&templates).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Page(c, templates, total, page, size)
}
// CreateVmTemplate 创建虚拟机模板
func (h *VmPricingHandler) CreateVmTemplate(c *gin.Context) {
var req VmTemplateCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.response.BadRequest(c, err.Error())
return
}
// 检查规格是否存在
var specCount int64
err := h.db.Table("sb_sandbox_specs").Where("spec_type = ?", req.SpecType).Count(&specCount).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if specCount == 0 {
h.response.BadRequest(c, "规格类型不存在")
return
}
// 如果设置为默认模板,需要先取消同规格的其他默认模板
if req.IsDefault {
err = h.db.Table("sb_sandbox_templates").
Where("spec_type = ? AND is_default = ?", req.SpecType, true).
Update("is_default", false).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
}
// 检查是否已存在相同的规格-模板组合
var existingCount int64
err = h.db.Table("sb_sandbox_templates").
Where("spec_type = ? AND template_id = ?", req.SpecType, req.TemplateID).
Count(&existingCount).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if existingCount > 0 {
h.response.BadRequest(c, "该规格和模板的组合已存在")
return
}
// 创建模板
insertData := map[string]interface{}{
"spec_type": req.SpecType,
"template_id": req.TemplateID,
"is_default": req.IsDefault,
}
err = h.db.Table("sb_sandbox_templates").Create(insertData).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "模板创建成功")
}
// DeleteVmTemplate 删除虚拟机模板
func (h *VmPricingHandler) DeleteVmTemplate(c *gin.Context) {
id := c.Param("id")
// 检查模板是否存在
var count int64
err := h.db.Table("sb_sandbox_templates").Where("id = ?", id).Count(&count).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
if count == 0 {
h.response.NotFound(c, "模板不存在")
return
}
// 删除模板
err = h.db.Table("sb_sandbox_templates").Where("id = ?", id).Delete(nil).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "模板删除成功")
}
// SetDefaultVmTemplate 设置默认模板
func (h *VmPricingHandler) SetDefaultVmTemplate(c *gin.Context) {
id := c.Param("id")
// 获取模板信息
var template VmTemplateResponse
err := h.db.Table("sb_sandbox_templates").Where("id = ?", id).First(&template).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
h.response.NotFound(c, "模板不存在")
return
}
h.response.InternalServerError(c, err.Error())
return
}
// 取消同规格的其他默认模板
err = h.db.Table("sb_sandbox_templates").
Where("spec_type = ? AND is_default = ? AND id != ?", template.SpecType, true, id).
Update("is_default", false).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
// 设置当前模板为默认
err = h.db.Table("sb_sandbox_templates").Where("id = ?", id).Update("is_default", true).Error
if err != nil {
h.response.InternalServerError(c, err.Error())
return
}
h.response.Success(c, "默认模板设置成功")
}

View File

@@ -0,0 +1,141 @@
package middlewares
import (
"bytes"
"io"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/pkg/utils"
)
// API日志中间件 - 记录所有接口的调用信息
func APILogMiddleware(logger *utils.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// 排除健康检查接口
if c.Request.URL.Path == "/health" {
c.Next()
return
}
// 记录开始时间
startTime := time.Now()
// 读取请求体(需要保存以便后续使用)
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
// 获取用户信息
userID := 0
userEmail := "unknown"
if userInfo, exists := c.Get("user"); exists {
if user, ok := userInfo.(*models.UserInfo); ok && user != nil {
if user.Email != "" {
userEmail = user.Email
} else if user.PreferredUsername != "" {
userEmail = user.PreferredUsername + "@goalfy.com"
}
// 尝试获取用户ID
if id, exists := c.Get("user_id"); exists {
switch v := id.(type) {
case int:
userID = v
case uint:
userID = int(v)
}
}
}
}
// 创建响应写入器包装器以捕获响应
responseWriter := &responseBodyWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = responseWriter
// 处理请求
c.Next()
// 计算耗时
duration := time.Since(startTime)
// 获取响应信息
responseCode := c.Writer.Status()
responseBody := responseWriter.body.String()
// 限制响应体大小(避免打印过大的响应)
if len(responseBody) > 1000 {
responseBody = responseBody[:1000] + "...(truncated)"
}
// 限制请求体大小
requestBodyStr := string(requestBody)
if len(requestBodyStr) > 1000 {
requestBodyStr = requestBodyStr[:1000] + "...(truncated)"
}
// 获取请求信息
method := c.Request.Method
path := c.Request.URL.Path
// 提取模块名称
module := extractModule(path)
// 打印日志
logger.Info("📝 [API日志] 接口调用记录",
zap.String("method", method),
zap.String("path", path),
zap.String("full_path", c.Request.URL.String()),
zap.String("module", module),
zap.Int("user_id", userID),
zap.String("user_email", userEmail),
zap.String("ip_address", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
zap.String("request_body", requestBodyStr),
zap.Int("response_code", responseCode),
zap.String("response_body", responseBody),
zap.Duration("duration", duration),
zap.Int64("duration_ms", duration.Milliseconds()),
zap.String("status", getStatus(responseCode)),
)
}
}
// responseBodyWriter 用于捕获响应体
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *responseBodyWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// extractModule 从路径提取模块名称
func extractModule(path string) string {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) >= 3 {
return parts[2] // /api/admin/{module}
}
return "unknown"
}
// getStatus 根据响应码获取状态
func getStatus(code int) string {
if code >= 200 && code < 300 {
return "success"
} else if code >= 400 && code < 500 {
return "client_error"
} else if code >= 500 {
return "server_error"
}
return "unknown"
}

View File

@@ -0,0 +1,47 @@
package middlewares
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"goalfymax-admin/pkg/utils"
)
// LoggingMiddleware 日志中间件
func LoggingMiddleware(logger *utils.Logger) gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// 记录请求日志
logger.Info("HTTP请求",
zap.String("method", param.Method),
zap.String("path", param.Path),
zap.Int("status", param.StatusCode),
zap.Duration("latency", param.Latency),
zap.String("client_ip", param.ClientIP),
zap.String("user_agent", param.Request.UserAgent()),
)
return ""
})
}
// RequestLogMiddleware 请求日志中间件
func RequestLogMiddleware(logger *utils.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 处理请求
c.Next()
// 记录请求信息
latency := time.Since(start)
logger.Info("请求处理完成",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", latency),
zap.String("client_ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
)
}
}

View File

@@ -0,0 +1,370 @@
package routes
import (
"goalfymax-admin/internal/api/handlers"
"goalfymax-admin/internal/api/middlewares"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/services"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/middleware"
"goalfymax-admin/pkg/redis"
"goalfymax-admin/pkg/utils"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// SetupRoutes 设置路由
func SetupRoutes(
userService services.UserService,
roleService services.RoleService,
pageService services.PageService,
quotaService services.QuotaService,
ssoService services.SSOService,
rbacService services.RBACService,
userLevelConfigService services.UserLevelConfigService,
systemConfigService services.SystemConfigService,
redisClient *redis.Client,
logger *utils.Logger,
appConfig *config.Config,
) *gin.Engine {
// 创建Gin引擎
r := gin.New()
// 添加CORS中间件
r.Use(cors.New(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"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 添加中间件
r.Use(middlewares.RequestLogMiddleware(logger))
r.Use(middlewares.APILogMiddleware(logger)) // API日志中间件记录数据修改接口
r.Use(gin.Recovery())
// 创建SSO客户端和认证中间件
ssoConfig := &models.SSOConfig{
SSOServerURL: appConfig.SSO.SSOServerURL,
ClientID: appConfig.SSO.ClientID,
ClientSecret: appConfig.SSO.ClientSecret,
RedirectURI: appConfig.SSO.RedirectURI,
Scope: appConfig.SSO.Scope,
ResourceAud: appConfig.SSO.ResourceAud,
Timeout: appConfig.SSO.Timeout,
}
ssoClient := middleware.NewSSOClient(ssoConfig, logger)
sessionManager := middleware.NewMemorySessionManager()
authMiddleware := middleware.NewAuthMiddleware(ssoClient, sessionManager, "/login")
// RBAC中间件已简化不再需要全局实例
// 创建处理器
quotaHandler := handlers.NewQuotaHandler(quotaService)
userProjectQuotaHandler := handlers.NewUserProjectQuotaHandler(
services.NewUserProjectQuotaService(
storage.NewUserProjectQuotaStorage(),
),
)
ssoHandler := handlers.NewSSOHandler(ssoService, logger)
messagePushService := services.NewMessagePushService()
ssoAdminService := services.NewSSOAdminService()
userHandler := handlers.NewUserHandler(userService, rbacService, logger)
auditLogService := services.NewAuditLogService(storage.NewAuditLogStorage())
goalfyUserHandler := handlers.NewGoalfyMaxUserHandler(
services.NewGoalfyMaxUserService(storage.NewGoalfyMaxUserStorage(), messagePushService, ssoAdminService, redisClient, storage.NewBalanceOperationLogStorage(), auditLogService, logger),
)
auditLogHandler := handlers.NewAuditLogHandler(auditLogService)
userFeedbackHandler := handlers.NewUserFeedbackHandler(
services.NewUserFeedbackService(storage.NewUserFeedbackStorage()),
)
messagePushHandler := handlers.NewMessagePushHandler(
messagePushService,
)
roleHandler := handlers.NewRoleHandler(roleService, rbacService, logger)
pageHandler := handlers.NewPageHandler(pageService, logger)
rbacHandler := handlers.NewRBACHandler(rbacService, logger)
vendorPricingHandler := handlers.NewVendorModelPricingHandler(storage.GetDB())
vmPricingHandler := handlers.NewVmPricingHandler(storage.GetDB())
mcpProviderHandler := handlers.NewMCPProviderHandler()
financeHandler := handlers.NewFinanceHandler()
userLevelConfigHandler := handlers.NewUserLevelConfigHandler(userLevelConfigService, logger)
systemConfigHandler := handlers.NewSystemConfigHandler(systemConfigService, logger)
inviteCodeHandler := handlers.NewInviteCodeHandler(
services.NewInviteCodeService(storage.NewInviteCodeStorage()),
userLevelConfigService,
)
inviteCodeApplicationHandler := handlers.NewInviteCodeApplicationHandler(storage.GetDB())
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API路由组
api := r.Group("/api")
{
// 公开接口(不需要认证)
public := api.Group("/public")
{
// 官网提交邀请码申请
public.POST("/invite-code/apply", inviteCodeApplicationHandler.SubmitApplication)
}
// SSO相关路由
sso := api.Group("/sso")
{
sso.POST("/login", ssoHandler.HandleSSOLogin) // SSO登录
sso.POST("/callback", ssoHandler.HandleSSOCallback) // SSO回调
sso.POST("/refresh", ssoHandler.HandleRefreshToken) // 刷新令牌
sso.POST("/logout", ssoHandler.HandleLogout) // 登出
sso.GET("/userinfo", ssoHandler.HandleUserInfo) // 获取用户信息
sso.GET("/online-users", ssoHandler.GetOnlineUsers) // 获取在线用户列表
sso.GET("/online-count", ssoHandler.GetOnlineUserCount) // 获取在线用户数量
sso.POST("/batch-logout", ssoHandler.BatchLogout) // 批量登出
}
// 管理员路由组(需要认证和动态权限检查)
admin := api.Group("/admin")
admin.Use(authMiddleware.RequireAuth())
// 使用简化的页面权限检查
{
// 用户管理 - 所有路由通过动态权限检查
users := admin.Group("/users")
{
users.GET("", userHandler.List) // 获取用户列表
users.POST("", userHandler.Create) // 创建用户
users.GET("/:id", userHandler.GetByID) // 获取用户详情
users.PUT("/:id", userHandler.Update) // 更新用户
users.DELETE("/:id", userHandler.Delete) // 删除用户
users.PUT("/:id/status", userHandler.UpdateStatus) // 更新用户状态
users.PUT("/:id/roles", userHandler.UpdateRoles) // 更新用户角色
users.GET("/:id/roles", userHandler.GetUserRoles) // 获取用户角色
users.GET("/:id/permissions", userHandler.GetUserPermissions) // 获取用户权限
users.GET("/check-role/:user_id", userHandler.CheckUserRole) // 检查用户系统角色
users.POST("/change-system-role", userHandler.ChangeUserSystemRole) // 变更用户系统角色
}
// GoalfyMax 用户管理
goalfyUsers := admin.Group("/goalfymax-users")
{
goalfyUsers.GET("", goalfyUserHandler.List) // 列表
goalfyUsers.POST("", goalfyUserHandler.Create) // 新增
goalfyUsers.GET(":id", goalfyUserHandler.GetByID) // 详情
goalfyUsers.PUT(":id", goalfyUserHandler.Update) // 编辑
goalfyUsers.DELETE(":id", goalfyUserHandler.Delete) // 删除
goalfyUsers.POST(":id/ban", goalfyUserHandler.Ban) // 封禁
goalfyUsers.POST(":id/unban", goalfyUserHandler.Unban) // 解封
goalfyUsers.POST(":id/add-balance", goalfyUserHandler.AddBalance) // 增加余额
goalfyUsers.POST(":id/deduct-balance", goalfyUserHandler.DeductBalance) // 减少余额
}
// 用户反馈管理
userFeedbacks := admin.Group("/user-feedback")
{
userFeedbacks.GET("", userFeedbackHandler.List) // 获取反馈列表
userFeedbacks.GET("/:id", userFeedbackHandler.GetByID) // 获取反馈详情
userFeedbacks.POST("/:id/mark-handled", userFeedbackHandler.MarkHandled) // 标记为已处理
userFeedbacks.DELETE("/:id", userFeedbackHandler.Delete) // 删除反馈
userFeedbacks.GET("/statistics", userFeedbackHandler.GetStatistics) // 获取统计信息
}
// 消息推送管理
messagePush := admin.Group("/message-push")
{
messagePush.POST("/send", messagePushHandler.SendMessage) // 发送消息
messagePush.GET("/logs", messagePushHandler.GetPushLogs) // 获取推送记录
messagePush.GET("/logs/:id", messagePushHandler.GetPushLogByID) // 获取推送记录详情
messagePush.GET("/users/search", messagePushHandler.SearchUsers) // 搜索用户
}
// 角色管理 - 所有路由通过动态权限检查
roles := admin.Group("/roles")
{
roles.GET("", roleHandler.List) // 获取角色列表
roles.POST("", roleHandler.Create) // 创建角色
roles.GET("/:id", roleHandler.GetByID) // 获取角色详情
roles.PUT("/:id", roleHandler.Update) // 更新角色
roles.DELETE("/:id", roleHandler.Delete) // 删除角色
roles.PUT("/:id/status", roleHandler.UpdateStatus) // 更新角色状态
roles.PUT("/:id/permissions", roleHandler.UpdatePermissions) // 更新角色权限
roles.GET("/:id/permissions", roleHandler.GetRolePermissions) // 获取角色权限
}
// 页面管理 - 所有路由通过页面权限检查
pages := admin.Group("/pages")
{
pages.GET("", pageHandler.List) // 获取页面列表
pages.POST("", pageHandler.Create) // 创建页面
pages.GET("/:id", pageHandler.GetByID) // 获取页面详情
pages.PUT("/:id", pageHandler.Update) // 更新页面
pages.DELETE("/:id", pageHandler.Delete) // 删除页面
}
// RBAC管理 - 所有路由通过动态权限检查
rbac := admin.Group("/rbac")
{
rbac.POST("/role-page-permissions", rbacHandler.AssignRolePagePermissions) // 分配角色页面权限
rbac.DELETE("/roles/:id/page-permissions", rbacHandler.RemoveRolePagePermissions) // 移除角色页面权限
rbac.GET("/roles/:id/page-permissions", rbacHandler.GetRolePagePermissions) // 获取角色页面权限
rbac.GET("/users/:id/permissions", rbacHandler.GetUserPermissions) // 获取用户权限
rbac.GET("/roles/:id/permissions", rbacHandler.GetRolePermissions) // 获取角色权限
rbac.GET("/check-page-permission", rbacHandler.CheckPagePermission) // 检查页面权限
rbac.GET("/users/:id/accessible-pages", rbacHandler.GetUserAccessiblePages) // 获取用户可访问页面
}
// 供应商模型价格配置
vendorPricing := admin.Group("/vendor-model-pricing")
{
vendorPricing.GET("", vendorPricingHandler.GetVendorModelPricing) // 获取价格配置列表
vendorPricing.PUT("/:id", vendorPricingHandler.UpdateModelPricing) // 更新模型价格
vendorPricing.GET("/providers", vendorPricingHandler.GetProviders) // 获取供应商列表
}
// 虚拟机价格配置
vmPricing := admin.Group("/vm-pricing")
{
vmPricing.GET("/specs", vmPricingHandler.GetVmSpecs) // 获取规格列表
vmPricing.POST("/specs", vmPricingHandler.CreateVmSpec) // 创建规格
vmPricing.PUT("/specs/:id", vmPricingHandler.UpdateVmSpec) // 更新规格价格
vmPricing.DELETE("/specs/:id", vmPricingHandler.DeleteVmSpec) // 删除规格
vmPricing.GET("/templates", vmPricingHandler.GetVmTemplates) // 获取模板列表
vmPricing.POST("/templates", vmPricingHandler.CreateVmTemplate) // 创建模板
vmPricing.DELETE("/templates/:id", vmPricingHandler.DeleteVmTemplate) // 删除模板
vmPricing.PUT("/templates/:id/default", vmPricingHandler.SetDefaultVmTemplate) // 设置默认模板
}
// MCP 价格配置PostgreSQL
mcpProviders := admin.Group("/mcp-providers")
{
mcpProviders.GET("", mcpProviderHandler.List)
mcpProviders.POST("", mcpProviderHandler.Create)
mcpProviders.GET(":id", mcpProviderHandler.GetByID)
mcpProviders.PUT(":id", mcpProviderHandler.Update)
mcpProviders.DELETE(":id", mcpProviderHandler.Delete)
mcpProviders.PATCH(":id/status", mcpProviderHandler.UpdateStatus)
mcpProviders.PATCH(":id/is-used", mcpProviderHandler.UpdateIsUsed)
}
// 用户等级配置管理
userLevelConfigs := admin.Group("/user-level-configs")
{
userLevelConfigs.GET("", userLevelConfigHandler.List) // 获取列表
userLevelConfigs.GET("/all", userLevelConfigHandler.GetAll) // 获取所有(不分页)
userLevelConfigs.POST("", userLevelConfigHandler.Create) // 创建
userLevelConfigs.GET("/:id", userLevelConfigHandler.GetByID) // 获取详情
userLevelConfigs.PUT("/:id", userLevelConfigHandler.Update) // 更新
userLevelConfigs.DELETE("/:id", userLevelConfigHandler.Delete) // 删除
userLevelConfigs.PUT("/:id/status", userLevelConfigHandler.UpdateStatus) // 更新状态
}
// 系统通用配置管理
systemConfigs := admin.Group("/system-configs")
{
systemConfigs.GET("", systemConfigHandler.List) // 获取列表
systemConfigs.GET("/all", systemConfigHandler.GetAll) // 获取所有(不分页)
systemConfigs.POST("", systemConfigHandler.Create) // 创建
systemConfigs.GET("/key/:key", systemConfigHandler.GetByKey) // 根据Key获取
systemConfigs.GET("/:id", systemConfigHandler.GetByID) // 获取详情
systemConfigs.PUT("/:id", systemConfigHandler.Update) // 更新
systemConfigs.DELETE("/:id", systemConfigHandler.Delete) // 删除
systemConfigs.PUT("/:id/status", systemConfigHandler.UpdateStatus) // 更新状态
}
// 邀请码管理(简化版)
inviteCodes := admin.Group("/invite-codes")
{
inviteCodes.GET("", inviteCodeHandler.GetInviteCodeList) // 获取邀请码列表
inviteCodes.POST("", inviteCodeHandler.CreateInviteCode) // 创建邀请码(支持设置过期时间)
inviteCodes.GET("/client-options", inviteCodeHandler.GetClientOptions) // 获取客户端选项
inviteCodes.GET("/statistics", inviteCodeHandler.GetInviteCodeStatistics) // 获取统计信息
inviteCodes.GET("/:id", inviteCodeHandler.GetInviteCodeDetail) // 获取邀请码详情
inviteCodes.PUT("/:id", inviteCodeHandler.UpdateInviteCode) // 更新邀请码(支持更新过期时间)
inviteCodes.DELETE("/:id", inviteCodeHandler.DeleteInviteCode) // 删除邀请码
inviteCodes.POST("/mark-used", inviteCodeHandler.MarkInviteCodeAsUsed) // 标记邀请码为已使用
inviteCodes.POST("/validate", inviteCodeHandler.ValidateInviteCode) // 验证邀请码是否有效
}
// 邀请码申请管理
inviteApplications := admin.Group("/invite-applications")
{
inviteApplications.GET("", inviteCodeApplicationHandler.GetApplicationList) // 获取申请列表
inviteApplications.GET("/statistics", inviteCodeApplicationHandler.GetStatistics) // 获取统计信息
inviteApplications.GET("/pending-count", inviteCodeApplicationHandler.GetPendingCount) // 获取待处理数量
inviteApplications.POST("/approve", inviteCodeApplicationHandler.ApproveApplication) // 审批通过申请
inviteApplications.POST("/reject", inviteCodeApplicationHandler.RejectApplication) // 审批拒绝申请
inviteApplications.POST("/batch-approve", inviteCodeApplicationHandler.BatchApproveApplications) // 批量审批通过
inviteApplications.POST("/batch-reject", inviteCodeApplicationHandler.BatchRejectApplications) // 批量审批拒绝
}
// 审计日志管理
auditLogs := admin.Group("/audit-logs")
{
auditLogs.GET("", auditLogHandler.List) // 获取审计日志列表
auditLogs.GET("/:id", auditLogHandler.GetByID) // 获取审计日志详情
}
}
// 财务数据(需要认证)
finance := api.Group("/finance")
finance.Use(authMiddleware.RequireAuth())
{
finance.GET("/sandbox-records", financeHandler.ListSandboxRecords)
finance.GET("/token-usages", financeHandler.ListTokenUsages)
finance.GET("/mcp-usages", financeHandler.ListMCPUsages)
finance.GET("/transaction-logs", financeHandler.ListTransactionLogs)
finance.GET("/payment-records", financeHandler.ListPaymentRecords)
finance.POST("/payment-records/refund", financeHandler.RefundPaymentRecord)
finance.GET("/mcp-account-recharge-records", financeHandler.ListMcpAccountRechargeRecords)
finance.POST("/mcp-account-recharge-records", financeHandler.CreateMcpAccountRechargeRecord)
finance.PUT("/mcp-account-recharge-records/:id", financeHandler.UpdateMcpAccountRechargeRecord)
finance.DELETE("/mcp-account-recharge-records/:id", financeHandler.DeleteMcpAccountRechargeRecord)
finance.GET("/mcp-provider-accounts", financeHandler.GetMcpProviderAccounts)
finance.GET("/mcp-account-balances", financeHandler.GetMcpAccountBalances)
finance.POST("/mcp-account-balances", financeHandler.CreateMcpAccountBalance)
finance.PUT("/mcp-account-balances/:provider_id", financeHandler.AdjustMcpAccountBalance)
finance.GET("/mcp-account-balances/:provider_id/history", financeHandler.GetMcpAccountBalanceHistory)
// 模型账号管理
finance.GET("/model-account-recharge-records", financeHandler.ListModelAccountRechargeRecords)
finance.POST("/model-account-recharge-records", financeHandler.CreateModelAccountRechargeRecord)
finance.PUT("/model-account-recharge-records/:id", financeHandler.UpdateModelAccountRechargeRecord)
finance.DELETE("/model-account-recharge-records/:id", financeHandler.DeleteModelAccountRechargeRecord)
finance.GET("/model-config-accounts", financeHandler.GetModelConfigAccounts)
finance.GET("/model-account-balances", financeHandler.GetModelAccountBalances)
finance.POST("/model-account-balances", financeHandler.CreateModelAccountBalance)
finance.PUT("/model-account-balances/:account", financeHandler.AdjustModelAccountBalance)
finance.GET("/model-account-balances/:account/history", financeHandler.GetModelAccountBalanceHistory)
}
// 配额相关路由(需要认证和动态权限检查)
quotas := api.Group("/quotas")
quotas.Use(authMiddleware.RequireAuth())
// 使用简化的页面权限检查
{
quotas.POST("/history", quotaHandler.GetQuotaHistory) // 获取配额历史
quotas.GET("/health", quotaHandler.HealthCheck) // 配额服务健康检查
quotas.GET("/rules", quotaHandler.GetQuotaRules) // 获取配额规则列表(转发网关)
quotas.POST("/rules", quotaHandler.CreateQuotaRule) // 创建规则(转发网关)
quotas.PUT("/rules/:id", quotaHandler.UpdateQuotaRule) // 更新规则(转发网关)
quotas.DELETE("/rules/:id", quotaHandler.DeleteQuotaRule) // 删除规则(转发网关)
// 用户项目配额 CRUD
userProject := quotas.Group("/user-project")
{
userProject.GET("", userProjectQuotaHandler.List)
userProject.POST("", userProjectQuotaHandler.Create)
userProject.GET(":id", userProjectQuotaHandler.GetByID)
userProject.PUT(":id", userProjectQuotaHandler.Update)
userProject.DELETE(":id", userProjectQuotaHandler.Delete)
}
}
}
return r
}

47
internal/config/README.md Normal file
View File

@@ -0,0 +1,47 @@
# 配置管理
本模块负责管理应用程序的配置,支持从 YAML 文件加载配置。
## 功能特性
- 支持 YAML 配置文件
- 配置热加载
- 默认值设置
- 类型安全的配置结构
## 配置结构
```yaml
server:
addr: "0.0.0.0"
port: 8080
database:
dsn: "user:password@tcp(localhost:3306)/goalfymax_admin?charset=utf8mb4&parseTime=True&loc=Local"
maxIdleConns: 10
maxOpenConns: 100
admin:
loginKey: "your-admin-password"
jwtSecret: "your-jwt-secret"
log:
level: "info"
format: "json"
output: "stdout"
```
## 使用方法
```go
// 加载配置
err := config.LoadConfig("etc/config.yaml")
if err != nil {
log.Fatal(err)
}
// 获取配置
cfg := config.GetConfig()
fmt.Println(cfg.Server.Addr)
```

322
internal/config/config.go Normal file
View File

@@ -0,0 +1,322 @@
package config
import (
"fmt"
"sync"
"time"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
)
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
MaxIdleConns int `mapstructure:"maxIdleConns"`
MaxOpenConns int `mapstructure:"maxOpenConns"`
LogLevel string `mapstructure:"logLevel"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Addr string `mapstructure:"addr"`
Port int `mapstructure:"port"`
}
// GatewayConfig 网关配置
type GatewayConfig struct {
BaseURL string `mapstructure:"base_url"`
Timeout int `mapstructure:"timeout"`
Auth GatewayAuthConfig `mapstructure:"auth"`
}
// GatewayAuthConfig 网关鉴权配置
type GatewayAuthConfig struct {
LoginURL string `mapstructure:"login_url"`
Key string `mapstructure:"key"`
}
// SSOConfig SSO配置
type SSOConfig struct {
SSOServerURL string `mapstructure:"sso_server_url"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
RedirectURI string `mapstructure:"redirect_uri"`
Scope string `mapstructure:"scope"`
ResourceAud string `mapstructure:"resource_aud"`
AdminToken string `mapstructure:"admin_token"`
Timeout time.Duration `mapstructure:"timeout"`
}
// MessagePushConfig 消息推送配置
type MessagePushConfig struct {
GoalfyMaxBaseURL string `mapstructure:"goalfymax_base_url"`
Timeout int `mapstructure:"timeout"`
RetryCount int `mapstructure:"retry_count"`
RetryInterval int `mapstructure:"retry_interval"`
}
// AlertConfig 告警配置
type AlertConfig struct {
DingTalk DingTalkConfig `mapstructure:"dingtalk"`
}
// DingTalkConfig 钉钉配置
type DingTalkConfig struct {
Enabled bool `mapstructure:"enabled"`
Webhook string `mapstructure:"webhook"`
Secret string `mapstructure:"secret"`
TimeoutSeconds int `mapstructure:"timeout_seconds"`
Keyword string `mapstructure:"keyword"`
}
// PayConfig 支付服务配置
type PayConfig struct {
BaseURL string `mapstructure:"base_url"`
Timeout int `mapstructure:"timeout"`
}
// OssConfig 对象存储S3兼容配置
type OssConfig struct {
Endpoint string `mapstructure:"endpoint"`
Region string `mapstructure:"region"`
AccessKeyID string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"`
Bucket string `mapstructure:"bucket"`
AssumeRoleArn string `mapstructure:"assume_role_arn"`
PresignUrlExpire time.Duration `mapstructure:"presign_url_expire"`
}
// RedisConfig Redis配置
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// EmailConfig 邮件配置
type EmailConfig struct {
Sender string `mapstructure:"sender"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
InviteURLPrefix string `mapstructure:"invite_url_prefix"`
}
// Config 定义总配置结构
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Gateway GatewayConfig `mapstructure:"gateway"`
SSO SSOConfig `mapstructure:"sso"`
MessagePush MessagePushConfig `mapstructure:"message_push"`
Alert AlertConfig `mapstructure:"alert"`
Oss OssConfig `mapstructure:"oss"`
Redis RedisConfig `mapstructure:"redis"`
Email EmailConfig `mapstructure:"email"`
Log LogConfig `mapstructure:"log"`
Client ClientConfig `mapstructure:"client"`
PostgreSQL PostgreSQLConfig `mapstructure:"postgresql"`
Pay PayConfig `mapstructure:"pay"`
Jobs JobsConfig `mapstructure:"jobs"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
}
// ClientOption 客户端选项
type ClientOption struct {
Key string `mapstructure:"key" json:"key"`
Value string `mapstructure:"value" json:"value"`
Label string `json:"label"` // 用于前端展示格式key+value
}
// ClientConfig 客户端配置
type ClientConfig struct {
Options []ClientOption `mapstructure:"options"`
}
// PostgreSQLConfig PG配置
type PostgreSQLConfig struct {
DSN string `mapstructure:"dsn"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
}
type JobsConfig struct {
McpUsageBalance McpUsageBalanceJobConfig `mapstructure:"mcp_usage_balance"`
ModelTokenBalance ModelTokenBalanceJobConfig `mapstructure:"model_token_balance"`
}
type McpUsageBalanceJobConfig struct {
Enabled bool `mapstructure:"enabled"`
RunOnStartup bool `mapstructure:"run_on_startup"`
DelayMinutes int `mapstructure:"delay_minutes"`
}
type ModelTokenBalanceJobConfig struct {
Enabled bool `mapstructure:"enabled"`
RunOnStartup bool `mapstructure:"run_on_startup"`
DelayMinutes int `mapstructure:"delay_minutes"`
}
func (j JobsConfig) IsMcpUsageEnabled() bool {
return j.McpUsageBalance.Enabled
}
var (
config *Config
configLock sync.RWMutex
)
// LoadConfig 从文件加载配置
func LoadConfig(path string) error {
fmt.Printf("Loading config from %s\n", path)
viper.SetConfigFile(path)
err := viper.ReadInConfig()
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
if err := viper.Unmarshal(&config); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
// 设置默认值
if config.Server.Addr == "" {
config.Server.Addr = "0.0.0.0"
}
if config.Server.Port == 0 {
config.Server.Port = 8080
}
if config.Database.MaxIdleConns == 0 {
config.Database.MaxIdleConns = 10
}
if config.Database.MaxOpenConns == 0 {
config.Database.MaxOpenConns = 100
}
if config.Database.LogLevel == "" {
config.Database.LogLevel = "info"
}
if config.Log.Level == "" {
config.Log.Level = "info"
}
if config.Log.Format == "" {
config.Log.Format = "json"
}
if config.Log.Output == "" {
config.Log.Output = "stdout"
}
if config.Jobs.McpUsageBalance.DelayMinutes <= 0 {
config.Jobs.McpUsageBalance.DelayMinutes = 5
}
if config.Jobs.ModelTokenBalance.DelayMinutes <= 0 {
config.Jobs.ModelTokenBalance.DelayMinutes = 5
}
if config.Gateway.BaseURL == "" {
config.Gateway.BaseURL = "http://localhost:8080"
}
if config.Gateway.Timeout == 0 {
config.Gateway.Timeout = 30
}
if config.Gateway.Auth.LoginURL == "" {
config.Gateway.Auth.LoginURL = "http://44.247.156.94:8080/aigateway-admin/api/login"
}
if config.Gateway.Auth.Key == "" {
config.Gateway.Auth.Key = "Jiahe.123"
}
if config.MessagePush.GoalfyMaxBaseURL == "" {
config.MessagePush.GoalfyMaxBaseURL = "http://goalfymax-backend:8080"
}
if config.MessagePush.Timeout == 0 {
config.MessagePush.Timeout = 30
}
if config.MessagePush.RetryCount == 0 {
config.MessagePush.RetryCount = 3
}
if config.MessagePush.RetryInterval == 0 {
config.MessagePush.RetryInterval = 1000
}
if config.Pay.BaseURL == "" {
config.Pay.BaseURL = "http://goalfy-pay:8080"
}
if config.Pay.Timeout == 0 {
config.Pay.Timeout = 30
}
if config.Redis.Addr == "" {
config.Redis.Addr = "localhost:6379"
}
if config.Redis.DB == 0 {
config.Redis.DB = 0
}
// OSS 预签名默认过期时间
if config.Oss.PresignUrlExpire == 0 {
config.Oss.PresignUrlExpire = 10 * time.Minute
}
if config.Alert.DingTalk.TimeoutSeconds <= 0 {
config.Alert.DingTalk.TimeoutSeconds = 5
}
// 设置客户端默认选项
if len(config.Client.Options) == 0 {
config.Client.Options = []ClientOption{
{Key: "5hNXkkkVPfFWUjRvzVP23w", Value: "https://ob-staging-goalfymax.goalfyai.com/"},
{Key: "J10f8yxU1XDl1Tn00MXKeA", Value: "https://staging-goalfymax.goalfyai.com/"},
{Key: "xRpT9mgNpt2YvoY9z4FToA", Value: "https://goalfymax.goalfyai.com/"},
}
}
// 为每个客户端选项生成 label
for i := range config.Client.Options {
config.Client.Options[i].Label = config.Client.Options[i].Key + "+" + config.Client.Options[i].Value
}
return nil
}
// GetConfig 获取当前配置
func GetConfig() *Config {
configLock.RLock()
defer configLock.RUnlock()
return config
}
// GetDatabaseConfig 获取数据库配置
func GetDatabaseConfig() mysql.Config {
cfg := GetConfig()
return mysql.Config{
DSN: cfg.Database.DSN,
}
}
// GetClientOptions 获取客户端选项列表
func GetClientOptions() []ClientOption {
cfg := GetConfig()
return cfg.Client.Options
}
// GetClientValue 根据key获取客户端的展示值
func GetClientValue(key string) string {
options := GetClientOptions()
for _, opt := range options {
if opt.Key == key {
return opt.Value
}
}
return ""
}

View File

@@ -0,0 +1,49 @@
package jobs
import (
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
func StartMcpUsageBalanceScheduler(cfg config.McpUsageBalanceJobConfig, logger *utils.Logger) {
if !cfg.Enabled {
logger.Info("MCP usage balance scheduler disabled")
return
}
delay := time.Duration(cfg.DelayMinutes) * time.Minute
if delay < 0 {
delay = 0
}
runJob := func() {
logger.Info("MCP usage balance job started")
if err := services.RunMcpUsageBalanceJob(); err != nil {
logger.Error("MCP usage balance job failed", zap.Error(err))
} else {
logger.Info("MCP usage balance job completed")
}
}
if cfg.RunOnStartup {
runJob()
}
go func() {
for {
now := time.Now().UTC()
next := now.Truncate(time.Hour).Add(time.Hour)
sleep := next.Sub(now) + delay
if sleep < time.Minute {
sleep = time.Minute
}
time.Sleep(sleep)
runJob()
}
}()
}

View File

@@ -0,0 +1,49 @@
package jobs
import (
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/services"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
func StartModelTokenBalanceScheduler(cfg config.ModelTokenBalanceJobConfig, logger *utils.Logger) {
if !cfg.Enabled {
logger.Info("Model token balance scheduler disabled")
return
}
delay := time.Duration(cfg.DelayMinutes) * time.Minute
if delay < 0 {
delay = 0
}
runJob := func() {
logger.Info("Model token balance job started")
if err := services.RunModelTokenBalanceJob(); err != nil {
logger.Error("Model token balance job failed", zap.Error(err))
} else {
logger.Info("Model token balance job completed")
}
}
if cfg.RunOnStartup {
runJob()
}
go func() {
for {
now := time.Now().UTC()
next := now.Truncate(time.Hour).Add(time.Hour)
sleep := next.Sub(now) + delay
if sleep < time.Minute {
sleep = time.Minute
}
time.Sleep(sleep)
runJob()
}
}()
}

View File

@@ -0,0 +1,114 @@
package models
import (
"database/sql/driver"
"encoding/json"
"time"
)
// 操作类型常量
const (
OperationTypeBalanceAdjustment = "balance_adjustment" // 余额调整
OperationTypeUserLevelChange = "user_level_change" // 用户等级修改
OperationTypeUserStatusChange = "user_status_change" // 用户状态变更
OperationTypeModelPriceChange = "model_price_change" // 模型价格修改
OperationTypePermissionChange = "permission_change" // 权限变更
OperationTypeConfigChange = "config_change" // 配置修改
)
// 操作状态常量
const (
AuditLogStatusSuccess = "success" // 成功
AuditLogStatusFailed = "failed" // 失败
)
// 操作对象类型常量
const (
TargetTypeUser = "user" // 用户
TargetTypeModel = "model" // 模型
TargetTypeConfig = "config" // 配置
)
// OperationDetails 操作详情JSON结构
type OperationDetails map[string]interface{}
// Value 实现 driver.Valuer 接口
func (d OperationDetails) Value() (driver.Value, error) {
if d == nil {
return nil, nil
}
return json.Marshal(d)
}
// Scan 实现 sql.Scanner 接口
func (d *OperationDetails) Scan(value interface{}) error {
if value == nil {
*d = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, d)
}
// AuditLog 审计日志模型
type AuditLog struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
OperationType string `json:"operation_type" gorm:"type:varchar(50);not null;index:idx_operation_type_time;comment:操作类型"`
OperationTime time.Time `json:"operation_time" gorm:"not null;index:idx_operation_type_time;index:idx_operation_time;comment:操作时间"`
OperatorID int `json:"operator_id" gorm:"not null;index:idx_operator_time;comment:操作人ID"`
OperatorEmail string `json:"operator_email" gorm:"type:varchar(255);not null;index:idx_operator_email;comment:操作人邮箱"`
TargetType string `json:"target_type" gorm:"type:varchar(50);comment:操作对象类型"`
TargetID *int `json:"target_id" gorm:"comment:操作对象ID"`
TargetEmail string `json:"target_email" gorm:"type:varchar(255);index:idx_target_email;comment:操作对象邮箱"`
OperationDetails OperationDetails `json:"operation_details" gorm:"type:json;comment:操作详情JSON"`
IPAddress string `json:"ip_address" gorm:"type:varchar(45);comment:操作来源IP地址"`
UserAgent string `json:"user_agent" gorm:"type:varchar(500);comment:用户代理"`
Status string `json:"status" gorm:"type:varchar(20);default:'success';comment:操作状态"`
ErrorMessage string `json:"error_message" gorm:"type:text;comment:错误信息"`
CreatedAt time.Time `json:"created_at" gorm:"not null;comment:记录创建时间"`
UpdatedAt time.Time `json:"updated_at" gorm:"not null;comment:记录更新时间"`
}
// TableName 指定数据库表名
func (AuditLog) TableName() string {
return "admin_audit_logs"
}
// AuditLogListRequest 审计日志列表请求
type AuditLogListRequest struct {
OperationType string `form:"operation_type"` // 操作类型筛选
OperatorEmail string `form:"operator_email"` // 操作人筛选
TargetEmail string `form:"target_email"` // 操作对象搜索(模糊匹配)
StartTime string `form:"start_time"` // 开始时间
EndTime string `form:"end_time"` // 结束时间
Page int `form:"page,default=1"` // 页码
Size int `form:"size,default=20"` // 每页数量
SortBy string `form:"sort_by"` // 排序字段默认operation_time
SortOrder string `form:"sort_order"` // 排序方向desc/asc默认desc
}
// AuditLogListResponse 审计日志列表响应
type AuditLogListResponse struct {
List []AuditLog `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// CreateAuditLogRequest 创建审计日志请求(内部使用)
type CreateAuditLogRequest struct {
OperationType string `json:"operation_type" binding:"required"`
OperatorID int `json:"operator_id" binding:"required"`
OperatorEmail string `json:"operator_email" binding:"required"`
TargetType string `json:"target_type"`
TargetID *int `json:"target_id"`
TargetEmail string `json:"target_email"`
OperationDetails OperationDetails `json:"operation_details"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
}

View File

@@ -0,0 +1,28 @@
package models
import "time"
// 操作类型常量
const (
OperationTypeAdd = "add"
OperationTypeDeduct = "deduct"
)
// BalanceOperationLog 余额操作日志
type BalanceOperationLog struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
UserID int `json:"user_id" gorm:"not null;index:idx_user_id;comment:GoalfyMax用户ID"`
OperationType string `json:"operation_type" gorm:"type:varchar(10);not null;comment:操作类型:add/deduct"`
Amount float64 `json:"amount" gorm:"type:decimal(15,2);not null;comment:操作金额(美元)"`
BalanceBefore float64 `json:"balance_before" gorm:"type:decimal(15,2);not null;comment:操作前余额(美元)"`
BalanceAfter float64 `json:"balance_after" gorm:"type:decimal(15,2);not null;comment:操作后余额(美元)"`
OperatorID int `json:"operator_id" gorm:"not null;index:idx_operator_id;comment:操作者ID"`
OperatorName string `json:"operator_name" gorm:"type:varchar(50);comment:操作者名称"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
CreatedAt time.Time `json:"created_at" gorm:"not null;index:idx_created_at;comment:创建时间"`
UpdatedAt time.Time `json:"updated_at" gorm:"not null;comment:更新时间"`
}
func (BalanceOperationLog) TableName() string {
return "balance_operation_logs"
}

104
internal/models/common.go Normal file
View File

@@ -0,0 +1,104 @@
package models
import (
"time"
"gorm.io/gorm"
)
// BaseModel 基础模型,包含公共字段
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt,omitempty"`
}
// User 用户模型
type User struct {
BaseModel
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
Nickname string `gorm:"size:50" json:"nickname"`
Avatar string `gorm:"size:255" json:"avatar"`
Status int `gorm:"default:1;comment:状态 1:正常 0:禁用" json:"status"`
SSOProvider string `gorm:"size:50;not null;comment:SSO提供商" json:"ssoProvider"`
LastLoginAt *time.Time `gorm:"comment:最后登录时间" json:"lastLoginAt"`
LoginCount int `gorm:"default:0;comment:登录次数" json:"loginCount"`
RoleID uint `gorm:"not null;default:0;comment:角色ID" json:"roleId"`
}
// UserWithRoles 带角色信息的用户模型
type UserWithRoles struct {
User
Role *Role `json:"role,omitempty"`
}
// Role 角色模型
type Role struct {
BaseModel
Name string `gorm:"uniqueIndex;size:50;not null" json:"name"`
Level int `gorm:"uniqueIndex;not null" json:"level"`
Description string `gorm:"type:text" json:"description"`
IsDefault bool `gorm:"default:false" json:"isDefault"`
}
// SystemConfig 系统配置模型
type SystemConfig struct {
BaseModel
Key string `gorm:"uniqueIndex;size:100;not null" json:"key"`
Name string `gorm:"size:100;not null" json:"name"`
Value string `gorm:"type:text" json:"value"`
Type string `gorm:"size:20;default:string" json:"type"` // string, int, bool, json
Desc string `gorm:"size:255" json:"desc"`
Status int `gorm:"default:1" json:"status"` // 1:启用 0:禁用
}
// LoginLog 登录日志模型
type LoginLog struct {
BaseModel
UserID uint `gorm:"not null" json:"userId"`
Username string `gorm:"size:50;not null" json:"username"`
IP string `gorm:"size:45" json:"ip"`
UserAgent string `gorm:"size:500" json:"userAgent"`
Status int `gorm:"default:1" json:"status"` // 1:成功 0:失败
Message string `gorm:"size:255" json:"message"`
}
// OperationLog 操作日志模型
type OperationLog struct {
BaseModel
UserID uint `gorm:"not null" json:"userId"`
Username string `gorm:"size:50;not null" json:"username"`
Module string `gorm:"size:50" json:"module"`
Operation string `gorm:"size:50" json:"operation"`
Method string `gorm:"size:10" json:"method"`
Path string `gorm:"size:255" json:"path"`
IP string `gorm:"size:45" json:"ip"`
UserAgent string `gorm:"size:500" json:"userAgent"`
Request string `gorm:"type:text" json:"request"`
Response string `gorm:"type:text" json:"response"`
Status int `gorm:"default:1" json:"status"`
Duration int64 `json:"duration"` // 毫秒
}
// TableName 指定表名
func (User) TableName() string {
return "admin_users"
}
func (Role) TableName() string {
return "admin_roles"
}
func (SystemConfig) TableName() string {
return "admin_system_configs"
}
func (LoginLog) TableName() string {
return "admin_login_logs"
}
func (OperationLog) TableName() string {
return "admin_operation_logs"
}

View File

@@ -0,0 +1,80 @@
package models
import "time"
// GoalfyMaxUser 对应 admin_goalfymax_users 表
type GoalfyMaxUser struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
UserID int `json:"user_id" gorm:"not null;uniqueIndex:uk_user_id;comment:SSO用户ID"`
Username string `json:"username" gorm:"not null;type:varchar(50);comment:用户名"`
Email string `json:"email" gorm:"not null;type:varchar(100);comment:邮箱"`
Nickname string `json:"nickname" gorm:"type:varchar(50);comment:昵称"`
Avatar string `json:"avatar" gorm:"type:varchar(255);comment:头像URL"`
UserLevelCode string `json:"user_level_code" gorm:"type:varchar(50);default:'normal';comment:用户等级代码"`
IsBanned bool `json:"is_banned" gorm:"not null;default:false;comment:是否被封禁"`
BanReason string `json:"ban_reason" gorm:"type:varchar(255);comment:封禁原因"`
BannedAt *time.Time `json:"banned_at" gorm:"comment:封禁时间"`
BannedBy int `json:"banned_by" gorm:"comment:封禁操作者ID"`
LastLoginAt *time.Time `json:"last_login_at" gorm:"comment:最后登录时间"`
GoalfyHubPermission int `json:"goalfy_hub_permission" gorm:"default:0;comment:是否具备GoalfyHub权限 (0: 不具备, 1: 具备)"`
Version int `json:"version" gorm:"default:1;comment:1用户版 2观察版 3用户版和观察版"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"-" gorm:"default:null"`
// Balance 用户余额美元从Redis查询不存储在数据库中
Balance *float64 `json:"balance,omitempty" gorm:"-"`
}
func (GoalfyMaxUser) TableName() string {
return "admin_goalfymax_users"
}
// 列表请求
type GoalfyMaxUserListRequest struct {
Username string `form:"username"`
Email string `form:"email"`
Status *int `form:"status"` // 1 正常 0 封禁(映射到 IsBanned
Page int `form:"page,default=1"`
Size int `form:"size,default=20"`
}
// 更新请求
type GoalfyMaxUserUpdateRequest struct {
Nickname string `json:"nickname"`
Email string `json:"email"`
Avatar string `json:"avatar"`
UserLevelCode string `json:"user_level_code"`
Version *int `json:"version"` // 版本1-用户版 2-观察版 3-用户版和观察版
GoalfyHubPermission *int `json:"goalfy_hub_permission"`
}
// 封禁请求
type GoalfyMaxUserBanRequest struct {
Reason string `json:"reason" binding:"required"`
}
// 增加余额请求
type GoalfyMaxUserAddBalanceRequest struct {
Amount float64 `json:"amount" binding:"required,gt=0"` // 美元金额必须大于0
}
// 新增用户请求
type GoalfyMaxUserCreateRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
UserLevelCode string `json:"user_level_code"` // 默认为 normal
Version int `json:"version"` // 版本1-用户版 2-观察版 3-用户版和观察版默认为1
}
// 新增用户请求(支持字符串输入)
type GoalfyMaxUserCreateRequestString struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Phone string `json:"phone"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}

View File

@@ -0,0 +1,65 @@
package models
import (
"time"
"gorm.io/gorm"
)
// InviteCode 简化版邀请码模型
type InviteCode struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
Code string `json:"code" gorm:"not null;type:varchar(64);uniqueIndex;comment:邀请码"`
IsUsed bool `json:"is_used" gorm:"not null;default:0;type:tinyint(1);comment:是否已使用"`
ClientID string `json:"client_id" gorm:"type:varchar(64);comment:客户端ID"`
Email string `json:"email" gorm:"type:varchar(255);comment:关联邮箱"`
UserLevelID *uint `json:"user_level_id" gorm:"type:BIGINT;comment:用户等级ID"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:过期时间"`
CreatedAt time.Time `json:"created_at" gorm:"not null;comment:创建时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// TableName 指定数据库表名
func (InviteCode) TableName() string {
return "admin_invite_codes"
}
// 列表请求(简化)
type InviteCodeListRequest struct {
Code string `form:"code"`
IsUsed *bool `form:"is_used"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Page int `form:"page,default=1"`
Size int `form:"size,default=20"`
}
// 创建请求(支持设置过期时间)
type InviteCodeCreateRequest struct {
Emails []string `json:"emails" form:"emails"` // 邮箱列表,可选
UserLevelID *uint `json:"user_level_id" form:"user_level_id"` // 用户等级ID可选
ExpiresAt *time.Time `json:"expires_at" form:"expires_at"` // 过期时间,可选
ClientID string `json:"client_id" form:"client_id"` // 客户端ID可选保留向后兼容
}
// 更新请求(支持更新过期时间)
type InviteCodeUpdateRequest struct {
ClientID string `json:"client_id" form:"client_id"` // 客户端ID可选
Email string `json:"email" form:"email"` // 邮箱,可选
UserLevelID *uint `json:"user_level_id" form:"user_level_id"` // 用户等级ID可选
ExpiresAt *time.Time `json:"expires_at" form:"expires_at"` // 过期时间,可选
}
// 统计响应(简化,可选)
type InviteCodeStatistics struct {
Total int `json:"total"`
Used int `json:"used"`
Unused int `json:"unused"`
TodayCreated int `json:"today_created"`
}
// 列表响应
type InviteCodeListResponse struct {
List []InviteCode `json:"list"`
Total int64 `json:"total"`
}

View File

@@ -0,0 +1,92 @@
package models
import (
"time"
"gorm.io/gorm"
)
// InviteCodeApplication 邀请码申请模型
type InviteCodeApplication struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
Email string `json:"email" gorm:"not null;type:varchar(255);index;comment:申请邮箱"`
Reason string `json:"reason" gorm:"type:text;comment:申请理由"`
Language string `json:"language" gorm:"type:varchar(10);default:'zh';comment:语言:zh-中文,en-英文"`
Status string `json:"status" gorm:"not null;type:varchar(20);default:'pending';index;comment:申请状态:pending-待处理,approved-已通过,rejected-已拒绝"`
InviteCodeID *uint `json:"invite_code_id" gorm:"comment:关联的邀请码ID"`
InviteCode *InviteCode `json:"invite_code" gorm:"foreignKey:InviteCodeID;constraint:OnDelete:SET NULL;-:migration"`
RejectReason string `json:"reject_reason" gorm:"type:text;comment:拒绝理由"`
ApprovedAt *time.Time `json:"approved_at" gorm:"comment:审批时间"`
ApprovedBy string `json:"approved_by" gorm:"type:varchar(64);comment:审批人"`
EmailSentAt *time.Time `json:"email_sent_at" gorm:"comment:邮件发送时间"`
CreatedAt time.Time `json:"created_at" gorm:"not null;comment:创建时间"`
UpdatedAt time.Time `json:"updated_at" gorm:"not null;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:软删除时间"`
}
// TableName 指定数据库表名
func (InviteCodeApplication) TableName() string {
return "admin_invite_code_applications"
}
// InviteCodeApplicationListRequest 申请列表请求
type InviteCodeApplicationListRequest struct {
Email string `form:"email"`
Status string `form:"status"` // pending, approved, rejected
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Page int `form:"page,default=1"`
Size int `form:"size,default=20"`
}
// InviteCodeApplicationCreateRequest 创建申请请求(官网提交)
type InviteCodeApplicationCreateRequest struct {
Email string `json:"email" binding:"required,email"`
Reason string `json:"reason"`
Language string `json:"language"` // zh 或 en默认 zh
}
// InviteCodeApplicationApproveRequest 审批通过请求
type InviteCodeApplicationApproveRequest struct {
ApplicationID uint `json:"application_id" binding:"required"`
ValidDays int `json:"valid_days"` // 有效期天数默认7天
}
// InviteCodeApplicationRejectRequest 审批拒绝请求
type InviteCodeApplicationRejectRequest struct {
ApplicationID uint `json:"application_id" binding:"required"`
RejectReason string `json:"reject_reason"`
}
// InviteCodeApplicationBatchApproveRequest 批量审批通过请求
type InviteCodeApplicationBatchApproveRequest struct {
ApplicationIDs []uint `json:"application_ids" binding:"required"`
ValidDays int `json:"valid_days"` // 有效期天数默认7天
}
// InviteCodeApplicationBatchRejectRequest 批量审批拒绝请求
type InviteCodeApplicationBatchRejectRequest struct {
ApplicationIDs []uint `json:"application_ids" binding:"required"`
RejectReason string `json:"reject_reason"`
}
// InviteCodeApplicationListResponse 申请列表响应
type InviteCodeApplicationListResponse struct {
List []InviteCodeApplication `json:"list"`
Total int64 `json:"total"`
}
// InviteCodeApplicationStatistics 申请统计
type InviteCodeApplicationStatistics struct {
TotalPending int `json:"total_pending"` // 待处理数量
TotalApproved int `json:"total_approved"` // 已通过数量
TotalRejected int `json:"total_rejected"` // 已拒绝数量
TodayApplied int `json:"today_applied"` // 今日申请数量
}
// 申请状态常量
const (
ApplicationStatusPending = "pending"
ApplicationStatusApproved = "approved"
ApplicationStatusRejected = "rejected"
)

View File

@@ -0,0 +1,78 @@
package models
import (
"time"
)
// MessagePushLog 消息推送记录模型
type MessagePushLog struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Title string `gorm:"type:varchar(255);not null;default:'';comment:消息标题" json:"title"`
Content string `gorm:"type:text;not null;comment:消息内容" json:"content"`
TargetUsers string `gorm:"type:json;not null;comment:目标用户ID列表" json:"target_users"` // 存储JSON字符串
SenderID int `gorm:"not null;comment:发送人ID" json:"sender_id"`
SenderName string `gorm:"type:varchar(100);not null;comment:发送人姓名" json:"sender_name"`
Status int `gorm:"type:tinyint;default:0;comment:发送状态 0=待发送 1=发送中 2=发送成功 3=发送失败" json:"status"`
SuccessCount int `gorm:"default:0;comment:成功数量" json:"success_count"`
FailCount int `gorm:"default:0;comment:失败数量" json:"fail_count"`
ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message"`
SentAt *time.Time `gorm:"comment:发送时间" json:"sent_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定数据库表名
func (MessagePushLog) TableName() string {
return "admin_message_push_logs"
}
// MessagePushRequest 消息推送请求
type MessagePushRequest struct {
Title string `json:"title" validate:"required,min=1,max=100"`
Content string `json:"content" validate:"required,min=1,max=2000"`
UserIDs []int `json:"user_ids" validate:"required,min=1"`
}
// MessagePushResponse 消息推送响应
type MessagePushResponse struct {
LogID int64 `json:"log_id"`
SuccessCount int `json:"success_count"`
FailCount int `json:"fail_count"`
}
// MessagePushListRequest 推送记录列表请求
type MessagePushListRequest struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
Status *int `form:"status"` // 0=待发送, 1=发送中, 2=发送成功, 3=发送失败
SenderID *int `form:"sender_id"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
}
// MessagePushListResponse 推送记录列表响应
type MessagePushListResponse struct {
List []MessagePushLog `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// UserSearchRequest 用户搜索请求
type UserSearchRequest struct {
Keyword string `form:"keyword" validate:"required,min=1"`
Limit int `form:"limit,default=20"`
}
// UserSearchResponse 用户搜索响应
type UserSearchResponse struct {
Users []UserSearchItem `json:"users"`
Total int `json:"total"`
}
// UserSearchItem 用户搜索项
type UserSearchItem struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}

View File

@@ -0,0 +1,65 @@
package models
import (
"encoding/json"
"time"
)
// QuotaHistoryRequest 配额历史查询请求
type QuotaHistoryRequest struct {
StartDate string `json:"start_date" binding:"required"` // 开始日期 (YYYY-MM-DD)
EndDate string `json:"end_date" binding:"required"` // 结束日期 (YYYY-MM-DD)
UserID string `json:"user_id"` // 用户ID (可选)
ApiGroup string `json:"api_group"` // API组 (可选)
ProjectID string `json:"project_id"` // 项目ID (可选)
Period string `json:"period"` // 周期: daily, monthly
GroupBy []string `json:"group_by"` // 分组维度
}
// QuotaHistoryItem 配额历史数据项
//
// type QuotaHistoryItem struct {
// ID uint `json:"id"`
// UserID string `json:"user_id"`
// ApiGroup string `json:"api_group"`
// ProjectID string `json:"project_id"`
// Day string `json:"day"`
// Account string `json:"account"`
// Model string `json:"model"`
// QuotaUsed float64 `json:"quota_used"`
// CreatedAt time.Time `json:"created_at"`
// }
type QuotaHistoryItem struct {
ID uint `json:"ID"`
UserID string `json:"UserID"`
ApiGroup string `json:"ApiGroup"`
ProjectID string `json:"ProjectID"`
Day string `json:"Day"`
Account string `json:"Account"`
Model string `json:"Model"`
QuotaUsed float64 `json:"QuotaUsed"`
CreatedAt time.Time `json:"CreatedAt"`
}
// QuotaHistoryResponse 配额历史查询响应
type QuotaHistoryResponse struct {
Success bool `json:"success"`
Data []QuotaHistoryItem `json:"data"`
Message string `json:"message,omitempty"`
}
// GatewayConfig 网关配置
type GatewayConfig struct {
BaseURL string `json:"base_url"`
Timeout int `json:"timeout"` // 超时时间(秒)
}
// QuotaRule 透传结构(保持与网关一致,不强约束字段)
type QuotaRule map[string]interface{}
// QuotaRulesResponse 配额规则列表响应与网关一致Data透传
type QuotaRulesResponse struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Message string `json:"message,omitempty"`
}

52
internal/models/rbac.go Normal file
View File

@@ -0,0 +1,52 @@
package models
// RolePagePermission 角色页面权限关联
type RolePagePermission struct {
BaseModel
RoleID uint `gorm:"not null" json:"roleId"`
PageID uint `gorm:"not null" json:"pageId"`
Role Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
Page Page `gorm:"foreignKey:PageID" json:"page,omitempty"`
}
// Page 页面模型
type Page struct {
BaseModel
Name string `gorm:"size:50;not null" json:"name"`
Path string `gorm:"uniqueIndex;size:100;not null" json:"path"`
Icon string `gorm:"size:50" json:"icon"`
SortOrder int `gorm:"default:0" json:"sortOrder"`
IsActive bool `gorm:"default:true" json:"isActive"`
}
func (RolePagePermission) TableName() string {
return "admin_role_page_permissions"
}
func (Page) TableName() string {
return "admin_pages"
}
// RBAC请求模型
type UserRoleAssignRequest struct {
UserID uint `json:"userId" binding:"required"`
RoleIDs []uint `json:"roleIds"`
}
type RolePagePermissionAssignRequest struct {
RoleID uint `json:"roleId" binding:"required"`
PageIDs []uint `json:"pageIds"`
}
// 用户权限响应模型
type UserPermissionsResponse struct {
User User `json:"user"`
Roles []Role `json:"roles"`
Pages []Page `json:"pages"`
}
// 角色页面权限响应模型
type RolePagePermissionsResponse struct {
Role Role `json:"role"`
Pages []Page `json:"pages"`
}

141
internal/models/request.go Normal file
View File

@@ -0,0 +1,141 @@
package models
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// UserCreateRequest 创建用户请求
type UserCreateRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Nickname string `json:"nickname"`
SSOProvider string `json:"ssoProvider" binding:"required"`
Password string `json:"password" binding:"required,min=8"`
RoleID uint `json:"roleId"`
}
// UserUpdateRequest 更新用户请求
type UserUpdateRequest struct {
Nickname string `json:"nickname"`
Email string `json:"email" binding:"email"`
Avatar string `json:"avatar"`
Status *int `json:"status"`
SSOProvider string `json:"ssoProvider"`
RoleID *uint `json:"roleId"`
}
// RoleCreateRequest 创建角色请求
type RoleCreateRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Level int `json:"level" binding:"required,min=1,max=5"`
Description string `json:"description"`
IsDefault bool `json:"isDefault"`
}
// RoleUpdateRequest 更新角色请求
type RoleUpdateRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Level int `json:"level" binding:"required,min=1,max=5"`
Description string `json:"description"`
IsDefault bool `json:"isDefault"`
}
// PageCreateRequest 创建页面请求
type PageCreateRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Path string `json:"path" binding:"required"`
Icon string `json:"icon"`
SortOrder int `json:"sortOrder"`
IsActive bool `json:"isActive"`
}
// PageUpdateRequest 更新页面请求
type PageUpdateRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Path string `json:"path" binding:"required"`
Icon string `json:"icon"`
SortOrder int `json:"sortOrder"`
IsActive bool `json:"isActive"`
}
// SystemConfigRequest 系统配置请求
type SystemConfigRequest struct {
Key string `json:"key" binding:"required"`
Name string `json:"name" binding:"required"`
Value string `json:"value" binding:"required"`
Type string `json:"type"`
Desc string `json:"desc"`
}
// SystemConfigListRequest 系统配置列表请求
type SystemConfigListRequest struct {
PageRequest
Key string `form:"key"`
Name string `form:"name"`
Status *int `form:"status"`
}
// SystemConfigUpdateRequest 系统配置更新请求
type SystemConfigUpdateRequest struct {
Name string `json:"name" binding:"required"`
Value string `json:"value" binding:"required"`
Type string `json:"type"`
Desc string `json:"desc"`
}
// SystemConfigUpdateStatusRequest 系统配置状态更新请求
type SystemConfigUpdateStatusRequest struct {
Status int `json:"status" binding:"required,oneof=0 1"`
}
// PageRequest 分页请求
type PageRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
Size int `form:"size" binding:"omitempty,min=1,max=100"`
}
// UserListRequest 用户列表请求
type UserListRequest struct {
PageRequest
Username string `form:"username"`
Email string `form:"email"`
Status *int `form:"status"`
}
// RoleListRequest 角色列表请求
type RoleListRequest struct {
PageRequest
Name string `form:"name"`
Status *int `form:"status"`
}
// PageListRequest 页面列表请求
type PageListRequest struct {
PageRequest
Name string `form:"name"`
Path string `form:"path"`
IsActive *bool `form:"isActive"`
}
// LoginLogListRequest 登录日志列表请求
type LoginLogListRequest struct {
PageRequest
Username string `form:"username"`
IP string `form:"ip"`
Status *int `form:"status"`
StartTime string `form:"startTime"`
EndTime string `form:"endTime"`
}
// OperationLogListRequest 操作日志列表请求
type OperationLogListRequest struct {
PageRequest
Username string `form:"username"`
Module string `form:"module"`
Operation string `form:"operation"`
Status *int `form:"status"`
StartTime string `form:"startTime"`
EndTime string `form:"endTime"`
}

View File

@@ -0,0 +1,99 @@
package models
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// PageResponse 分页响应结构
type PageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
User User `json:"user"`
ExpireAt int64 `json:"expireAt"`
}
// UserListResponse 用户列表响应
type UserListResponse struct {
Users []User `json:"users"`
Total int64 `json:"total"`
}
// PageTreeResponse 页面树响应
type PageTreeResponse struct {
Pages []Page `json:"pages"`
}
// SystemConfigResponse 系统配置响应
type SystemConfigResponse struct {
Configs []SystemConfig `json:"configs"`
}
// 响应码常量
const (
CodeSuccess = 200
CodeError = 500
CodeInvalid = 400
CodeUnauthorized = 401
CodeForbidden = 403
CodeNotFound = 404
)
// 响应消息常量
const (
MsgSuccess = "操作成功"
MsgError = "操作失败"
MsgInvalid = "参数错误"
MsgUnauthorized = "未授权"
MsgForbidden = "禁止访问"
MsgNotFound = "资源不存在"
)
// NewResponse 创建响应
func NewResponse(code int, message string, data interface{}) *Response {
return &Response{
Code: code,
Message: message,
Data: data,
}
}
// NewSuccessResponse 创建成功响应
func NewSuccessResponse(data interface{}) *Response {
return &Response{
Code: CodeSuccess,
Message: MsgSuccess,
Data: data,
}
}
// NewErrorResponse 创建错误响应
func NewErrorResponse(message string) *Response {
return &Response{
Code: CodeError,
Message: message,
}
}
// NewPageResponse 创建分页响应
func NewPageResponse(data interface{}, total int64, page, size int) *PageResponse {
return &PageResponse{
Code: CodeSuccess,
Message: MsgSuccess,
Data: data,
Total: total,
Page: page,
Size: size,
}
}

196
internal/models/sso.go Normal file
View File

@@ -0,0 +1,196 @@
package models
import (
"time"
)
// SSOConfig SSO配置结构体
type SSOConfig struct {
// SSO服务器地址
SSOServerURL string `json:"sso_server_url"`
// OAuth客户端ID
ClientID string `json:"client_id"`
// OAuth客户端密钥
ClientSecret string `json:"client_secret"`
// 重定向URI
RedirectURI string `json:"redirect_uri"`
// 请求作用域
Scope string `json:"scope"`
// 资源受众
ResourceAud string `json:"resource_aud"`
// HTTP超时时间
Timeout time.Duration `json:"timeout"`
}
// TokenResponse OAuth令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// SSOUserInfo SSO API 返回的用户信息(用于解析 SSO 响应)
type SSOUserInfo struct {
Sub string `json:"sub"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Roles interface{} `json:"roles,omitempty"` // 使用 interface{} 来接受任何类型
}
// UserInfo 用户信息(用于应用程序内部)
type UserInfo struct {
Sub string `json:"sub"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Pages []Page `json:"pages,omitempty"`
Roles []Role `json:"roles,omitempty"`
}
// OpenIDConfiguration OpenID配置
type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
JwksURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
}
// JWKS JSON Web Key Set
type JWKS struct {
Keys []map[string]interface{} `json:"keys"`
}
// AuthCodeRequest 授权码请求参数
type AuthCodeRequest struct {
ClientID string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
ResponseType string `json:"response_type"`
Scope string `json:"scope"`
State string `json:"state"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
Prompt string `json:"prompt"`
}
// TokenRequest 令牌请求参数
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code"`
RedirectURI string `json:"redirect_uri"`
CodeVerifier string `json:"code_verifier"`
RefreshToken string `json:"refresh_token"`
}
// Session 用户会话信息
type Session struct {
UserID string `json:"user_id"`
UserInfo *UserInfo `json:"user_info"`
TokenInfo *TokenResponse `json:"token_info"`
ExpiresAt time.Time `json:"expires_at"`
}
// SSOLoginRequest SSO登录请求
type SSOLoginRequest struct {
// 可以为空用于初始化SSO登录流程
}
// SSOLoginResponse SSO登录响应
type SSOLoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
AuthURL string `json:"auth_url"`
State string `json:"state"`
CodeVerifier string `json:"code_verifier"`
}
// SSOCallbackRequest SSO回调请求
type SSOCallbackRequest struct {
Code string `json:"code" binding:"required"`
State string `json:"state" binding:"required"`
}
// SSOCallbackResponse SSO回调响应
type SSOCallbackResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
AccessToken string `json:"access_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
UserInfo *UserInfo `json:"user_info,omitempty"`
UUID string `json:"uuid,omitempty"`
}
// RefreshTokenRequest 刷新令牌请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// RefreshTokenResponse 刷新令牌响应
type RefreshTokenResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
AccessToken string `json:"access_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
}
// LogoutRequest 登出请求
type LogoutRequest struct {
// AccessToken字段保留用于向后兼容但实际不再使用
AccessToken string `json:"access_token,omitempty"`
}
// LogoutResponse 登出响应
type LogoutResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// UserInfoResponse 用户信息响应
type UserInfoResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
UserInfo *UserInfo `json:"user_info,omitempty"`
}
// PKCEState PKCE状态信息
type PKCEState struct {
BaseModel
State string `gorm:"size:191;uniqueIndex;column:state" json:"state"`
CodeVerifier string `gorm:"column:code_verifier" json:"code_verifier"`
}
func (p *PKCEState) TableName() string {
return "admin_pkce_states"
}
// LoginInfo 用户登录信息
type LoginInfo struct {
BaseModel
UserID int `gorm:"column:user_id;not null" json:"user_id"`
UserName string `gorm:"column:user_name;size:100;not null" json:"user_name"`
Email string `gorm:"column:email;size:255;not null" json:"email"`
UUID string `gorm:"column:uuid;size:100;not null" json:"uuid"`
IsOnline bool `gorm:"column:is_online;default:false" json:"is_online"`
}
func (l *LoginInfo) TableName() string {
return "admin_login_infos"
}
// ErrorResponse 错误响应
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,63 @@
package models
import (
"gorm.io/gorm"
"time"
)
// UserFeedback 用户反馈模型
type UserFeedback struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UID uint64 `gorm:"column:uid;not null;index;comment:用户ID" json:"user_id"`
Content string `gorm:"type:text;not null;comment:反馈内容" json:"content"`
FileKeys string `gorm:"column:file_keys;type:json;not null;comment:关联文件Key列表" json:"file_keys"`
Status int `gorm:"default:0;comment:0未处理 1已处理" json:"status"`
HandledBy *int `gorm:"column:handled_by;comment:处理人" json:"handled_by"`
HandledAt *time.Time `gorm:"column:handled_at;comment:处理时间" json:"handled_at"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// TableName 指定数据库表名
func (UserFeedback) TableName() string {
return "m_problem_feedbacks"
}
// UserFeedbackListRequest 用户反馈列表请求
type UserFeedbackListRequest struct {
Page int `json:"page" form:"page" binding:"min=1"`
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100"`
Status *int `json:"status" form:"status"`
UserID *int `json:"user_id" form:"user_id"`
Keyword string `json:"keyword" form:"keyword"`
StartTime string `json:"start_time" form:"start_time"`
EndTime string `json:"end_time" form:"end_time"`
}
// UserFeedbackListResponse 用户反馈列表响应
type UserFeedbackListResponse struct {
List []UserFeedbackItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// UserFeedbackItem 列表项扩展返回可访问URL
type UserFeedbackItem struct {
ID int64 `json:"id"`
UserID uint64 `json:"user_id"`
Content string `json:"content"`
FileKeys []string `json:"file_keys"`
FileContents []string `json:"file_contents"` // Base64编码的图片内容
Status int `json:"status"`
HandledBy *int `json:"handled_by"`
HandledAt *time.Time `json:"handled_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserFeedbackMarkRequest 标记处理请求
type UserFeedbackMarkRequest struct {
Note string `json:"note" form:"note"`
}

View File

@@ -0,0 +1,59 @@
package models
import "time"
// UserLevelConfig 用户等级配置表
type UserLevelConfig struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement;comment:主键ID"`
LevelName string `json:"level_name" gorm:"not null;uniqueIndex:uk_level_name;type:varchar(50);comment:等级名称"`
LevelCode string `json:"level_code" gorm:"not null;uniqueIndex:uk_level_code;type:varchar(50);comment:等级代码"`
ProjectLimit int `json:"project_limit" gorm:"not null;default:0;comment:项目数限制0表示不限"`
CoderVMLimit int `json:"coder_vm_limit" gorm:"not null;default:0;comment:Coder VM上限0表示不限"`
BrowserVMLimit int `json:"browser_vm_limit" gorm:"not null;default:0;comment:Browser VM上限0表示不限"`
ProcessLimit int `json:"process_limit" gorm:"not null;default:0;comment:进程上限0表示不限"`
Description string `json:"description" gorm:"type:varchar(255);comment:等级描述"`
SortOrder int `json:"sort_order" gorm:"not null;default:0;comment:排序顺序"`
Status int `json:"status" gorm:"not null;default:1;comment:状态 1-启用 0-禁用"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (UserLevelConfig) TableName() string {
return "admin_user_level_configs"
}
// UserLevelConfigListRequest 列表请求
type UserLevelConfigListRequest struct {
LevelName string `form:"level_name"`
Status *int `form:"status"`
Page int `form:"page,default=1"`
Size int `form:"size,default=20"`
}
// UserLevelConfigCreateRequest 创建请求
type UserLevelConfigCreateRequest struct {
LevelName string `json:"level_name" binding:"required,min=1,max=50"`
LevelCode string `json:"level_code" binding:"required,min=1,max=50"`
ProjectLimit int `json:"project_limit" binding:"min=0"`
CoderVMLimit int `json:"coder_vm_limit" binding:"min=0"`
BrowserVMLimit int `json:"browser_vm_limit" binding:"min=0"`
ProcessLimit int `json:"process_limit" binding:"min=0"`
Description string `json:"description" binding:"max=255"`
SortOrder int `json:"sort_order"`
}
// UserLevelConfigUpdateRequest 更新请求
type UserLevelConfigUpdateRequest struct {
LevelName string `json:"level_name" binding:"required,min=1,max=50"`
ProjectLimit int `json:"project_limit" binding:"min=0"`
CoderVMLimit int `json:"coder_vm_limit" binding:"min=0"`
BrowserVMLimit int `json:"browser_vm_limit" binding:"min=0"`
ProcessLimit int `json:"process_limit" binding:"min=0"`
Description string `json:"description" binding:"max=255"`
SortOrder int `json:"sort_order"`
}
// UserLevelConfigUpdateStatusRequest 更新状态请求
type UserLevelConfigUpdateStatusRequest struct {
Status int `json:"status" binding:"required,oneof=0 1"`
}

View File

@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
// UserProjectQuota 用户资源配额上限
// 仅记录用户在项目/虚拟机/进程三个资源维度的数量上限
type UserProjectQuota struct {
ID uint `gorm:"primarykey;column:id" json:"id"`
UserID string `gorm:"type:varchar(64);uniqueIndex;not null;column:user_id" json:"user_id"`
ProjectLimit int `gorm:"not null;default:0;column:project_limit" json:"project_limit"`
CoderVMLimit int `gorm:"not null;default:0;column:coder_vm_limit" json:"coder_vm_limit"`
BrowserVMLimit int `gorm:"not null;default:0;column:browser_vm_limit" json:"browser_vm_limit"`
ProcessLimit int `gorm:"not null;default:0;column:process_limit" json:"process_limit"`
Enabled bool `gorm:"not null;default:true;column:enabled" json:"enabled"`
Description string `gorm:"type:varchar(255);column:description" json:"description"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"deleted_at"`
}
func (UserProjectQuota) TableName() string {
return "user_project_quota"
}

View File

@@ -0,0 +1,327 @@
package notifier
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"text/template"
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/pkg/utils"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
type accountType string
const (
accountTypeMCP accountType = "MCP账号"
accountTypeModel accountType = "模型账号"
)
var (
notifierMu sync.RWMutex
dingTalkService *dingTalkNotifier
)
// Init 初始化通知器
func Init(alertCfg config.AlertConfig, env string, logger *utils.Logger) {
notifierMu.Lock()
defer notifierMu.Unlock()
if alertCfg.DingTalk.TimeoutSeconds <= 0 {
alertCfg.DingTalk.TimeoutSeconds = 5
}
if !alertCfg.DingTalk.Enabled {
logger.Info("DingTalk notifier disabled")
dingTalkService = nil
return
}
service := newDingTalkNotifier(alertCfg.DingTalk, env, logger)
dingTalkService = service
logger.Info("DingTalk notifier initialized",
zap.String("env", env),
zap.String("webhook_hint", maskWebhook(alertCfg.DingTalk.Webhook)))
}
// NotifyMcpLowBalance MCP账号余额不足通知
func NotifyMcpLowBalance(provider, account string, balance, threshold decimal.Decimal) {
notifyLowBalance(accountTypeMCP, provider, account, "", balance, threshold)
}
// NotifyModelLowBalance 模型账号余额不足通知
func NotifyModelLowBalance(provider, account, model string, balance, threshold decimal.Decimal) {
notifyLowBalance(accountTypeModel, provider, account, model, balance, threshold)
}
func notifyLowBalance(accType accountType, provider, account, model string, balance, threshold decimal.Decimal) {
notifierMu.RLock()
service := dingTalkService
notifierMu.RUnlock()
if service == nil {
return
}
if err := service.sendLowBalanceAlert(accType, provider, account, model, balance, threshold); err != nil {
service.logger.Error("发送钉钉余额告警失败", zap.Error(err),
zap.String("account_type", string(accType)),
zap.String("provider", provider),
zap.String("account", account),
zap.String("model", model))
}
}
type dingTalkNotifier struct {
webhook string
secret string
env string
host string
client *http.Client
keyword string
logger *utils.Logger
tpl *template.Template
}
type dingTalkMarkdownPayload struct {
MsgType string `json:"msgtype"`
Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
} `json:"markdown"`
}
type dingTalkResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
const lowBalanceMarkdownTemplate = `[自动生成]
{{.Emoji}} **{{.DisplayTitle}}**
> **级别**: {{.SeverityLabel}}
> **环境**: {{.Environment}}
> **主机**: {{.Host}}
>
> **服务**: {{.Service}}
- **时间**{{.Timestamp}}
- **Provider**{{.Provider}}
- **账号**{{.Account}}
{{- if .Model }}
- **模型**{{.Model}}
{{- end }}
- **当前余额**${{.Balance}}
- **阈值**${{.Threshold}}
`
type lowBalanceTemplateData struct {
Emoji string
DisplayTitle string
SeverityLabel string
Environment string
Host string
Service string
Provider string
Account string
Model string
Balance string
Threshold string
Timestamp string
}
type severityMetaEntry struct {
Emoji string
Label string
}
var (
severityCritical = severityMetaEntry{Emoji: "🚨", Label: "严重告警"}
defaultService = "balance-monitor"
)
func newDingTalkNotifier(cfg config.DingTalkConfig, env string, logger *utils.Logger) *dingTalkNotifier {
timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
if timeout <= 0 {
timeout = 5 * time.Second
}
return &dingTalkNotifier{
webhook: strings.TrimSpace(cfg.Webhook),
secret: strings.TrimSpace(cfg.Secret),
env: strings.TrimSpace(env),
host: detectHost(),
client: &http.Client{
Timeout: timeout,
},
keyword: strings.TrimSpace(cfg.Keyword),
logger: logger,
tpl: template.Must(template.New("lowBalance").Parse(lowBalanceMarkdownTemplate)),
}
}
func (n *dingTalkNotifier) sendLowBalanceAlert(accType accountType, provider, account, model string, balance, threshold decimal.Decimal) error {
if strings.TrimSpace(n.webhook) == "" {
return fmt.Errorf("dingtalk webhook 未配置")
}
accountLabel := strings.TrimSpace(account)
if accountLabel == "" {
accountLabel = "(空)"
}
title := fmt.Sprintf("%s余额告警", accType)
displayTitle := title
if keyword := strings.TrimSpace(n.keyword); keyword != "" {
if !strings.Contains(title, keyword) {
title = fmt.Sprintf("%s %s", keyword, title)
}
if !strings.Contains(displayTitle, keyword) {
displayTitle = fmt.Sprintf("%s %s", keyword, displayTitle)
}
}
data := lowBalanceTemplateData{
Emoji: severityCritical.Emoji,
DisplayTitle: displayTitle,
SeverityLabel: severityCritical.Label,
Environment: valueOrFallback(n.env, "unknown"),
Host: valueOrFallback(n.host, "unknown-host"),
Service: defaultService,
Provider: valueOrFallback(provider, "unknown"),
Account: accountLabel,
Model: strings.TrimSpace(model),
Balance: balance.StringFixed(4),
Threshold: threshold.StringFixed(4),
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
var buf bytes.Buffer
if err := n.tpl.Execute(&buf, data); err != nil {
return fmt.Errorf("渲染模板失败: %w", err)
}
return n.sendMarkdown(context.Background(), title, buf.String())
}
func (n *dingTalkNotifier) sendMarkdown(ctx context.Context, title, markdown string) error {
webhookURL, err := n.webhookWithSignature()
if err != nil {
return fmt.Errorf("生成签名失败: %w", err)
}
payload := dingTalkMarkdownPayload{MsgType: "markdown"}
payload.Markdown.Title = title
payload.Markdown.Text = markdown
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("序列化Payload失败: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := n.client.Do(req)
if err != nil {
return fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
var result dingTalkResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("解析响应失败: %w", err)
}
if resp.StatusCode != http.StatusOK || result.ErrCode != 0 {
return fmt.Errorf("钉钉返回错误: status=%d code=%d msg=%s", resp.StatusCode, result.ErrCode, result.ErrMsg)
}
return nil
}
func (n *dingTalkNotifier) webhookWithSignature() (string, error) {
if strings.TrimSpace(n.secret) == "" {
return n.webhook, nil
}
ts := time.Now().UnixMilli()
strToSign := fmt.Sprintf("%d\n%s", ts, n.secret)
mac := hmac.New(sha256.New, []byte(n.secret))
if _, err := mac.Write([]byte(strToSign)); err != nil {
return "", fmt.Errorf("计算签名失败: %w", err)
}
signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
params := fmt.Sprintf("timestamp=%d&sign=%s", ts, signature)
return appendQuery(n.webhook, params), nil
}
func appendQuery(base, query string) string {
if strings.Contains(base, "?") {
if strings.HasSuffix(base, "?") || strings.HasSuffix(base, "&") {
return base + query
}
return base + "&" + query
}
return base + "?" + query
}
func detectHost() string {
if ip := getLocalIP(); ip != "" {
return ip
}
if name, err := os.Hostname(); err == nil && strings.TrimSpace(name) != "" {
return name
}
return "unknown-host"
}
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ip := ipnet.IP.To4(); ip != nil {
return ip.String()
}
}
}
return ""
}
func maskWebhook(webhook string) string {
trimmed := strings.TrimSpace(webhook)
if trimmed == "" {
return ""
}
if len(trimmed) <= 12 {
return trimmed[:3] + "***"
}
return trimmed[:6] + "..." + trimmed[len(trimmed)-4:]
}
func valueOrFallback(value, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return strings.TrimSpace(value)
}

100
internal/oss/s3.go Normal file
View File

@@ -0,0 +1,100 @@
package oss
import (
"bytes"
"context"
"fmt"
"sync"
"time"
"goalfymax-admin/internal/config"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type s3Client struct {
client *s3.Client
presign *s3.PresignClient
cfg config.OssConfig
}
var (
clientOnce sync.Once
c *s3Client
)
func initClient() error {
var initErr error
clientOnce.Do(func() {
cfg := config.GetConfig().Oss
if cfg.Region == "" || cfg.Bucket == "" || cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" {
initErr = fmt.Errorf("OSS未配置: region/bucket/ak/sk 不能为空")
return
}
awsCfg := aws.Config{
Region: cfg.Region,
Credentials: credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.AccessKeySecret, ""),
}
s3c := s3.NewFromConfig(awsCfg)
c = &s3Client{
client: s3c,
presign: s3.NewPresignClient(s3c),
cfg: cfg,
}
})
return initErr
}
// GetPresignedGetURL 生成S3对象的预签名下载URL
func GetPresignedGetURL(ctx context.Context, key string) (string, error) {
if err := initClient(); err != nil {
return "", err
}
expire := c.cfg.PresignUrlExpire
if expire <= 0 {
expire = 10 * time.Minute
}
req, err := c.presign.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(c.cfg.Bucket),
Key: aws.String(key),
}, func(po *s3.PresignOptions) {
po.Expires = expire
})
if err != nil {
return "", fmt.Errorf("生成预签名URL失败: %w", err)
}
return req.URL, nil
}
// DownloadFileContent 从S3下载文件内容和MIME类型
func DownloadFileContent(ctx context.Context, key string) ([]byte, string, error) {
if err := initClient(); err != nil {
return nil, "", err
}
resp, err := c.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(c.cfg.Bucket),
Key: aws.String(key),
})
if err != nil {
return nil, "", fmt.Errorf("从S3下载文件失败: %w", err)
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return nil, "", fmt.Errorf("读取文件内容失败: %w", err)
}
mimeType := "application/octet-stream" // 默认MIME类型
if resp.ContentType != nil && *resp.ContentType != "" {
mimeType = *resp.ContentType
}
return buf.Bytes(), mimeType, nil
}

170
internal/services/README.md Normal file
View File

@@ -0,0 +1,170 @@
# 服务层
本模块负责业务逻辑的实现,提供各种业务服务。
## 功能特性
- 用户管理服务
- 角色管理服务
- 菜单管理服务
- 日志管理服务
- 统一的错误处理
- 业务逻辑封装
## 模块结构
```
services/
├── user_service.go # 用户服务
├── role_service.go # 角色服务
├── menu_service.go # 菜单服务
├── log_service.go # 日志服务
└── README.md # 说明文档
```
## 服务接口
### 用户服务 (UserService)
```go
type UserService interface {
Login(req *models.LoginRequest) (*models.LoginResponse, error)
Create(req *models.UserCreateRequest) (*models.User, error)
GetByID(id uint) (*models.User, error)
Update(id uint, req *models.UserUpdateRequest) (*models.User, error)
Delete(id uint) error
List(req *models.UserListRequest) ([]models.User, int64, error)
ChangePassword(id uint, req *models.UserChangePasswordRequest) error
UpdateStatus(id uint, status int) error
}
```
### 角色服务 (RoleService)
```go
type RoleService interface {
Create(req *models.RoleCreateRequest) (*models.Role, error)
GetByID(id uint) (*models.Role, error)
Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error)
Delete(id uint) error
List(req *models.RoleListRequest) ([]models.Role, int64, error)
UpdateStatus(id uint, status int) error
}
```
### 菜单服务 (MenuService)
```go
type MenuService interface {
Create(req *models.MenuCreateRequest) (*models.Menu, error)
GetByID(id uint) (*models.Menu, error)
Update(id uint, req *models.MenuUpdateRequest) (*models.Menu, error)
Delete(id uint) error
List(req *models.MenuListRequest) ([]models.Menu, int64, error)
GetTree() ([]models.Menu, error)
UpdateStatus(id uint, status int) error
UpdateSort(id uint, sort int) error
}
```
### 日志服务 (LogService)
```go
type LogService interface {
CreateLoginLog(log *models.LoginLog) error
CreateOperationLog(log *models.OperationLog) error
GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error)
GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error)
DeleteLoginLogs(beforeDate string) error
DeleteOperationLogs(beforeDate string) error
}
```
## 使用方法
### 创建服务实例
```go
// 创建用户服务
userService := services.NewUserService(
storage.NewUserStorage(),
utils.NewJWTManager("secret-key"),
logger,
)
// 创建角色服务
roleService := services.NewRoleService(
storage.NewRoleStorage(),
logger,
)
// 创建菜单服务
menuService := services.NewMenuService(
storage.NewMenuStorage(),
logger,
)
// 创建日志服务
logService := services.NewLogService(
storage.NewLogStorage(),
logger,
)
```
### 使用服务
```go
// 用户登录
loginResp, err := userService.Login(&models.LoginRequest{
Username: "admin",
Password: "password",
})
// 创建用户
user, err := userService.Create(&models.UserCreateRequest{
Username: "newuser",
Email: "newuser@example.com",
Password: "password",
Role: "user",
})
// 获取用户列表
users, total, err := userService.List(&models.UserListRequest{
PageRequest: models.PageRequest{Page: 1, Size: 10},
Username: "admin",
})
// 获取菜单树
menus, err := menuService.GetTree()
// 记录操作日志
err := logService.CreateOperationLog(&models.OperationLog{
UserID: 1,
Username: "admin",
Module: "user",
Operation: "create",
Method: "POST",
Path: "/api/users",
IP: "127.0.0.1",
Status: 1,
})
```
## 错误处理
所有服务都遵循统一的错误处理模式:
- 业务逻辑错误返回具体的错误信息
- 数据库错误记录日志并返回通用错误信息
- 参数验证错误返回具体的验证信息
## 日志记录
服务层会自动记录关键操作的日志:
- 用户登录/登出
- 用户创建/更新/删除
- 角色创建/更新/删除
- 菜单创建/更新/删除
- 系统操作日志

View File

@@ -0,0 +1,70 @@
package services
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"time"
)
// AuditLogService 审计日志服务接口
type AuditLogService interface {
Create(req *models.CreateAuditLogRequest) error
GetByID(id uint) (*models.AuditLog, error)
List(req *models.AuditLogListRequest) (*models.AuditLogListResponse, error)
}
type auditLogService struct {
storage storage.AuditLogStorage
}
// NewAuditLogService 创建审计日志服务实例
func NewAuditLogService(storage storage.AuditLogStorage) AuditLogService {
return &auditLogService{
storage: storage,
}
}
// Create 创建审计日志
func (s *auditLogService) Create(req *models.CreateAuditLogRequest) error {
log := &models.AuditLog{
OperationType: req.OperationType,
OperationTime: time.Now(),
OperatorID: req.OperatorID,
OperatorEmail: req.OperatorEmail,
TargetType: req.TargetType,
TargetID: req.TargetID,
TargetEmail: req.TargetEmail,
OperationDetails: req.OperationDetails,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
Status: req.Status,
ErrorMessage: req.ErrorMessage,
}
// 设置默认状态
if log.Status == "" {
log.Status = models.AuditLogStatusSuccess
}
return s.storage.Create(log)
}
// GetByID 根据ID获取审计日志
func (s *auditLogService) GetByID(id uint) (*models.AuditLog, error) {
return s.storage.GetByID(id)
}
// List 查询审计日志列表
func (s *auditLogService) List(req *models.AuditLogListRequest) (*models.AuditLogListResponse, error) {
logs, total, err := s.storage.List(req)
if err != nil {
return nil, err
}
return &models.AuditLogListResponse{
List: logs,
Total: total,
Page: req.Page,
Size: req.Size,
}, nil
}

View File

@@ -0,0 +1,443 @@
package services
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"math"
"net/smtp"
"time"
"github.com/jordan-wright/email"
"goalfymax-admin/internal/config"
)
type EmailService struct {
host string
port int
username string
password string
sender string
inviteURLPrefix string
}
// NewEmailService 创建邮件服务实例
func NewEmailService() *EmailService {
cfg := config.GetConfig()
return &EmailService{
host: cfg.Email.Host,
port: cfg.Email.Port,
username: cfg.Email.Username,
password: cfg.Email.Password,
sender: cfg.Email.Sender,
inviteURLPrefix: cfg.Email.InviteURLPrefix,
}
}
// SendInviteCodeApprovalEmail 发送邀请码审批通过邮件
func (s *EmailService) SendInviteCodeApprovalEmail(toEmail, inviteCode, language string, expiresAt *time.Time) error {
var subject string
if language == "en" {
subject = "Your GoalfyAI Beta Access Invitation"
} else {
subject = "GoalfyAI 内测邀请函"
}
// 构造邮件内容
htmlContent := s.generateApprovalEmailHTML(inviteCode, language, expiresAt)
return s.sendEmail(toEmail, subject, htmlContent)
}
// SendInviteCodeRejectionEmail 发送邀请码申请拒绝邮件
func (s *EmailService) SendInviteCodeRejectionEmail(toEmail, rejectReason string) error {
subject := "关于您的 GoalfyAI 申请"
// 构造邮件内容
htmlContent := s.generateRejectionEmailHTML(rejectReason)
return s.sendEmail(toEmail, subject, htmlContent)
}
// generateApprovalEmailHTML 生成审批通过的邮件HTML
func (s *EmailService) generateApprovalEmailHTML(inviteCode, language string, expiresAt *time.Time) string {
if language == "en" {
return s.GenerateApprovalEmailEN(inviteCode, expiresAt)
}
return s.GenerateApprovalEmailZH(inviteCode, expiresAt)
}
// formatExpiryTimeEN 格式化过期时间为英文显示(全部显示为小时,向上取整)
func formatExpiryTimeEN(expiresAt *time.Time) string {
if expiresAt == nil {
return "until used"
}
now := time.Now()
if expiresAt.Before(now) {
return "expired"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
if hours == 1 {
return "1 hour"
}
return fmt.Sprintf("%d hours", hours)
}
// formatExpiryTimeZH 格式化过期时间为中文显示(全部显示为小时,向上取整)
func formatExpiryTimeZH(expiresAt *time.Time) string {
if expiresAt == nil {
return "永久有效"
}
now := time.Now()
if expiresAt.Before(now) {
return "已过期"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
return fmt.Sprintf("%d小时", hours)
}
// GenerateApprovalEmailEN 生成英文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailEN(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeEN(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>Thank you again for your interest in GoalfyAI!</p>
<p>We're excited to let you know that your request for beta access has been approved.<br>
You can now activate your GoalfyAI account using the link below:</p>
<p>👉 <a href="{{.InviteURL}}">Activate Your Account</a><br>
<span style="color: #666; font-size: 14px;">(This link is valid for {{.ExpiryHours}})</span></p>
<p>With this invite, you'll be among the first to explore our intelligent task execution system—designed for long-range, professional workflows. We'd love to hear your feedback as we continue to refine the experience.</p>
<p>Need help getting started? Visit our website for tips, use cases, and product updates:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>Thanks again for joining us on this journey.<br>
Let's build the future of intelligent tasks—together.</p>
<p>Warm regards,<br>
The GoalfyAI Team</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
This email is sent automatically. Please do not reply.<br>
For any questions, please contact <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_en").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// GenerateApprovalEmailZH 生成中文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailZH(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeZH(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>感谢您对 GoalfyAI 的关注与支持!</p>
<p>我们很高兴通知您,您的内测申请已通过审核。<br>
请通过以下链接激活您的 GoalfyAI 账户:</p>
<p>👉 <a href="{{.InviteURL}}">点击激活账户</a><br>
<span style="color: #666; font-size: 14px;">(该链接在 {{.ExpiryHours}} 内有效)</span></p>
<p>通过本次邀请,您将率先体验我们为长周期专业任务打造的智能任务系统。我们也非常欢迎您在使用过程中给予反馈,帮助我们持续优化产品体验。</p>
<p>如需了解更多使用建议、典型场景或最新进展,欢迎访问官网:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>感谢您的加入,<br>
让我们一同开启智能任务的新篇章!</p>
<p>此致,<br>
GoalfyAI 团队</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
本邮件为自动化发送,请勿回复。<br>
如有疑问请联系 <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_zh").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// generateRejectionEmailHTML 生成申请拒绝的邮件HTML
func (s *EmailService) generateRejectionEmailHTML(rejectReason string) string {
if rejectReason == "" {
rejectReason = "感谢您对 GoalfyAI 的关注。经过审核,您的账户申请暂未通过。"
}
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #f5f5f5;
padding: 40px 20px;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header {
background-color: #000000;
padding: 32px 40px;
text-align: center;
}
.header-logo {
font-size: 28px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.5px;
}
.content {
padding: 40px;
}
.greeting {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 24px;
}
.message {
font-size: 15px;
color: #4a4a4a;
margin-bottom: 24px;
line-height: 1.7;
}
.reason-box {
background-color: #fafafa;
border-left: 3px solid #4a4a4a;
padding: 20px 24px;
margin: 24px 0;
border-radius: 0 4px 4px 0;
}
.reason-box p {
font-size: 15px;
color: #1a1a1a;
line-height: 1.7;
}
.support-box {
background-color: #fafafa;
border-radius: 6px;
padding: 20px 24px;
margin-top: 32px;
text-align: center;
}
.support-box p {
font-size: 14px;
color: #4a4a4a;
margin: 0;
}
.support-box a {
color: #000000;
text-decoration: none;
font-weight: 600;
}
.footer {
background-color: #fafafa;
padding: 32px 40px;
text-align: center;
border-top: 1px solid #e5e5e5;
}
.footer-brand {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 8px;
}
.footer-contact {
font-size: 13px;
color: #666666;
margin-top: 8px;
}
.footer-contact a {
color: #000000;
text-decoration: none;
}
@media only screen and (max-width: 600px) {
.content {
padding: 24px 20px;
}
.header {
padding: 24px 20px;
}
.reason-box {
padding: 16px 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="header">
<div class="header-logo">GoalfyAI</div>
</div>
<div class="content">
<div class="greeting">您好,</div>
<div class="message">
感谢您对 GoalfyAI 的关注和申请。
</div>
<div class="reason-box">
<p>{{.RejectReason}}</p>
</div>
<div class="message" style="margin-top: 24px;">
我们期待未来有机会为您提供服务。
</div>
<div class="support-box">
<p>如有任何疑问,欢迎联系我们</p>
<p style="margin-top: 8px;">
<a href="mailto:goalfymax@goalfyai.com">goalfymax@goalfyai.com</a>
</p>
</div>
</div>
<div class="footer">
<div class="footer-brand">GoalfyAI 团队</div>
<div class="footer-contact">
© 2025 GoalfyAI. All rights reserved.
</div>
</div>
</div>
</body>
</html>
`
tmpl, _ := template.New("rejection").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"RejectReason": rejectReason,
})
return buf.String()
}
// sendEmail 发送邮件的通用方法
func (s *EmailService) sendEmail(toEmail, subject, htmlContent string) error {
e := email.NewEmail()
e.From = s.sender
e.To = []string{toEmail}
e.Subject = subject
e.HTML = []byte(htmlContent)
// 创建SMTP认证
auth := smtp.PlainAuth("", s.username, s.password, s.host)
// 配置TLS
tlsConfig := &tls.Config{
ServerName: s.host,
}
// 发送邮件
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 如果是465端口使用SSL
if s.port == 465 {
return e.SendWithTLS(addr, auth, tlsConfig)
}
// 否则使用STARTTLS
return e.SendWithStartTLS(addr, auth, tlsConfig)
}
// SendBatchEmails 批量发送邮件(异步)
func (s *EmailService) SendBatchEmails(emails []string, subject, htmlContent string) []error {
errors := make([]error, len(emails))
for i, email := range emails {
errors[i] = s.sendEmail(email, subject, htmlContent)
// 避免发送过快
time.Sleep(100 * time.Millisecond)
}
return errors
}
// TestConnection 测试邮件服务器连接
func (s *EmailService) TestConnection() error {
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 尝试连接SMTP服务器
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer client.Close()
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,321 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"goalfymax-admin/pkg/utils"
"io"
"net/http"
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"go.uber.org/zap"
)
// GatewayClient 网关客户端
type GatewayClient struct {
baseURL string
timeout time.Duration
logger *utils.Logger
token string
}
// NewGatewayClient 创建网关客户端
func NewGatewayClient(baseURL string, timeout time.Duration, logger *utils.Logger) *GatewayClient {
return &GatewayClient{
baseURL: baseURL,
timeout: timeout,
logger: logger,
}
}
// acquireToken 从配置的登录接口获取新的网关 token不做过期判定
func (c *GatewayClient) acquireToken() (string, error) {
return "admin_control_0807", nil
cfg := config.GetConfig()
loginURL := cfg.Gateway.Auth.LoginURL
key := cfg.Gateway.Auth.Key
payload, _ := json.Marshal(map[string]string{"key": key})
req, err := http.NewRequest("POST", loginURL, bytes.NewBuffer(payload))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: c.timeout}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login status: %d %s", resp.StatusCode, string(body))
}
var out struct {
Success bool `json:"success"`
Token string `json:"token"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
if !out.Success || out.Token == "" {
return "", fmt.Errorf("login failed: %s", string(body))
}
c.logger.Info("login succeeded", zap.String("token", out.Token))
c.token = out.Token
return c.token, nil
}
// doWithAuth 发送请求自动注入token若401则重取token并重试一次
func (c *GatewayClient) doWithAuth(req *http.Request) (*http.Response, error) {
if c.token == "" {
var err error
if c.token, err = c.acquireToken(); err != nil {
return nil, err
}
}
req.Header.Set("Authorization", "Bearer "+c.token)
client := &http.Client{Timeout: c.timeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
// 读尽响应体以复用连接
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if _, err := c.acquireToken(); err != nil {
return nil, err
}
// 重试一次
// 重新构建请求体仅当是可重读的bytes.Buffer这里假设上层构造的Body为bytes.Buffer或nil
// 如果是一次性流,上层应改为传入可重读体
if req.GetBody != nil {
bodyRc, _ := req.GetBody()
req.Body = bodyRc
}
req.Header.Set("Authorization", "Bearer "+c.token)
return client.Do(req)
}
return resp, nil
}
// GetQuotaHistory 获取配额历史数据
func (c *GatewayClient) GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error) {
// 构建请求URL
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/history", c.baseURL)
// 序列化请求数据
jsonData, err := json.Marshal(req)
if err != nil {
c.logger.Error("序列化请求数据失败", zap.Error(err))
return nil, fmt.Errorf("序列化请求数据失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
c.logger.Error("创建HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
// 发送请求
c.logger.Debug("发送配额历史查询请求",
zap.String("url", url),
zap.String("data", string(jsonData)),
)
resp, err := c.doWithAuth(httpReq)
if err != nil {
c.logger.Error("发送HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("读取响应数据失败", zap.Error(err))
return nil, fmt.Errorf("读取响应数据失败: %w", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关返回错误状态码",
zap.Int("status_code", resp.StatusCode),
zap.String("response", string(body)),
)
return nil, fmt.Errorf("网关返回错误状态码: %d", resp.StatusCode)
}
// 解析响应
var response models.QuotaHistoryResponse
if err := json.Unmarshal(body, &response); err != nil {
c.logger.Error("解析响应数据失败", zap.Error(err))
return nil, fmt.Errorf("解析响应数据失败: %w", err)
}
c.logger.Info("配额历史查询成功",
zap.Int("data_count", len(response.Data)),
zap.Bool("success", response.Success),
)
return &response, nil
}
// GetQuotaRules 获取配额规则列表代理到网关携带Authorization
func (c *GatewayClient) GetQuotaRules(authToken string) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules", c.baseURL)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
c.logger.Error("创建HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
httpReq.Header.Set("Accept", "application/json")
if authToken != "" {
httpReq.Header.Set("Authorization", authToken)
}
c.logger.Debug("请求配额规则列表", zap.String("url", url))
resp, err := c.doWithAuth(httpReq)
if err != nil {
c.logger.Error("发送HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("读取响应数据失败", zap.Error(err))
return nil, fmt.Errorf("读取响应数据失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关返回错误状态码", zap.Int("status_code", resp.StatusCode), zap.String("response", string(body)))
return nil, fmt.Errorf("网关返回错误状态码: %d", resp.StatusCode)
}
var response models.QuotaRulesResponse
if err := json.Unmarshal(body, &response); err != nil {
c.logger.Error("解析响应数据失败", zap.Error(err))
return nil, fmt.Errorf("解析响应数据失败: %w", err)
}
c.logger.Info("获取配额规则成功")
return &response, nil
}
// CreateQuotaRule 创建配额规则(代理网关)
func (c *GatewayClient) CreateQuotaRule(authToken string, body any) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules", c.baseURL)
payload, _ := json.Marshal(body)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// UpdateQuotaRule 更新配额规则(代理网关)
func (c *GatewayClient) UpdateQuotaRule(authToken string, id string, body any) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules/%s", c.baseURL, id)
payload, _ := json.Marshal(body)
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteQuotaRule 删除配额规则(代理网关)
func (c *GatewayClient) DeleteQuotaRule(authToken string, id string) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules/%s", c.baseURL, id)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return nil, err
}
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// HealthCheck 健康检查
func (c *GatewayClient) HealthCheck() error {
url := fmt.Sprintf("%s/aigateway-admin/health", c.baseURL)
client := &http.Client{
Timeout: c.timeout,
}
resp, err := client.Get(url)
if err != nil {
c.logger.Error("网关健康检查失败", zap.Error(err))
return fmt.Errorf("网关健康检查失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关健康检查返回错误状态码", zap.Int("status_code", resp.StatusCode))
return fmt.Errorf("网关健康检查返回错误状态码: %d", resp.StatusCode)
}
c.logger.Info("网关健康检查成功")
return nil
}

View File

@@ -0,0 +1,512 @@
package services
import (
"context"
"errors"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/redis"
"goalfymax-admin/pkg/utils"
"strconv"
goredis "github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
type GoalfyMaxUserService interface {
List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error)
GetByID(id uint) (*models.GoalfyMaxUser, error)
Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error)
Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error)
Delete(id uint) error
Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error
Unban(id uint) error
AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
}
type goalfyMaxUserService struct {
storage storage.GoalfyMaxUserStorage
messagePushService MessagePushService
ssoAdminService SSOAdminService
redisClient *redis.Client
balanceOperationLogStorage storage.BalanceOperationLogStorage
auditLogService AuditLogService
logger *utils.Logger
}
func NewGoalfyMaxUserService(s storage.GoalfyMaxUserStorage, messagePushService MessagePushService, ssoAdminService SSOAdminService, redisClient *redis.Client, balanceOperationLogStorage storage.BalanceOperationLogStorage, auditLogService AuditLogService, logger *utils.Logger) GoalfyMaxUserService {
return &goalfyMaxUserService{
storage: s,
messagePushService: messagePushService,
ssoAdminService: ssoAdminService,
redisClient: redisClient,
balanceOperationLogStorage: balanceOperationLogStorage,
auditLogService: auditLogService,
logger: logger,
}
}
func (s *goalfyMaxUserService) List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error) {
users, total, err := s.storage.List(req)
if err != nil {
return nil, 0, err
}
// 如果Redis客户端可用查询每个用户的余额
if s.redisClient != nil {
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
for i := range users {
// 默认余额为0
balanceUSD := 0.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", users[i].UserID)
// 从Redis获取余额
balanceStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
// 解析余额值
balanceValue, err := strconv.ParseInt(balanceStr, 10, 64)
if err == nil {
// 转换为美元除以100000000
balanceUSD = float64(balanceValue) / balanceMultiplier
}
}
// 无论是否查询成功都设置余额查询失败则为0
users[i].Balance = &balanceUSD
}
} else {
// 如果Redis客户端不可用设置所有用户余额为0
zeroBalance := 0.0
for i := range users {
users[i].Balance = &zeroBalance
}
}
return users, total, nil
}
func (s *goalfyMaxUserService) GetByID(id uint) (*models.GoalfyMaxUser, error) {
return s.storage.GetByID(id)
}
func (s *goalfyMaxUserService) Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error) {
// 检查用户名是否已存在
_, err := s.storage.GetByUsername(req.Username)
if err == nil {
return nil, errors.New("用户名已存在")
}
// 检查邮箱是否已存在
_, err = s.storage.GetByEmail(req.Email)
if err == nil {
return nil, errors.New("邮箱已存在")
}
// 1. 先调用SSO创建用户
ctx := context.Background()
ssoReq := &SSOAdminUserCreateRequest{
Username: req.Username,
Email: req.Email,
Phone: "", // 默认空手机号,可以根据需要修改
Password: req.Password, // 使用用户输入的密码
}
ssoUser, err := s.ssoAdminService.CreateUser(ctx, ssoReq)
if err != nil {
s.logger.Error("create goalfymax user failed", zap.Error(err))
return nil, fmt.Errorf("SSO创建用户失败: %w", err)
}
// 2. 设置系统角色:默认 custom而非固定角色ID
if err := s.ssoAdminService.SetSystemRole(ctx, ssoUser.ID, "custom"); err != nil {
// 如果设置失败,记录错误但不阻止用户创建
if s.logger != nil {
s.logger.Error("设置系统角色失败", zap.Int("user_id", ssoUser.ID), zap.Error(err))
}
}
// 3. 在本地数据库创建用户记录使用SSO返回的用户ID
// 如果未指定用户等级,默认为 normal
userLevelCode := req.UserLevelCode
if userLevelCode == "" {
userLevelCode = "normal"
}
// 如果未指定版本,默认为 1用户版
version := req.Version
if version == 0 {
version = 1
}
user := &models.GoalfyMaxUser{
UserID: ssoUser.ID, // 使用SSO返回的用户ID
Username: req.Username,
Email: req.Email,
Nickname: req.Nickname,
Avatar: req.Avatar,
UserLevelCode: userLevelCode,
Version: version,
IsBanned: false,
}
if err := s.storage.Create(user); err != nil {
// 如果本地创建失败需要清理SSO用户这里简化处理实际应该调用SSO删除接口
if s.logger != nil {
s.logger.Warn("本地用户创建失败但SSO用户已创建", zap.Int("sso_user_id", ssoUser.ID), zap.Error(err))
}
return nil, fmt.Errorf("创建用户失败: %w", err)
}
return user, nil
}
func (s *goalfyMaxUserService) Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error) {
user, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("用户不存在")
}
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
user.Email = req.Email
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.UserLevelCode != "" {
user.UserLevelCode = req.UserLevelCode
}
if req.Version != nil {
user.Version = *req.Version
}
if req.GoalfyHubPermission != nil {
user.GoalfyHubPermission = *req.GoalfyHubPermission
}
if err := s.storage.Update(user); err != nil {
return nil, err
}
return user, nil
}
func (s *goalfyMaxUserService) Delete(id uint) error {
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
return s.storage.Delete(id)
}
func (s *goalfyMaxUserService) Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行封禁操作
if err := s.storage.SetBanned(id, req.Reason, adminID); err != nil {
return err
}
// 发送封禁通知
ctx := context.Background()
banMessage := &models.MessagePushRequest{
Title: "封禁通知",
Content: fmt.Sprintf("您的账户已被封禁。封禁原因:%s。如有疑问请联系客服。", req.Reason),
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞封禁操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, banMessage, adminID, "系统管理员"); err != nil {
// 记录错误日志,但不影响封禁操作
if s.logger != nil {
s.logger.Error("发送封禁通知失败", zap.Int("user_id", user.UserID), zap.Int("admin_id", adminID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) Unban(id uint) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行解封操作
if err := s.storage.Unban(id); err != nil {
return err
}
// 发送解封通知
ctx := context.Background()
unbanMessage := &models.MessagePushRequest{
Title: "解封通知",
Content: "您的账户已被解封,现在可以正常使用所有功能。感谢您的理解与配合。",
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞解封操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, unbanMessage, 0, "系统管理员"); err != nil {
// 记录错误日志,但不影响解封操作
if s.logger != nil {
s.logger.Error("发送解封通知失败", zap.Int("user_id", user.UserID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法增加余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 获取操作前余额
balanceBeforeUSD := 0.0
balanceBeforeStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
balanceBeforeValue, err := strconv.ParseInt(balanceBeforeStr, 10, 64)
if err == nil {
balanceBeforeUSD = float64(balanceBeforeValue) / balanceMultiplier
}
}
// 将美元金额转换为Redis存储的数值
amountToAdd := int64(req.Amount * balanceMultiplier)
// 使用Redis的INCRBY命令增加余额如果key不存在会自动创建并设置为0然后加上amountToAdd
newBalance, err := s.redisClient.Rdb.IncrBy(ctx, redisKey, amountToAdd).Result()
if err != nil {
return fmt.Errorf("增加余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeAdd,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeAdd),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "recharge",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额增加成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}
func (s *goalfyMaxUserService) DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法减少余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 将美元金额转换为Redis存储的数值
amountToDeduct := int64(req.Amount * balanceMultiplier)
// 先获取当前余额,检查是否足够
currentBalance, err := s.redisClient.Rdb.Get(ctx, redisKey).Int64()
if err != nil {
// 如果key不存在表示余额为0不能减少
if err == goredis.Nil {
return errors.New("余额不足,无法减少")
}
return fmt.Errorf("查询余额失败: %w", err)
}
// 计算操作前余额
balanceBeforeUSD := float64(currentBalance) / balanceMultiplier
// 检查余额是否足够
if currentBalance < amountToDeduct {
return errors.New("余额不足,无法减少")
}
// 使用Redis的DECRBY命令减少余额
newBalance, err := s.redisClient.Rdb.DecrBy(ctx, redisKey, amountToDeduct).Result()
if err != nil {
return fmt.Errorf("减少余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeDeduct,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeDeduct),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "deduct",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额减少成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}

View File

@@ -0,0 +1,310 @@
package services
import (
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"goalfymax-admin/internal/models"
)
type InviteCodeApplicationService struct {
db *gorm.DB
emailService *EmailService
}
// NewInviteCodeApplicationService 创建邀请码申请服务
func NewInviteCodeApplicationService(db *gorm.DB) *InviteCodeApplicationService {
return &InviteCodeApplicationService{
db: db,
emailService: NewEmailService(),
}
}
// SubmitApplication 提交邀请码申请(官网使用)
func (s *InviteCodeApplicationService) SubmitApplication(req *models.InviteCodeApplicationCreateRequest) (*models.InviteCodeApplication, error) {
// 检查是否已经有待处理或已通过的申请
var existingApp models.InviteCodeApplication
err := s.db.Where("email = ? AND status IN (?, ?)",
req.Email,
models.ApplicationStatusPending,
models.ApplicationStatusApproved,
).First(&existingApp).Error
if err == nil {
// 如果找到了记录,说明已经有申请
if existingApp.Status == models.ApplicationStatusPending {
return nil, errors.New("您已经提交过申请,请等待审核")
}
if existingApp.Status == models.ApplicationStatusApproved {
return nil, errors.New("您的申请已通过,请检查邮箱")
}
}
// 设置默认语言
language := req.Language
if language == "" {
language = "zh"
}
// 创建新的申请
application := &models.InviteCodeApplication{
Email: req.Email,
Reason: req.Reason,
Language: language,
Status: models.ApplicationStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.Create(application).Error; err != nil {
return nil, fmt.Errorf("创建申请失败: %w", err)
}
return application, nil
}
// GetApplicationList 获取申请列表(后台管理使用)
func (s *InviteCodeApplicationService) GetApplicationList(req *models.InviteCodeApplicationListRequest) (*models.InviteCodeApplicationListResponse, error) {
var applications []models.InviteCodeApplication
var total int64
query := s.db.Model(&models.InviteCodeApplication{})
// 添加过滤条件
if req.Email != "" {
query = query.Where("email LIKE ?", "%"+req.Email+"%")
}
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
endTime, _ := time.Parse("2006-01-02", req.EndTime)
query = query.Where("created_at < ?", endTime.AddDate(0, 0, 1))
}
// 获取总数
query.Count(&total)
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.
Preload("InviteCode").
Order("created_at DESC").
Limit(req.Size).
Offset(offset).
Find(&applications).Error
if err != nil {
return nil, fmt.Errorf("查询申请列表失败: %w", err)
}
return &models.InviteCodeApplicationListResponse{
List: applications,
Total: total,
}, nil
}
// GetStatistics 获取申请统计
func (s *InviteCodeApplicationService) GetStatistics() (*models.InviteCodeApplicationStatistics, error) {
var stats models.InviteCodeApplicationStatistics
// 待处理数量
var pendingCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusPending).
Count(&pendingCount)
stats.TotalPending = int(pendingCount)
// 已通过数量
var approvedCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusApproved).
Count(&approvedCount)
stats.TotalApproved = int(approvedCount)
// 已拒绝数量
var rejectedCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusRejected).
Count(&rejectedCount)
stats.TotalRejected = int(rejectedCount)
// 今日申请数量
var todayCount int64
todayStart := time.Now().Truncate(24 * time.Hour)
s.db.Model(&models.InviteCodeApplication{}).
Where("created_at >= ?", todayStart).
Count(&todayCount)
stats.TodayApplied = int(todayCount)
return &stats, nil
}
// ApproveApplication 审批通过申请
func (s *InviteCodeApplicationService) ApproveApplication(req *models.InviteCodeApplicationApproveRequest, approvedBy string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 获取申请信息
var application models.InviteCodeApplication
if err := tx.First(&application, req.ApplicationID).Error; err != nil {
return fmt.Errorf("申请不存在: %w", err)
}
if application.Status != models.ApplicationStatusPending {
return errors.New("申请状态不是待处理,无法审批")
}
// 设置有效期默认3天72小时
validDays := req.ValidDays
if validDays <= 0 {
validDays = 3
}
expiresAt := time.Now().AddDate(0, 0, validDays)
// 创建邀请码
inviteCode := &models.InviteCode{
Code: s.generateInviteCode(),
IsUsed: false,
ClientID: "xRpT9mgNpt2YvoY9z4FToA", // 默认为正式版客户端
ExpiresAt: &expiresAt,
CreatedAt: time.Now(),
}
if err := tx.Create(inviteCode).Error; err != nil {
return fmt.Errorf("创建邀请码失败: %w", err)
}
// 更新申请状态
now := time.Now()
updates := map[string]interface{}{
"status": models.ApplicationStatusApproved,
"invite_code_id": inviteCode.ID,
"approved_at": now,
"approved_by": approvedBy,
"updated_at": now,
}
if err := tx.Model(&application).Updates(updates).Error; err != nil {
return fmt.Errorf("更新申请状态失败: %w", err)
}
// 发送邮件(异步,不影响事务)
go func() {
// 获取语言设置,默认为中文
lang := application.Language
if lang == "" {
lang = "zh"
}
if err := s.emailService.SendInviteCodeApprovalEmail(application.Email, inviteCode.Code, lang, &expiresAt); err != nil {
// 记录邮件发送失败,但不回滚事务
fmt.Printf("发送审批通过邮件失败: %v\n", err)
} else {
// 更新邮件发送时间
emailSentAt := time.Now()
tx.Model(&application).Update("email_sent_at", emailSentAt)
}
}()
return nil
})
}
// RejectApplication 审批拒绝申请
func (s *InviteCodeApplicationService) RejectApplication(req *models.InviteCodeApplicationRejectRequest, approvedBy string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 获取申请信息
var application models.InviteCodeApplication
if err := tx.First(&application, req.ApplicationID).Error; err != nil {
return fmt.Errorf("申请不存在: %w", err)
}
if application.Status != models.ApplicationStatusPending {
return errors.New("申请状态不是待处理,无法审批")
}
// 更新申请状态
now := time.Now()
updates := map[string]interface{}{
"status": models.ApplicationStatusRejected,
"reject_reason": req.RejectReason,
"approved_at": now,
"approved_by": approvedBy,
"updated_at": now,
}
if err := tx.Model(&application).Updates(updates).Error; err != nil {
return fmt.Errorf("更新申请状态失败: %w", err)
}
// 拒绝操作不发送邮件通知
return nil
})
}
// BatchApproveApplications 批量审批通过
func (s *InviteCodeApplicationService) BatchApproveApplications(req *models.InviteCodeApplicationBatchApproveRequest, approvedBy string) error {
for _, appID := range req.ApplicationIDs {
approveReq := &models.InviteCodeApplicationApproveRequest{
ApplicationID: appID,
ValidDays: req.ValidDays,
}
if err := s.ApproveApplication(approveReq, approvedBy); err != nil {
// 记录错误但继续处理其他申请
fmt.Printf("审批申请 %d 失败: %v\n", appID, err)
}
}
return nil
}
// BatchRejectApplications 批量审批拒绝
func (s *InviteCodeApplicationService) BatchRejectApplications(req *models.InviteCodeApplicationBatchRejectRequest, approvedBy string) error {
for _, appID := range req.ApplicationIDs {
rejectReq := &models.InviteCodeApplicationRejectRequest{
ApplicationID: appID,
RejectReason: req.RejectReason,
}
if err := s.RejectApplication(rejectReq, approvedBy); err != nil {
// 记录错误但继续处理其他申请
fmt.Printf("拒绝申请 %d 失败: %v\n", appID, err)
}
}
return nil
}
// generateInviteCode 生成唯一的邀请码格式GFY-XXXXXXXX-XXXX
func (s *InviteCodeApplicationService) generateInviteCode() string {
// 生成UUID并转换为大写
uuidStr := uuid.New().String()
// 移除UUID中的连字符并转为大写
cleaned := ""
for _, c := range uuidStr {
if c != '-' {
cleaned += string(c)
}
}
cleaned = strings.ToUpper(cleaned)
// 截取需要的部分GFY-8位-4位
// 格式GFY-XXXXXXXX-XXXX
part1 := cleaned[0:8] // 8位
part2 := cleaned[8:12] // 4位
return fmt.Sprintf("GFY-%s-%s", part1, part2)
}
// GetPendingApplicationsCount 获取待处理申请数量
func (s *InviteCodeApplicationService) GetPendingApplicationsCount() (int64, error) {
var count int64
err := s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusPending).
Count(&count).Error
return count, err
}

View File

@@ -0,0 +1,291 @@
package services
import (
"fmt"
"math/rand"
"strings"
"time"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
)
type InviteCodeService interface {
Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error)
GetByID(id uint) (*models.InviteCode, error)
GetByCode(code string) (*models.InviteCode, error)
List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error)
Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error)
Delete(id uint) error
GetStatistics() (*models.InviteCodeStatistics, error)
MarkAsUsed(code string) error
ValidateInviteCode(code string) error
}
type inviteCodeService struct {
storage storage.InviteCodeStorage
emailService *EmailService
}
func NewInviteCodeService(storage storage.InviteCodeStorage) InviteCodeService {
return &inviteCodeService{
storage: storage,
emailService: NewEmailService(),
}
}
func (s *inviteCodeService) Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error) {
if req == nil {
return nil, fmt.Errorf("请求参数不能为空")
}
// 设置过期时间
var expiresAt *time.Time
if req.ExpiresAt != nil {
expiresAt = req.ExpiresAt
} else {
// 默认30天后过期
defaultExpiry := time.Now().AddDate(0, 0, 30)
expiresAt = &defaultExpiry
}
// 设置用户等级ID如果未提供则默认为1
var userLevelID *uint
if req.UserLevelID != nil {
userLevelID = req.UserLevelID
} else {
defaultUserLevelID := uint(1)
userLevelID = &defaultUserLevelID
}
// 如果提供了邮箱列表,为每个邮箱创建一个邀请码
if len(req.Emails) > 0 {
// 对邮箱列表进行去重处理
emailMap := make(map[string]bool)
uniqueEmails := make([]string, 0, len(req.Emails))
for _, email := range req.Emails {
// 去除空格并转换为小写进行去重
email = strings.TrimSpace(strings.ToLower(email))
if email == "" {
continue
}
// 如果邮箱已存在,跳过
if emailMap[email] {
continue
}
emailMap[email] = true
uniqueEmails = append(uniqueEmails, email)
}
var inviteCodes []*models.InviteCode
for _, email := range uniqueEmails {
// 生成唯一邀请码
var code string
for {
code = s.generateCode()
existingCode, err := s.storage.GetByCode(code)
if err != nil || existingCode == nil {
break // 邀请码不存在,可以使用
}
}
inviteCode := &models.InviteCode{
Code: code,
ClientID: req.ClientID,
Email: email,
UserLevelID: userLevelID,
ExpiresAt: expiresAt,
CreatedAt: time.Now(),
}
if err := s.storage.Create(inviteCode); err != nil {
return nil, fmt.Errorf("创建邀请码失败: %w", err)
}
// 发送邀请邮件(如果提供了邮箱)
// 使用 goroutine 异步发送,避免阻塞
if email != "" && s.emailService != nil {
go func(emailAddr, code string) {
// 使用默认语言 "zh" 发送邮件
if err := s.emailService.SendInviteCodeApprovalEmail(emailAddr, code, "zh", expiresAt); err != nil {
// 记录错误但不影响创建流程,邮件发送失败不影响邀请码创建
fmt.Printf("[InviteCodeService] 发送邀请邮件失败: %v (邀请码: %s, 邮箱: %s)\n", err, code, emailAddr)
} else {
fmt.Printf("[InviteCodeService] 邀请邮件发送成功 (邀请码: %s, 邮箱: %s)\n", code, emailAddr)
}
}(email, inviteCode.Code)
}
inviteCodes = append(inviteCodes, inviteCode)
}
if len(inviteCodes) == 0 {
return nil, fmt.Errorf("没有有效的邮箱地址")
}
return inviteCodes, nil
}
// 如果没有提供邮箱,只创建一个邀请码(向后兼容)
var code string
for {
code = s.generateCode()
existingCode, err := s.storage.GetByCode(code)
if err != nil || existingCode == nil {
break // 邀请码不存在,可以使用
}
}
inviteCode := &models.InviteCode{
Code: code,
ClientID: req.ClientID,
Email: "",
UserLevelID: userLevelID,
ExpiresAt: expiresAt,
CreatedAt: time.Now(),
}
if err := s.storage.Create(inviteCode); err != nil {
return nil, fmt.Errorf("创建邀请码失败: %w", err)
}
return []*models.InviteCode{inviteCode}, nil
}
func (s *inviteCodeService) GetByID(id uint) (*models.InviteCode, error) {
return s.storage.GetByID(id)
}
func (s *inviteCodeService) GetByCode(code string) (*models.InviteCode, error) {
return s.storage.GetByCode(code)
}
func (s *inviteCodeService) List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error) {
inviteCodes, total, err := s.storage.List(req)
if err != nil {
return nil, fmt.Errorf("获取邀请码列表失败: %w", err)
}
return &models.InviteCodeListResponse{
List: inviteCodes,
Total: total,
}, nil
}
func (s *inviteCodeService) Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error) {
// 获取现有邀请码
inviteCode, err := s.storage.GetByID(id)
if err != nil {
return nil, fmt.Errorf("邀请码不存在")
}
// 检查是否已被使用
if inviteCode.IsUsed {
return nil, fmt.Errorf("已使用的邀请码无法修改")
}
// 更新客户端ID
if req != nil && req.ClientID != "" {
inviteCode.ClientID = req.ClientID
}
// 更新邮箱
if req != nil && req.Email != "" {
inviteCode.Email = req.Email
}
// 更新用户等级ID
if req != nil && req.UserLevelID != nil {
inviteCode.UserLevelID = req.UserLevelID
}
// 更新过期时间
if req != nil && req.ExpiresAt != nil {
inviteCode.ExpiresAt = req.ExpiresAt
}
// 保存更新
if err := s.storage.Update(inviteCode); err != nil {
return nil, fmt.Errorf("更新邀请码失败: %w", err)
}
return inviteCode, nil
}
func (s *inviteCodeService) Delete(id uint) error {
_, err := s.storage.GetByID(id)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if err := s.storage.Delete(id); err != nil {
return fmt.Errorf("删除邀请码失败: %w", err)
}
return nil
}
func (s *inviteCodeService) GetStatistics() (*models.InviteCodeStatistics, error) {
return s.storage.GetStatistics()
}
func (s *inviteCodeService) MarkAsUsed(code string) error {
inviteCode, err := s.storage.GetByCode(code)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if inviteCode.IsUsed {
return fmt.Errorf("邀请码已被使用")
}
// 检查是否过期
if s.storage.IsExpired(inviteCode) {
return fmt.Errorf("邀请码已过期")
}
inviteCode.IsUsed = true
if err := s.storage.Update(inviteCode); err != nil {
return fmt.Errorf("标记邀请码为已使用失败: %w", err)
}
return nil
}
// ValidateInviteCode 验证邀请码是否有效(未使用且未过期)
func (s *inviteCodeService) ValidateInviteCode(code string) error {
inviteCode, err := s.storage.GetByCode(code)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if inviteCode.IsUsed {
return fmt.Errorf("邀请码已被使用")
}
// 检查是否过期
if s.storage.IsExpired(inviteCode) {
return fmt.Errorf("邀请码已过期")
}
return nil
}
func (s *inviteCodeService) generateCode() string {
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// 生成8位随机字符
part1 := make([]byte, 8)
for i := range part1 {
part1[i] = chars[rand.Intn(len(chars))]
}
// 生成4位随机字符
part2 := make([]byte, 4)
for i := range part2 {
part2[i] = chars[rand.Intn(len(chars))]
}
return fmt.Sprintf("GFY-%s-%s", string(part1), string(part2))
}

View File

@@ -0,0 +1,85 @@
package services
import (
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// LogService 日志服务接口
type LogService interface {
CreateLoginLog(log *models.LoginLog) error
CreateOperationLog(log *models.OperationLog) error
GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error)
GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error)
DeleteLoginLogs(beforeDate string) error
DeleteOperationLogs(beforeDate string) error
}
type logService struct {
logStorage storage.LogStorage
logger *utils.Logger
}
// NewLogService 创建日志服务实例
func NewLogService(logStorage storage.LogStorage, logger *utils.Logger) LogService {
return &logService{
logStorage: logStorage,
logger: logger,
}
}
// CreateLoginLog 创建登录日志
func (s *logService) CreateLoginLog(log *models.LoginLog) error {
err := s.logStorage.CreateLoginLog(log)
if err != nil {
s.logger.Error("创建登录日志失败", zap.Error(err))
return err
}
return nil
}
// CreateOperationLog 创建操作日志
func (s *logService) CreateOperationLog(log *models.OperationLog) error {
err := s.logStorage.CreateOperationLog(log)
if err != nil {
s.logger.Error("创建操作日志失败", zap.Error(err))
return err
}
return nil
}
// GetLoginLogs 获取登录日志列表
func (s *logService) GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error) {
return s.logStorage.GetLoginLogs(req)
}
// GetOperationLogs 获取操作日志列表
func (s *logService) GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error) {
return s.logStorage.GetOperationLogs(req)
}
// DeleteLoginLogs 删除指定日期之前的登录日志
func (s *logService) DeleteLoginLogs(beforeDate string) error {
err := s.logStorage.DeleteLoginLogs(beforeDate)
if err != nil {
s.logger.Error("删除登录日志失败", zap.Error(err))
return err
}
s.logger.Info("登录日志删除成功", zap.String("beforeDate", beforeDate))
return nil
}
// DeleteOperationLogs 删除指定日期之前的操作日志
func (s *logService) DeleteOperationLogs(beforeDate string) error {
err := s.logStorage.DeleteOperationLogs(beforeDate)
if err != nil {
s.logger.Error("删除操作日志失败", zap.Error(err))
return err
}
s.logger.Info("操作日志删除成功", zap.String("beforeDate", beforeDate))
return nil
}

View File

@@ -0,0 +1,393 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"html"
"html/template"
"io"
"net/http"
"time"
"unicode/utf8"
)
// MessagePushService 消息推送服务接口
type MessagePushService interface {
SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error)
GetPushLogs(ctx context.Context, req *models.MessagePushListRequest) (*models.MessagePushListResponse, error)
GetPushLogByID(ctx context.Context, id int64) (*models.MessagePushLog, error)
SearchUsers(ctx context.Context, req *models.UserSearchRequest) (*models.UserSearchResponse, error)
}
type messagePushService struct {
storage storage.MessagePushStorage
goalfymaxClient *http.Client
goalfymaxURL string
retryCount int
retryInterval time.Duration
emailService *EmailService
goalfyMaxUserStorage storage.GoalfyMaxUserStorage
}
// NewMessagePushService 创建消息推送服务实例
func NewMessagePushService() MessagePushService {
cfg := config.GetConfig()
return &messagePushService{
storage: storage.NewMessagePushStorage(),
goalfymaxClient: &http.Client{
Timeout: time.Duration(cfg.MessagePush.Timeout) * time.Second,
},
goalfymaxURL: cfg.MessagePush.GoalfyMaxBaseURL,
retryCount: cfg.MessagePush.RetryCount,
retryInterval: time.Duration(cfg.MessagePush.RetryInterval) * time.Millisecond,
emailService: NewEmailService(),
goalfyMaxUserStorage: storage.NewGoalfyMaxUserStorage(),
}
}
// SendMessage 发送消息
func (s *messagePushService) SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error) {
// 参数校验
if len(req.UserIDs) == 0 {
return nil, fmt.Errorf("目标用户不能为空")
}
if len(req.Title) == 0 {
return nil, fmt.Errorf("消息标题不能为空")
}
titleRuneCount := utf8.RuneCountInString(req.Title)
if titleRuneCount > 100 {
return nil, fmt.Errorf("消息标题不能超过100字符")
}
if len(req.Content) == 0 {
return nil, fmt.Errorf("消息内容不能为空")
}
contentRuneCount := utf8.RuneCountInString(req.Content)
if contentRuneCount > 2000 {
return nil, fmt.Errorf("消息内容不能超过2000字符")
}
// 序列化目标用户
targetUsersJSON, err := storage.SerializeTargetUsers(req.UserIDs)
if err != nil {
return nil, fmt.Errorf("序列化目标用户失败: %w", err)
}
// 创建推送记录
log := &models.MessagePushLog{
Title: req.Title,
Content: req.Content,
TargetUsers: targetUsersJSON,
SenderID: senderID,
SenderName: senderName,
Status: 0, // 待发送
}
if err := s.storage.Create(ctx, log); err != nil {
return nil, fmt.Errorf("创建推送记录失败: %w", err)
}
// 异步发送消息
go s.sendToGoalfyMaxAsync(context.Background(), log.ID, req.Title, req.Content, req.UserIDs)
return &models.MessagePushResponse{
LogID: log.ID,
SuccessCount: 0,
FailCount: 0,
}, nil
}
// sendToGoalfyMaxAsync 异步发送到GoalfyMax
func (s *messagePushService) sendToGoalfyMaxAsync(ctx context.Context, logID int64, title string, content string, userIDs []int) {
// 更新状态为发送中
s.storage.UpdateStatus(ctx, logID, 1, 0, 0, "")
successCount := 0
failCount := 0
var errorMessage string
// 调用GoalfyMax接口
err := s.callGoalfyMaxAPI(ctx, title, content, userIDs)
if err != nil {
failCount = len(userIDs)
errorMessage = err.Error()
s.storage.UpdateStatus(ctx, logID, 3, successCount, failCount, errorMessage)
return
}
// 发送成功,同时发送邮件
s.sendEmails(ctx, title, content, userIDs)
// 发送成功
successCount = len(userIDs)
s.storage.UpdateStatus(ctx, logID, 2, successCount, failCount, "")
}
// sendEmails 发送邮件给用户
func (s *messagePushService) sendEmails(ctx context.Context, title string, content string, userIDs []int) {
// 生成邮件HTML内容
htmlContent := s.generateMessageEmailHTML(title, content)
// 循环发送邮件给每个用户
for _, userID := range userIDs {
user, err := s.goalfyMaxUserStorage.GetByUserID(userID)
if err != nil {
// 如果获取用户失败,跳过该用户
continue
}
if user.Email != "" {
// 发送邮件
if err := s.emailService.sendEmail(user.Email, title, htmlContent); err != nil {
// 记录错误但不中断其他邮件的发送
continue
}
// 避免发送过快,每次发送后稍作延迟
time.Sleep(100 * time.Millisecond)
}
}
}
// generateMessageEmailHTML 生成消息通知邮件的HTML内容
func (s *messagePushService) generateMessageEmailHTML(title, content string) string {
// 转义HTML特殊字符然后将换行符转换为<br>
escapedContent := html.EscapeString(content)
// 将换行符转换为HTML换行
contentHTML := fmt.Sprintf(`<div style="white-space: pre-wrap;">%s</div>`, escapedContent)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #f5f5f5;
padding: 40px 20px;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header {
background-color: #000000;
padding: 32px 40px;
text-align: center;
}
.header-logo {
font-size: 28px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.5px;
}
.content {
padding: 40px;
}
.title {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 24px;
line-height: 1.4;
}
.message-content {
font-size: 15px;
color: #4a4a4a;
line-height: 1.7;
background-color: #fafafa;
padding: 20px 24px;
border-radius: 6px;
margin-bottom: 24px;
}
.footer {
background-color: #fafafa;
padding: 32px 40px;
text-align: center;
border-top: 1px solid #e5e5e5;
}
.footer-brand {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 8px;
}
.footer-contact {
font-size: 13px;
color: #666666;
margin-top: 8px;
}
@media only screen and (max-width: 600px) {
.content {
padding: 24px 20px;
}
.header {
padding: 24px 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="header">
<div class="header-logo">GoalfyAI</div>
</div>
<div class="content">
<div class="title">{{.Title}}</div>
<div class="message-content">{{.Content}}</div>
</div>
<div class="footer">
<div class="footer-brand">GoalfyAI 团队</div>
<div class="footer-contact">
© 2025 GoalfyAI. All rights reserved.
</div>
</div>
</div>
</body>
</html>
`
tmpl, _ := template.New("message").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]interface{}{
"Title": html.EscapeString(title),
"Content": template.HTML(contentHTML),
})
return buf.String()
}
// callGoalfyMaxAPI 调用GoalfyMax API
func (s *messagePushService) callGoalfyMaxAPI(ctx context.Context, title string, content string, userIDs []int) error {
// 构建请求体
reqBody := map[string]interface{}{
"title": title,
"content": content,
"user_ids": userIDs,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("序列化请求失败: %w", err)
}
var lastErr error
for i := 0; i < s.retryCount; i++ {
// 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, "POST", s.goalfymaxURL+"/api/notifications/send", bytes.NewBuffer(jsonData))
if err != nil {
lastErr = fmt.Errorf("创建请求失败: %w", err)
continue
}
// 设置固定Token
req.Header.Set("Authorization", "Bearer admin-message-push")
req.Header.Set("Content-Type", "application/json")
// 发送请求
resp, err := s.goalfymaxClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("发送请求失败 (尝试 %d/%d): %w", i+1, s.retryCount, err)
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = fmt.Errorf("读取响应失败: %w", err)
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
if resp.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("GoalfyMax接口返回错误: %d, 响应: %s", resp.StatusCode, string(body))
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
// 成功返回nil
return nil
}
// 所有重试都失败
return lastErr
}
// GetPushLogs 获取推送记录列表
func (s *messagePushService) GetPushLogs(ctx context.Context, req *models.MessagePushListRequest) (*models.MessagePushListResponse, error) {
// 参数校验
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 10
}
// 调用存储层
logs, total, err := s.storage.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取推送记录失败: %w", err)
}
return &models.MessagePushListResponse{
List: logs,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetPushLogByID 根据ID获取推送记录
func (s *messagePushService) GetPushLogByID(ctx context.Context, id int64) (*models.MessagePushLog, error) {
if id <= 0 {
return nil, fmt.Errorf("推送记录ID无效")
}
log, err := s.storage.GetByID(ctx, id)
if err != nil {
return nil, err
}
return log, nil
}
// SearchUsers 搜索用户
func (s *messagePushService) SearchUsers(ctx context.Context, req *models.UserSearchRequest) (*models.UserSearchResponse, error) {
// 参数校验
if req.Limit <= 0 || req.Limit > 1000 {
req.Limit = 20
}
users, err := s.storage.SearchUsers(ctx, req.Keyword, req.Limit)
if err != nil {
return nil, fmt.Errorf("搜索用户失败: %w", err)
}
return &models.UserSearchResponse{
Users: users,
Total: len(users),
}, nil
}

View File

@@ -0,0 +1,131 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// PageService 页面服务接口
type PageService interface {
Create(req *models.PageCreateRequest) (*models.Page, error)
GetByID(id uint) (*models.Page, error)
Update(id uint, req *models.PageUpdateRequest) (*models.Page, error)
Delete(id uint) error
List(req *models.PageListRequest) ([]models.Page, int64, error)
}
type pageService struct {
pageStorage storage.PageStorage
logger *utils.Logger
}
// NewPageService 创建页面服务实例
func NewPageService(pageStorage storage.PageStorage, logger *utils.Logger) PageService {
return &pageService{
pageStorage: pageStorage,
logger: logger,
}
}
// Create 创建页面
func (s *pageService) Create(req *models.PageCreateRequest) (*models.Page, error) {
// 检查页面路径是否已存在
_, err := s.pageStorage.GetByPath(req.Path)
if err == nil {
return nil, errors.New("页面路径已存在")
}
// 创建页面
page := &models.Page{
Name: req.Name,
Path: req.Path,
Icon: req.Icon,
SortOrder: req.SortOrder,
IsActive: req.IsActive,
}
err = s.pageStorage.Create(page)
if err != nil {
s.logger.Error("创建页面失败", zap.Error(err))
return nil, errors.New("创建页面失败")
}
s.logger.Info("页面创建成功", zap.String("name", page.Name))
return page, nil
}
// GetByID 根据ID获取页面
func (s *pageService) GetByID(id uint) (*models.Page, error) {
page, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("获取页面失败", zap.Uint("id", id), zap.Error(err))
return nil, errors.New("页面不存在")
}
return page, nil
}
// Update 更新页面
func (s *pageService) Update(id uint, req *models.PageUpdateRequest) (*models.Page, error) {
// 获取现有页面
page, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("页面不存在", zap.Uint("id", id), zap.Error(err))
return nil, errors.New("页面不存在")
}
// 如果路径发生变化,检查新路径是否已存在
if req.Path != page.Path {
_, err := s.pageStorage.GetByPath(req.Path)
if err == nil {
return nil, errors.New("页面路径已被其他页面使用")
}
page.Path = req.Path
}
// 更新页面信息
page.Name = req.Name
page.Icon = req.Icon
page.SortOrder = req.SortOrder
page.IsActive = req.IsActive
err = s.pageStorage.Update(page)
if err != nil {
s.logger.Error("更新页面失败", zap.Error(err))
return nil, errors.New("更新页面失败")
}
s.logger.Info("页面更新成功", zap.Uint("page_id", id))
return page, nil
}
// Delete 删除页面
func (s *pageService) Delete(id uint) error {
// 检查页面是否存在
_, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("页面不存在", zap.Uint("id", id), zap.Error(err))
return errors.New("页面不存在")
}
err = s.pageStorage.Delete(id)
if err != nil {
s.logger.Error("删除页面失败", zap.Error(err))
return errors.New("删除页面失败")
}
s.logger.Info("页面删除成功", zap.Uint("page_id", id))
return nil
}
// List 获取页面列表
func (s *pageService) List(req *models.PageListRequest) ([]models.Page, int64, error) {
pages, total, err := s.pageStorage.List(req)
if err != nil {
s.logger.Error("获取页面列表失败", zap.Error(err))
return nil, 0, errors.New("获取页面列表失败")
}
return pages, total, nil
}

View File

@@ -0,0 +1,128 @@
package services
import (
"errors"
"goalfymax-admin/internal/models"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
// QuotaService 配额服务接口
type QuotaService interface {
GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error)
HealthCheck() error
GetQuotaRules() (*models.QuotaRulesResponse, error)
CreateQuotaRule(body any) (*models.QuotaRulesResponse, error)
UpdateQuotaRule(id string, body any) (*models.QuotaRulesResponse, error)
DeleteQuotaRule(id string) (*models.QuotaRulesResponse, error)
}
type quotaService struct {
gatewayClient *GatewayClient
logger *utils.Logger
}
// NewQuotaService 创建配额服务实例
func NewQuotaService(gatewayClient *GatewayClient, logger *utils.Logger) QuotaService {
return &quotaService{
gatewayClient: gatewayClient,
logger: logger,
}
}
// GetQuotaHistory 获取配额历史数据
func (s *quotaService) GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error) {
// 验证请求参数
if err := s.validateQuotaHistoryRequest(req); err != nil {
s.logger.Error("配额历史请求参数验证失败", zap.Error(err))
return nil, err
}
// 调用网关客户端
response, err := s.gatewayClient.GetQuotaHistory(req)
if err != nil {
s.logger.Error("获取配额历史数据失败", zap.Error(err))
return nil, err
}
s.logger.Info("配额历史数据获取成功",
zap.String("start_date", req.StartDate),
zap.String("end_date", req.EndDate),
zap.Int("data_count", len(response.Data)),
)
return response, nil
}
// HealthCheck 健康检查
func (s *quotaService) HealthCheck() error {
err := s.gatewayClient.HealthCheck()
if err != nil {
s.logger.Error("配额服务健康检查失败", zap.Error(err))
return err
}
s.logger.Info("配额服务健康检查成功")
return nil
}
// GetQuotaRules 获取配额规则列表
func (s *quotaService) GetQuotaRules() (*models.QuotaRulesResponse, error) {
resp, err := s.gatewayClient.GetQuotaRules("")
if err != nil {
s.logger.Error("获取配额规则失败", zap.Error(err))
return nil, err
}
return resp, nil
}
func (s *quotaService) CreateQuotaRule(body any) (*models.QuotaRulesResponse, error) {
return s.gatewayClient.CreateQuotaRule("", body)
}
func (s *quotaService) UpdateQuotaRule(id string, body any) (*models.QuotaRulesResponse, error) {
if id == "" {
return nil, errors.New("缺少规则ID")
}
return s.gatewayClient.UpdateQuotaRule("", id, body)
}
func (s *quotaService) DeleteQuotaRule(id string) (*models.QuotaRulesResponse, error) {
if id == "" {
return nil, errors.New("缺少规则ID")
}
return s.gatewayClient.DeleteQuotaRule("", id)
}
// validateQuotaHistoryRequest 验证配额历史请求参数
func (s *quotaService) validateQuotaHistoryRequest(req *models.QuotaHistoryRequest) error {
if req.StartDate == "" {
return errors.New("开始日期不能为空")
}
if req.EndDate == "" {
return errors.New("结束日期不能为空")
}
// 验证日期格式 (简单验证,实际项目中可以使用更严格的验证)
if len(req.StartDate) != 10 || len(req.EndDate) != 10 {
return errors.New("日期格式不正确,应为 YYYY-MM-DD")
}
// 验证周期参数
if req.Period != "" && req.Period != "daily" && req.Period != "monthly" {
return errors.New("周期参数只能是 daily 或 monthly")
}
// 设置默认值
if req.Period == "" {
req.Period = "daily"
}
if len(req.GroupBy) == 0 {
req.GroupBy = []string{"user_id"}
}
return nil
}

View File

@@ -0,0 +1,165 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// RBACService 简化的RBAC服务接口
type RBACService interface {
// 角色页面权限管理
AssignRolePagePermissions(req *models.RolePagePermissionAssignRequest) error
RemoveRolePagePermissions(roleID uint, pageIDs []uint) error
GetRolePagePermissions(roleID uint) ([]models.Page, error)
GetRolePagePermissionIDs(roleID uint) ([]uint, error)
// 页面权限检查
CheckUserPagePermission(userID uint, pagePath string) (bool, error)
GetUserAccessiblePages(userID uint) ([]string, error)
GetUserPermissionsResponse(userID uint) (*models.UserPermissionsResponse, error)
GetRolePagePermissionsResponse(roleID uint) (*models.RolePagePermissionsResponse, error)
// 角色管理
GetRoleByID(roleID uint) (*models.Role, error)
}
type rbacService struct {
rbacStorage storage.RBACStorage
userStorage storage.UserStorage
logger *utils.Logger
}
// NewRBACService 创建RBAC服务实例
func NewRBACService(rbacStorage storage.RBACStorage, userStorage storage.UserStorage, logger *utils.Logger) RBACService {
return &rbacService{
rbacStorage: rbacStorage,
userStorage: userStorage,
logger: logger,
}
}
// AssignRolePagePermissions 分配角色页面权限
func (s *rbacService) AssignRolePagePermissions(req *models.RolePagePermissionAssignRequest) error {
err := s.rbacStorage.AssignRolePagePermissions(req.RoleID, req.PageIDs)
if err != nil {
s.logger.Error("分配角色页面权限失败", zap.Uint("roleID", req.RoleID), zap.Uints("pageIDs", req.PageIDs), zap.Error(err))
return errors.New("分配角色页面权限失败")
}
s.logger.Info("分配角色页面权限成功", zap.Uint("roleID", req.RoleID), zap.Uints("pageIDs", req.PageIDs))
return nil
}
// RemoveRolePagePermissions 移除角色页面权限
func (s *rbacService) RemoveRolePagePermissions(roleID uint, pageIDs []uint) error {
err := s.rbacStorage.RemoveRolePagePermissions(roleID, pageIDs)
if err != nil {
s.logger.Error("移除角色页面权限失败", zap.Uint("roleID", roleID), zap.Uints("pageIDs", pageIDs), zap.Error(err))
return errors.New("移除角色页面权限失败")
}
s.logger.Info("移除角色页面权限成功", zap.Uint("roleID", roleID), zap.Uints("pageIDs", pageIDs))
return nil
}
// GetRolePagePermissions 获取角色页面权限
func (s *rbacService) GetRolePagePermissions(roleID uint) ([]models.Page, error) {
pages, err := s.rbacStorage.GetRolePagePermissions(roleID)
if err != nil {
s.logger.Error("获取角色页面权限失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限失败")
}
return pages, nil
}
// GetRolePagePermissionIDs 获取角色页面权限ID列表
func (s *rbacService) GetRolePagePermissionIDs(roleID uint) ([]uint, error) {
pageIDs, err := s.rbacStorage.GetRolePagePermissionIDs(roleID)
if err != nil {
s.logger.Error("获取角色页面权限ID失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限ID失败")
}
return pageIDs, nil
}
// CheckUserPagePermission 检查用户页面权限
func (s *rbacService) CheckUserPagePermission(userID uint, pagePath string) (bool, error) {
hasPermission, err := s.rbacStorage.CheckUserRolePagePermission(userID, pagePath)
if err != nil {
s.logger.Error("检查用户页面权限失败", zap.Uint("userID", userID), zap.String("pagePath", pagePath), zap.Error(err))
return false, errors.New("检查用户页面权限失败")
}
return hasPermission, nil
}
// GetUserAccessiblePages 获取用户可访问页面
func (s *rbacService) GetUserAccessiblePages(userID uint) ([]string, error) {
pages, err := s.rbacStorage.GetUserRoleAccessiblePages(userID)
if err != nil {
s.logger.Error("获取用户可访问页面失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户可访问页面失败")
}
return pages, nil
}
// GetUserPermissionsResponse 获取用户权限响应
func (s *rbacService) GetUserPermissionsResponse(userID uint) (*models.UserPermissionsResponse, error) {
// 获取用户信息
user, err := s.userStorage.GetByID(userID)
if err != nil {
s.logger.Error("获取用户信息失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户信息失败")
}
// 查询角色信息
role, err := s.rbacStorage.GetRoleByID(user.RoleID)
if err != nil {
s.logger.Error("获取角色信息失败", zap.Uint("roleID", user.RoleID), zap.Error(err))
return nil, errors.New("获取角色信息失败")
}
// 获取用户可访问页面
pagePaths, err := s.GetUserAccessiblePages(userID)
if err != nil {
s.logger.Error("获取用户可访问页面失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户可访问页面失败")
}
// 转换页面路径为页面对象
var pages []models.Page
for _, path := range pagePaths {
pages = append(pages, models.Page{Path: path})
}
return &models.UserPermissionsResponse{
User: *user,
Roles: []models.Role{*role},
Pages: pages,
}, nil
}
// GetRolePagePermissionsResponse 获取角色页面权限响应
func (s *rbacService) GetRolePagePermissionsResponse(roleID uint) (*models.RolePagePermissionsResponse, error) {
// 获取角色信息(这里需要从角色存储中获取,暂时简化)
role := models.Role{BaseModel: models.BaseModel{ID: roleID}}
// 获取角色页面权限
pages, err := s.GetRolePagePermissions(roleID)
if err != nil {
s.logger.Error("获取角色页面权限失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限失败")
}
return &models.RolePagePermissionsResponse{
Role: role,
Pages: pages,
}, nil
}
// GetRoleByID 根据ID获取角色
func (s *rbacService) GetRoleByID(roleID uint) (*models.Role, error) {
return s.rbacStorage.GetRoleByID(roleID)
}

View File

@@ -0,0 +1,137 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// RoleService 角色服务接口
type RoleService interface {
Create(req *models.RoleCreateRequest) (*models.Role, error)
GetByID(id uint) (*models.Role, error)
Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error)
Delete(id uint) error
List(req *models.RoleListRequest) ([]models.Role, int64, error)
UpdateStatus(id uint, status int) error
}
type roleService struct {
roleStorage storage.RoleStorage
logger *utils.Logger
}
// NewRoleService 创建角色服务实例
func NewRoleService(roleStorage storage.RoleStorage, logger *utils.Logger) RoleService {
return &roleService{
roleStorage: roleStorage,
logger: logger,
}
}
// Create 创建角色
func (s *roleService) Create(req *models.RoleCreateRequest) (*models.Role, error) {
// 检查角色名称是否已存在
_, err := s.roleStorage.GetByName(req.Name)
if err == nil {
return nil, errors.New("角色名称已存在")
}
// 创建角色
role := &models.Role{
Name: req.Name,
Level: req.Level,
Description: req.Description,
IsDefault: req.IsDefault,
}
err = s.roleStorage.Create(role)
if err != nil {
s.logger.Error("创建角色失败", zap.Error(err))
return nil, errors.New("创建角色失败")
}
s.logger.Info("角色创建成功", zap.String("name", role.Name))
return role, nil
}
// GetByID 根据ID获取角色
func (s *roleService) GetByID(id uint) (*models.Role, error) {
return s.roleStorage.GetByID(id)
}
// Update 更新角色
func (s *roleService) Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error) {
// 获取角色信息
role, err := s.roleStorage.GetByID(id)
if err != nil {
return nil, errors.New("角色不存在")
}
// 检查角色名称是否已被其他角色使用
if req.Name != role.Name {
existingRole, err := s.roleStorage.GetByName(req.Name)
if err == nil && existingRole.ID != id {
return nil, errors.New("角色名称已被其他角色使用")
}
role.Name = req.Name
}
// 更新角色信息
role.Name = req.Name
role.Level = req.Level
role.Description = req.Description
role.IsDefault = req.IsDefault
err = s.roleStorage.Update(role)
if err != nil {
s.logger.Error("更新角色失败", zap.Error(err))
return nil, errors.New("更新角色失败")
}
s.logger.Info("角色更新成功", zap.Uint("role_id", id))
return role, nil
}
// Delete 删除角色
func (s *roleService) Delete(id uint) error {
// 检查角色是否存在
_, err := s.roleStorage.GetByID(id)
if err != nil {
return errors.New("角色不存在")
}
err = s.roleStorage.Delete(id)
if err != nil {
s.logger.Error("删除角色失败", zap.Error(err))
return errors.New("删除角色失败")
}
s.logger.Info("角色删除成功", zap.Uint("role_id", id))
return nil
}
// List 获取角色列表
func (s *roleService) List(req *models.RoleListRequest) ([]models.Role, int64, error) {
return s.roleStorage.List(req)
}
// UpdateStatus 更新角色状态
func (s *roleService) UpdateStatus(id uint, status int) error {
// 检查角色是否存在
_, err := s.roleStorage.GetByID(id)
if err != nil {
return errors.New("角色不存在")
}
err = s.roleStorage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新角色状态失败", zap.Error(err))
return errors.New("更新角色状态失败")
}
s.logger.Info("角色状态更新成功", zap.Uint("role_id", id), zap.Int("status", status))
return nil
}

View File

@@ -0,0 +1,170 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"goalfymax-admin/internal/config"
)
// SSOAdminUserCreateRequest SSO创建用户请求
type SSOAdminUserCreateRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
}
// SSOAdminUserCreateResponse SSO创建用户响应
type SSOAdminUserCreateResponse struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Phone string `json:"phone"`
Message string `json:"message,omitempty"`
}
// SSOAdminUserRoleAssignRequest SSO用户角色分配请求
type SSOAdminUserRoleAssignRequest struct {
UserID int `json:"user_id"`
RoleID int `json:"role_id"`
}
// SSOAdminUserRoleAssignResponse SSO用户角色分配响应
type SSOAdminUserRoleAssignResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
// SSOAdminService SSO管理服务接口
type SSOAdminService interface {
CreateUser(ctx context.Context, req *SSOAdminUserCreateRequest) (*SSOAdminUserCreateResponse, error)
SetSystemRole(ctx context.Context, userID int, systemRole string) error
}
// ssoAdminService SSO管理服务实现
type ssoAdminService struct {
baseURL string
adminToken string
timeout time.Duration
client *http.Client
}
// NewSSOAdminService 创建SSO管理服务实例
func NewSSOAdminService() SSOAdminService {
cfg := config.GetConfig()
client := &http.Client{
Timeout: cfg.SSO.Timeout,
}
return &ssoAdminService{
baseURL: cfg.SSO.SSOServerURL,
adminToken: cfg.SSO.AdminToken,
timeout: cfg.SSO.Timeout,
client: client,
}
}
// CreateUser 创建SSO用户
func (s *ssoAdminService) CreateUser(ctx context.Context, req *SSOAdminUserCreateRequest) (*SSOAdminUserCreateResponse, error) {
url := fmt.Sprintf("%s/api/admin/users", s.baseURL)
// 构造请求体
reqBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminToken))
httpReq.Header.Set("Accept", "*/*")
httpReq.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Pragma", "no-cache")
// 发送请求
resp, err := s.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("SSO API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
}
// 解析响应
var response SSOAdminUserCreateResponse
if err := json.Unmarshal(respBody, &response); err != nil {
return nil, fmt.Errorf("解析SSO响应失败: %w", err)
}
return &response, nil
}
// SetSystemRole 设置系统角色custom/sys_admin
func (s *ssoAdminService) SetSystemRole(ctx context.Context, userID int, systemRole string) error {
url := fmt.Sprintf("%s/api/admin/users/%d/role", s.baseURL, userID)
// 构造请求体
reqBodyBytes, err := json.Marshal(map[string]string{
"system_role": systemRole,
})
if err != nil {
return fmt.Errorf("序列化请求体失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBodyBytes))
if err != nil {
return fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminToken))
httpReq.Header.Set("Accept", "*/*")
httpReq.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Pragma", "no-cache")
// 发送请求
resp, err := s.client.Do(httpReq)
if err != nil {
return fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应体失败: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("SSO系统角色API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
}
// 允许无 success 包装的简易响应,这里不做进一步结构校验
return nil
}

View File

@@ -0,0 +1,618 @@
package services
import (
"context"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/middleware"
"goalfymax-admin/pkg/utils"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SSOService SSO服务接口
type SSOService interface {
// 登录相关
InitiateLogin(ctx context.Context) (*models.SSOLoginResponse, error)
HandleCallback(ctx context.Context, req *models.SSOCallbackRequest) (*models.SSOCallbackResponse, error)
RefreshToken(ctx context.Context, req *models.RefreshTokenRequest) (*models.RefreshTokenResponse, error)
Logout(ctx context.Context, token string) (*models.LogoutResponse, error)
GetUserInfo(ctx context.Context, token string) (*models.UserInfoResponse, error)
// 登录信息管理
RecordUserLogin(ctx context.Context, req *UserLoginRequest) error
UserLogout(ctx context.Context, userID int, uuid string) error
GetUserLoginInfo(ctx context.Context, userID int) (*models.LoginInfo, error)
IsUserOnline(ctx context.Context, userID int) (bool, error)
GetOnlineUsers(ctx context.Context) ([]*models.LoginInfo, error)
GetOnlineUserCount(ctx context.Context) (int64, error)
BatchUserLogout(ctx context.Context, userIDs []int) error
}
// UserLoginRequest 用户登录请求结构体
type UserLoginRequest struct {
UserID int `json:"user_id" binding:"required"`
UserName string `json:"user_name" binding:"required,max=100"`
Email string `json:"email" binding:"required,email,max=255"`
UUID string `json:"uuid" binding:"required"`
}
type ssoService struct {
client *middleware.SSOClient
pkceStateStorage storage.PKCEStateStorage
loginInfoStorage storage.LoginInfoStorage
rbacService RBACService
logger *utils.Logger
}
// NewSSOService 创建SSO服务实例
func NewSSOService(client *middleware.SSOClient, pkceStateStorage storage.PKCEStateStorage, loginInfoStorage storage.LoginInfoStorage, rbacService RBACService, logger *utils.Logger) SSOService {
return &ssoService{
client: client,
pkceStateStorage: pkceStateStorage,
loginInfoStorage: loginInfoStorage,
rbacService: rbacService,
logger: logger,
}
}
// InitiateLogin 初始化SSO登录
func (s *ssoService) InitiateLogin(ctx context.Context) (*models.SSOLoginResponse, error) {
// 生成状态参数
state := fmt.Sprintf("state_%d", time.Now().UnixNano())
// 获取授权URL
authURL, codeVerifier, err := s.client.GetAuthorizationURL(state)
if err != nil {
s.logger.Error("failed to generate authorization URL", zap.Error(err))
return nil, fmt.Errorf("生成授权URL失败: %w", err)
}
// 将state和codeVerifier存储到数据库
pkceState := &models.PKCEState{
State: state,
CodeVerifier: codeVerifier,
}
if err := s.pkceStateStorage.Create(pkceState); err != nil {
s.logger.Error("failed to store PKCE state", zap.Error(err))
return nil, fmt.Errorf("存储PKCE状态失败: %w", err)
}
return &models.SSOLoginResponse{
Success: true,
Message: "SSO login initiated",
AuthURL: authURL,
State: state,
CodeVerifier: "", // 不返回codeVerifier到前端保护安全
}, nil
}
// HandleCallback 处理SSO回调
func (s *ssoService) HandleCallback(ctx context.Context, req *models.SSOCallbackRequest) (*models.SSOCallbackResponse, error) {
// 从数据库获取PKCE状态信息
pkceState, err := s.pkceStateStorage.GetByState(req.State)
if err != nil {
s.logger.Error("failed to get PKCE state", zap.String("state", req.State), zap.Error(err))
return nil, fmt.Errorf("无效或过期的状态参数")
}
// 交换令牌
tokenResp, err := s.client.ExchangeCodeForToken(ctx, req.Code, pkceState.CodeVerifier)
if err != nil {
s.logger.Error("failed to exchange token", zap.Error(err))
return nil, fmt.Errorf("令牌交换失败: %w", err)
}
// 获取用户信息
userInfo, err := s.client.GetUserInfo(ctx, tokenResp.AccessToken)
if err != nil {
s.logger.Error("failed to get user info", zap.Error(err))
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 记录用户登录信息
uid, err := strconv.Atoi(userInfo.Sub)
if err != nil {
s.logger.Error("failed to convert user id", zap.String("sub", userInfo.Sub), zap.Error(err))
return nil, fmt.Errorf("无效的用户ID")
}
// 计算本次登录应赋予的角色IDsys_admin → 5否则 → 0
assignedRoleID := uint(0)
if len(userInfo.Roles) > 0 {
for _, r := range userInfo.Roles {
if r.Name == "sys_admin" {
assignedRoleID = uint(5)
break
}
}
}
if assignedRoleID == 0 {
s.logger.Info("本次登录未检测到 sys_admin 角色赋予无权限角色ID=0", zap.Int("userID", uid))
} else {
s.logger.Info("本次登录检测到 sys_admin 角色赋予角色ID=5", zap.Int("userID", uid))
}
// 查找或创建用户记录,并根据本次判定设置 role_idsys_admin=5否则=0
_, err = s.findOrCreateUserWithRole(uint(uid), userInfo, assignedRoleID)
if err != nil {
s.logger.Error("failed to find or create user", zap.Error(err))
return nil, fmt.Errorf("创建或查找用户失败: %w", err)
}
uuid := uuid.NewString()
err = s.RecordUserLogin(ctx, &UserLoginRequest{
UserID: uid,
UserName: userInfo.Name,
Email: userInfo.Email,
UUID: uuid,
})
if err != nil {
s.logger.Error("failed to record user login", zap.Int("user_id", uid), zap.Error(err))
// 不返回错误,继续处理
}
// 获取用户可访问页面信息
userPages, err := s.rbacService.GetUserAccessiblePages(uint(uid))
if err != nil {
s.logger.Warn("获取用户页面权限失败", zap.Error(err))
userPages = []string{}
}
s.logger.Info("获取用户可访问页面", zap.Uint("userID", uint(uid)), zap.Strings("pages", userPages))
// 获取用户角色信息(用于前端体验判断,后端仍以接口鉴权为准)
var user models.User
var userRoleName string
err = storage.DB.Where("id = ?", uint(uid)).First(&user).Error
if err != nil {
s.logger.Warn("获取用户信息失败", zap.Error(err))
} else {
role, rerr := s.rbacService.GetRoleByID(user.RoleID)
if rerr != nil {
s.logger.Warn("获取角色信息失败", zap.Error(rerr))
} else {
userRoleName = role.Name
s.logger.Info("获取用户角色", zap.Uint("userID", uint(uid)), zap.String("roleName", userRoleName))
}
}
// 构建包含页面权限的用户信息
userInfoWithPages := &models.UserInfo{
Sub: userInfo.Sub,
Name: userInfo.Name,
Email: userInfo.Email,
PreferredUsername: userInfo.PreferredUsername,
Pages: convertPagesFromPaths(userPages),
Roles: []models.Role{},
}
if userRoleName != "" {
userInfoWithPages.Roles = append(userInfoWithPages.Roles, models.Role{Name: userRoleName})
}
// 清理PKCE状态
_ = s.pkceStateStorage.DeleteByState(req.State)
return &models.SSOCallbackResponse{
Success: true,
Message: "SSO login successful",
AccessToken: tokenResp.AccessToken,
IDToken: tokenResp.IDToken,
RefreshToken: tokenResp.RefreshToken,
ExpiresIn: tokenResp.ExpiresIn,
UserInfo: userInfoWithPages,
UUID: uuid,
}, nil
}
// convertPagesFromPaths 将页面路径转换为页面对象
func convertPagesFromPaths(paths []string) []models.Page {
var pages []models.Page
fmt.Printf("🔍 [convertPagesFromPaths] 输入路径: %v\n", paths)
// 从数据库获取完整的页面信息
err := storage.DB.Where("path IN ? AND is_active = TRUE AND deleted_at IS NULL", paths).Find(&pages).Error
if err != nil {
fmt.Printf("❌ [convertPagesFromPaths] 数据库查询失败: %v\n", err)
// 如果数据库查询失败,至少返回路径信息
for _, path := range paths {
pages = append(pages, models.Page{
Path: path,
IsActive: true, // 默认设置为激活状态
})
}
} else {
fmt.Printf("✅ [convertPagesFromPaths] 数据库查询成功,找到 %d 个页面\n", len(pages))
for _, page := range pages {
fmt.Printf(" - 页面: %s, 激活状态: %v\n", page.Path, page.IsActive)
}
}
return pages
}
// RefreshToken 刷新令牌
func (s *ssoService) RefreshToken(ctx context.Context, req *models.RefreshTokenRequest) (*models.RefreshTokenResponse, error) {
// 刷新令牌
tokenResp, err := s.client.RefreshToken(ctx, req.RefreshToken)
if err != nil {
s.logger.Error("failed to refresh token", zap.Error(err))
return nil, fmt.Errorf("令牌刷新失败: %w", err)
}
return &models.RefreshTokenResponse{
Success: true,
Message: "Token refreshed successfully",
AccessToken: tokenResp.AccessToken,
IDToken: tokenResp.IDToken,
RefreshToken: tokenResp.RefreshToken,
ExpiresIn: tokenResp.ExpiresIn,
}, nil
}
// Logout 登出
func (s *ssoService) Logout(ctx context.Context, token string) (*models.LogoutResponse, error) {
// 获取用户信息用于记录登出
userInfo, err := s.client.GetUserInfo(ctx, token)
if err != nil {
s.logger.Error("failed to get user info during logout", zap.Error(err))
// 继续执行登出,不中断流程
}
// 调用SSO服务登出
err = s.client.Logout(ctx, token)
if err != nil {
s.logger.Error("failed to logout", zap.Error(err))
return nil, fmt.Errorf("登出失败: %w", err)
}
// 记录用户登出信息
if userInfo != nil {
uid, err := strconv.Atoi(userInfo.Sub)
if err != nil {
s.logger.Error("failed to convert user id during logout", zap.String("sub", userInfo.Sub), zap.Error(err))
} else {
// 先根据user_id查找用户的uuid
loginInfo, err := s.loginInfoStorage.GetByUserID(uid)
if err != nil {
s.logger.Error("failed to get user login info during logout", zap.Int("user_id", uid), zap.Error(err))
} else {
// 如果UUID为空直接根据user_id进行登出
if loginInfo.UUID == "" {
s.logger.Warn("UUID is empty, logging out by user_id only", zap.Int("user_id", uid))
err = s.loginInfoStorage.SetUserOffline(uid)
if err != nil {
s.logger.Error("failed to set user offline by user_id", zap.Int("user_id", uid), zap.Error(err))
}
} else {
// 使用找到的uuid进行登出
err = s.UserLogout(ctx, uid, loginInfo.UUID)
if err != nil {
s.logger.Error("failed to record user logout", zap.Int("user_id", uid), zap.Error(err))
}
}
}
}
}
redirectUrl := s.client.GetServerUrl() + "/oauth2/logout?redirect_uri=" + url.QueryEscape(s.client.GetRedirectUrl())
return &models.LogoutResponse{
Success: true,
Message: redirectUrl,
}, nil
}
// GetUserInfo 获取用户信息
func (s *ssoService) GetUserInfo(ctx context.Context, token string) (*models.UserInfoResponse, error) {
// 获取用户信息
userInfo, err := s.client.GetUserInfo(ctx, token)
if err != nil {
s.logger.Error("failed to get user info", zap.Error(err))
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
return &models.UserInfoResponse{
Success: true,
Message: "User info retrieved successfully",
UserInfo: userInfo,
}, nil
}
// RecordUserLogin 记录用户登录状态
func (s *ssoService) RecordUserLogin(ctx context.Context, req *UserLoginRequest) error {
// 检查用户是否已存在
existingUser, err := s.loginInfoStorage.GetByUserID(req.UserID)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("failed to get existing user login info",
zap.Int("user_id", req.UserID),
zap.Error(err))
return fmt.Errorf("获取用户登录信息失败: %w", err)
}
if existingUser != nil {
// 用户存在直接更新状态
existingUser.IsOnline = true
existingUser.UUID = req.UUID // 同时更新UUID可能有变化
existingUser.UserName = req.UserName
existingUser.Email = req.Email
err = s.loginInfoStorage.Update(existingUser)
if err != nil {
s.logger.Error("failed to update user online status",
zap.Int("user_id", req.UserID),
zap.Error(err))
return fmt.Errorf("更新用户在线状态失败: %w", err)
}
s.logger.Info("user login status updated successfully",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName))
} else {
// 用户不存在,创建新用户并设置为在线
newLoginInfo := &models.LoginInfo{
UserID: req.UserID,
UserName: req.UserName,
Email: req.Email,
UUID: req.UUID,
IsOnline: true,
}
err = s.loginInfoStorage.Create(newLoginInfo)
if err != nil {
s.logger.Error("failed to create new user login info",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName),
zap.Error(err))
return fmt.Errorf("创建用户登录信息失败: %w", err)
}
s.logger.Info("new user login info created successfully",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName))
}
return nil
}
// UserLogout 用户登出
func (s *ssoService) UserLogout(ctx context.Context, userID int, uuid string) error {
// 检查用户是否存在
existingUser, err := s.loginInfoStorage.GetByUserIDAndUUID(userID, uuid)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("failed to get user login info for logout",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.Error(err))
return fmt.Errorf("获取用户登录信息失败: %w", err)
}
if existingUser == nil {
// 用户不存在,不需要操作
s.logger.Info("user not found, no logout action needed",
zap.Int("user_id", userID),
zap.String("uuid", uuid))
return nil
}
// 检查用户是否已经离线
if !existingUser.IsOnline {
// 用户已经离线,不需要操作
s.logger.Info("user is already offline, no action needed",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.String("user_name", existingUser.UserName))
return nil
}
// 设置用户为离线状态
err = s.loginInfoStorage.SetUserOffline(userID)
if err != nil {
s.logger.Error("failed to set user offline",
zap.Int("user_id", userID),
zap.String("user_name", existingUser.UserName),
zap.Error(err))
return fmt.Errorf("设置用户离线状态失败: %w", err)
}
s.logger.Info("user logout successfully",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.String("user_name", existingUser.UserName))
return nil
}
// GetUserLoginInfo 获取用户登录信息
func (s *ssoService) GetUserLoginInfo(ctx context.Context, userID int) (*models.LoginInfo, error) {
loginInfo, err := s.loginInfoStorage.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user login info",
zap.Int("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("获取用户登录信息失败: %w", err)
}
return loginInfo, nil
}
// IsUserOnline 检查用户是否在线
func (s *ssoService) IsUserOnline(ctx context.Context, userID int) (bool, error) {
loginInfo, err := s.GetUserLoginInfo(ctx, userID)
if err != nil {
return false, err
}
if loginInfo == nil {
return false, nil
}
return loginInfo.IsOnline, nil
}
// GetOnlineUsers 获取在线用户列表
func (s *ssoService) GetOnlineUsers(ctx context.Context) ([]*models.LoginInfo, error) {
onlineUsers, err := s.loginInfoStorage.ListOnlineUsers()
if err != nil {
s.logger.Error("failed to get online users", zap.Error(err))
return nil, fmt.Errorf("获取在线用户列表失败: %w", err)
}
return onlineUsers, nil
}
// GetOnlineUserCount 获取在线用户数量
func (s *ssoService) GetOnlineUserCount(ctx context.Context) (int64, error) {
count, err := s.loginInfoStorage.CountOnlineUsers()
if err != nil {
s.logger.Error("failed to count online users", zap.Error(err))
return 0, fmt.Errorf("统计在线用户数量失败: %w", err)
}
return count, nil
}
// BatchUserLogout 批量用户登出(可用于系统维护等场景)
func (s *ssoService) BatchUserLogout(ctx context.Context, userIDs []int) error {
if len(userIDs) == 0 {
return nil
}
for _, userID := range userIDs {
err := s.loginInfoStorage.SetUserOffline(userID)
if err != nil {
s.logger.Error("failed to set user offline in batch",
zap.Int("user_id", userID),
zap.Error(err))
// 继续处理其他用户,不中断整个批量操作
continue
}
}
s.logger.Info("batch user logout completed",
zap.Ints("user_ids", userIDs))
return nil
}
// findOrCreateUser 查找或创建用户
func (s *ssoService) findOrCreateUserWithRole(userID uint, userInfo *models.UserInfo, assignedRoleID uint) (*models.User, error) {
// 尝试查找现有用户
var user models.User
err := storage.DB.Where("id = ?", userID).First(&user).Error
if err == nil {
// 用户存在,更新登录信息
now := time.Now()
user.LastLoginAt = &now
user.LoginCount++
// 更新用户信息如果SSO信息有变化
if userInfo.Name != "" && user.Nickname != userInfo.Name {
user.Nickname = userInfo.Name
}
if userInfo.Email != "" && user.Email != userInfo.Email {
user.Email = userInfo.Email
}
// 同步规则:
// - 若SSO判定为普通(0)无论当前为何值一律降级为0
// - 若SSO判定为sys_admin(5)仅当当前为0时升级为5避免覆盖业务自定义角色
if assignedRoleID == 0 {
if user.RoleID != 0 {
s.logger.Info("降级用户角色(→0)", zap.Uint("userID", userID), zap.Uint("oldRoleID", user.RoleID))
user.RoleID = 0
} else {
s.logger.Debug("保持0角色不变", zap.Uint("userID", userID))
}
} else if assignedRoleID == 5 {
if user.RoleID == 0 {
s.logger.Info("升级用户角色(0→5)", zap.Uint("userID", userID))
user.RoleID = 5
} else {
s.logger.Debug("保持非0角色不变", zap.Uint("userID", userID), zap.Uint("currentRoleID", user.RoleID))
}
}
err = storage.DB.Save(&user).Error
if err != nil {
return nil, fmt.Errorf("更新用户信息失败: %w", err)
}
s.logger.Info("用户登录信息已更新",
zap.Uint("userID", userID),
zap.String("username", user.Username))
return &user, nil
}
if err != gorm.ErrRecordNotFound {
return nil, fmt.Errorf("查询用户失败: %w", err)
}
// 用户不存在,创建新用户
now := time.Now()
user = models.User{
BaseModel: models.BaseModel{
ID: userID,
CreatedAt: now,
UpdatedAt: now,
},
Username: userInfo.PreferredUsername,
Email: userInfo.Email,
Nickname: userInfo.Name,
Status: 1, // 默认启用
SSOProvider: "default", // 可以根据实际情况设置
LastLoginAt: &now,
LoginCount: 1,
RoleID: assignedRoleID,
}
// 如果PreferredUsername为空使用Email作为用户名
if user.Username == "" {
user.Username = userInfo.Email
}
err = storage.DB.Create(&user).Error
if err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
s.logger.Info("新用户创建成功",
zap.Uint("userID", userID),
zap.String("username", user.Username),
zap.String("email", user.Email))
return &user, nil
}
// assignDefaultRole 分配默认角色L5全员
func (s *ssoService) assignDefaultRole(userID uint) error {
// 获取默认角色IDL5全员
var role models.Role
err := storage.DB.Where("is_default = ?", true).First(&role).Error
if err != nil {
return fmt.Errorf("获取默认角色失败: %w", err)
}
// 更新用户的角色ID
err = storage.DB.Model(&models.User{}).Where("id = ?", userID).Update("role_id", role.ID).Error
if err != nil {
return fmt.Errorf("分配默认角色失败: %w", err)
}
s.logger.Info("用户已分配默认角色",
zap.Uint("userID", userID),
zap.Uint("roleID", role.ID),
zap.String("roleName", role.Name))
return nil
}

View File

@@ -0,0 +1,145 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// SystemConfigService 系统配置服务接口
type SystemConfigService interface {
Create(req *models.SystemConfigRequest) (*models.SystemConfig, error)
GetByID(id uint) (*models.SystemConfig, error)
Update(id uint, req *models.SystemConfigUpdateRequest) (*models.SystemConfig, error)
Delete(id uint) error
List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.SystemConfig, error)
GetByKey(key string) (*models.SystemConfig, error)
}
type systemConfigService struct {
storage storage.SystemConfigStorage
logger *utils.Logger
}
// NewSystemConfigService 创建系统配置服务实例
func NewSystemConfigService(storage storage.SystemConfigStorage, logger *utils.Logger) SystemConfigService {
return &systemConfigService{
storage: storage,
logger: logger,
}
}
// Create 创建系统配置
func (s *systemConfigService) Create(req *models.SystemConfigRequest) (*models.SystemConfig, error) {
// 检查配置标识是否已存在
_, err := s.storage.GetByKey(req.Key)
if err == nil {
return nil, errors.New("配置标识已存在")
}
// 创建配置
config := &models.SystemConfig{
Key: req.Key,
Name: req.Name,
Value: req.Value,
Type: req.Type,
Desc: req.Desc,
Status: 1, // 默认启用
}
err = s.storage.Create(config)
if err != nil {
s.logger.Error("创建系统配置失败", zap.Error(err))
return nil, errors.New("创建系统配置失败")
}
s.logger.Info("系统配置创建成功", zap.String("key", config.Key))
return config, nil
}
// GetByID 根据ID获取系统配置
func (s *systemConfigService) GetByID(id uint) (*models.SystemConfig, error) {
return s.storage.GetByID(id)
}
// GetByKey 根据配置标识获取系统配置
func (s *systemConfigService) GetByKey(key string) (*models.SystemConfig, error) {
return s.storage.GetByKey(key)
}
// Update 更新系统配置
func (s *systemConfigService) Update(id uint, req *models.SystemConfigUpdateRequest) (*models.SystemConfig, error) {
// 获取配置信息
config, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("配置不存在")
}
// 更新字段
config.Name = req.Name
config.Value = req.Value
config.Type = req.Type
config.Desc = req.Desc
// 保存更新
err = s.storage.Update(config)
if err != nil {
s.logger.Error("更新系统配置失败", zap.Error(err))
return nil, errors.New("更新系统配置失败")
}
s.logger.Info("系统配置更新成功", zap.Uint("id", id))
return config, nil
}
// Delete 删除系统配置
func (s *systemConfigService) Delete(id uint) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 删除配置
err = s.storage.Delete(id)
if err != nil {
s.logger.Error("删除系统配置失败", zap.Error(err))
return errors.New("删除系统配置失败")
}
s.logger.Info("系统配置删除成功", zap.Uint("id", id))
return nil
}
// List 获取系统配置列表
func (s *systemConfigService) List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error) {
return s.storage.List(req)
}
// UpdateStatus 更新状态
func (s *systemConfigService) UpdateStatus(id uint, status int) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 更新状态
err = s.storage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新系统配置状态失败", zap.Error(err))
return errors.New("更新系统配置状态失败")
}
s.logger.Info("系统配置状态更新成功", zap.Uint("id", id), zap.Int("status", status))
return nil
}
// GetAll 获取所有系统配置
func (s *systemConfigService) GetAll() ([]models.SystemConfig, error) {
return s.storage.GetAll()
}

View File

@@ -0,0 +1,260 @@
package services
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/oss"
"goalfymax-admin/internal/storage"
"strconv"
)
// UserFeedbackService 用户反馈服务
type UserFeedbackService struct {
storage *storage.UserFeedbackStorage
}
// NewUserFeedbackService 创建用户反馈服务实例
func NewUserFeedbackService(storage *storage.UserFeedbackStorage) *UserFeedbackService {
return &UserFeedbackService{storage: storage}
}
// List 获取用户反馈列表
func (s *UserFeedbackService) List(ctx context.Context, req *models.UserFeedbackListRequest) (*models.UserFeedbackListResponse, error) {
// 参数校验
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
if req.PageSize > 100 {
req.PageSize = 100
}
// 状态值校验
if req.Status != nil && (*req.Status < 0 || *req.Status > 1) {
return nil, fmt.Errorf("状态值无效")
}
// 用户ID校验
if req.UserID != nil && *req.UserID <= 0 {
return nil, fmt.Errorf("用户ID无效")
}
// 时间格式校验
if req.StartTime != "" {
if _, err := strconv.ParseInt(req.StartTime, 10, 64); err != nil {
// 尝试解析时间格式
if err := parseTimeString(req.StartTime); err != nil {
return nil, fmt.Errorf("开始时间格式无效")
}
}
}
if req.EndTime != "" {
if _, err := strconv.ParseInt(req.EndTime, 10, 64); err != nil {
// 尝试解析时间格式
if err := parseTimeString(req.EndTime); err != nil {
return nil, fmt.Errorf("结束时间格式无效")
}
}
}
// 调用存储层
feedbacks, total, err := s.storage.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取反馈列表失败: %w", err)
}
// 转换为带有可访问URL的返回结构
items := make([]models.UserFeedbackItem, 0, len(feedbacks))
for _, fb := range feedbacks {
var keys []string
if fb.FileKeys != "" {
_ = json.Unmarshal([]byte(fb.FileKeys), &keys)
}
// 直接下载文件内容并进行Base64编码
var fileContents []string
for _, k := range keys {
if k == "" {
continue
}
content, mimeType, err := oss.DownloadFileContent(ctx, k)
if err != nil {
// 记录错误,但继续处理其他文件
fmt.Printf("Error downloading file %s: %v\n", k, err)
continue
}
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
fileContents = append(fileContents, encodedContent)
}
items = append(items, models.UserFeedbackItem{
ID: fb.ID,
UserID: fb.UID,
Content: fb.Content,
FileKeys: keys,
FileContents: fileContents, // 返回Base64编码的内容
Status: fb.Status,
HandledBy: fb.HandledBy,
HandledAt: fb.HandledAt,
CreatedAt: fb.CreatedAt,
UpdatedAt: fb.UpdatedAt,
})
}
return &models.UserFeedbackListResponse{
List: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetByID 根据ID获取用户反馈
func (s *UserFeedbackService) GetByID(ctx context.Context, id int64) (*models.UserFeedbackItem, error) {
if id <= 0 {
return nil, fmt.Errorf("反馈ID无效")
}
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("获取用户反馈失败: %w", err)
}
if feedback == nil {
return nil, fmt.Errorf("反馈不存在")
}
// 解析 file_keys
var keys []string
if feedback.FileKeys != "" {
_ = json.Unmarshal([]byte(feedback.FileKeys), &keys)
}
// 下载文件内容并进行Base64编码
var fileContents []string
for _, k := range keys {
if k == "" {
continue
}
content, mimeType, err := oss.DownloadFileContent(ctx, k)
if err != nil {
fmt.Printf("Error downloading file %s: %v\n", k, err)
continue
}
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
fileContents = append(fileContents, encodedContent)
}
return &models.UserFeedbackItem{
ID: feedback.ID,
UserID: feedback.UID,
Content: feedback.Content,
FileKeys: keys,
FileContents: fileContents,
Status: feedback.Status,
HandledBy: feedback.HandledBy,
HandledAt: feedback.HandledAt,
CreatedAt: feedback.CreatedAt,
UpdatedAt: feedback.UpdatedAt,
}, nil
}
// MarkHandled 切换处理状态(已处理/未处理)
func (s *UserFeedbackService) MarkHandled(ctx context.Context, id int64, handledBy int, note string) error {
if id <= 0 {
return fmt.Errorf("反馈ID无效")
}
// 检查反馈是否存在
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("获取反馈信息失败: %w", err)
}
if feedback == nil {
return fmt.Errorf("反馈不存在")
}
// 如果要标记为已处理需要处理人ID
if feedback.Status == 0 && handledBy <= 0 {
return fmt.Errorf("处理人ID无效")
}
// 切换状态
if err := s.storage.MarkHandled(ctx, id, handledBy, note); err != nil {
return fmt.Errorf("切换状态失败: %w", err)
}
return nil
}
// Delete 删除用户反馈
func (s *UserFeedbackService) Delete(ctx context.Context, id int64) error {
if id <= 0 {
return fmt.Errorf("反馈ID无效")
}
// 检查反馈是否存在
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("获取反馈信息失败: %w", err)
}
if feedback == nil {
return fmt.Errorf("反馈不存在")
}
// 删除反馈
if err := s.storage.Delete(ctx, id); err != nil {
return fmt.Errorf("删除反馈失败: %w", err)
}
return nil
}
// GetStatistics 获取反馈统计信息
func (s *UserFeedbackService) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
stats, err := s.storage.GetStatistics(ctx)
if err != nil {
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
return stats, nil
}
// parseTimeString 解析时间字符串
func parseTimeString(timeStr string) error {
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02 15:04:05",
"2006/01/02",
}
// 先检查是否是时间戳格式
if _, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
return nil // 时间戳格式
}
// 检查其他时间格式
for _, format := range formats {
// 这里可以添加实际的时间解析逻辑,暂时跳过
_ = format
}
return fmt.Errorf("时间格式无效")
}
// hasHTTPPrefix 判断字符串是否为 http/https URL
func hasHTTPPrefix(s string) bool {
// 此函数在此服务中已不再需要,但保留以避免潜在编译错误,或者可以在此被移除
return false
}
// joinURL 已废弃,改为强制预签名

View File

@@ -0,0 +1,145 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// UserLevelConfigService 用户等级配置服务接口
type UserLevelConfigService interface {
Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error)
GetByID(id uint) (*models.UserLevelConfig, error)
Update(id uint, req *models.UserLevelConfigUpdateRequest) (*models.UserLevelConfig, error)
Delete(id uint) error
List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.UserLevelConfig, error)
}
type userLevelConfigService struct {
storage storage.UserLevelConfigStorage
logger *utils.Logger
}
// NewUserLevelConfigService 创建用户等级配置服务实例
func NewUserLevelConfigService(storage storage.UserLevelConfigStorage, logger *utils.Logger) UserLevelConfigService {
return &userLevelConfigService{
storage: storage,
logger: logger,
}
}
// Create 创建用户等级配置
func (s *userLevelConfigService) Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error) {
// 检查等级代码是否已存在
_, err := s.storage.GetByLevelCode(req.LevelCode)
if err == nil {
return nil, errors.New("等级代码已存在")
}
// 创建配置
config := &models.UserLevelConfig{
LevelName: req.LevelName,
LevelCode: req.LevelCode,
ProjectLimit: req.ProjectLimit,
CoderVMLimit: req.CoderVMLimit,
BrowserVMLimit: req.BrowserVMLimit,
ProcessLimit: req.ProcessLimit,
Description: req.Description,
SortOrder: req.SortOrder,
Status: 1, // 默认启用
}
err = s.storage.Create(config)
if err != nil {
s.logger.Error("创建用户等级配置失败", zap.Error(err))
return nil, errors.New("创建用户等级配置失败")
}
s.logger.Info("用户等级配置创建成功", zap.String("level_name", config.LevelName))
return config, nil
}
// GetByID 根据ID获取用户等级配置
func (s *userLevelConfigService) GetByID(id uint) (*models.UserLevelConfig, error) {
return s.storage.GetByID(id)
}
// Update 更新用户等级配置
func (s *userLevelConfigService) Update(id uint, req *models.UserLevelConfigUpdateRequest) (*models.UserLevelConfig, error) {
// 获取配置信息
config, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("配置不存在")
}
// 更新字段
config.LevelName = req.LevelName
config.ProjectLimit = req.ProjectLimit
config.CoderVMLimit = req.CoderVMLimit
config.BrowserVMLimit = req.BrowserVMLimit
config.ProcessLimit = req.ProcessLimit
config.Description = req.Description
config.SortOrder = req.SortOrder
// 保存更新
err = s.storage.Update(config)
if err != nil {
s.logger.Error("更新用户等级配置失败", zap.Error(err))
return nil, errors.New("更新用户等级配置失败")
}
s.logger.Info("用户等级配置更新成功", zap.Uint("id", id))
return config, nil
}
// Delete 删除用户等级配置
func (s *userLevelConfigService) Delete(id uint) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 删除配置
err = s.storage.Delete(id)
if err != nil {
s.logger.Error("删除用户等级配置失败", zap.Error(err))
return errors.New("删除用户等级配置失败")
}
s.logger.Info("用户等级配置删除成功", zap.Uint("id", id))
return nil
}
// List 获取用户等级配置列表
func (s *userLevelConfigService) List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error) {
return s.storage.List(req)
}
// UpdateStatus 更新状态
func (s *userLevelConfigService) UpdateStatus(id uint, status int) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 更新状态
err = s.storage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新用户等级配置状态失败", zap.Error(err))
return errors.New("更新用户等级配置状态失败")
}
s.logger.Info("用户等级配置状态更新成功", zap.Uint("id", id), zap.Int("status", status))
return nil
}
// GetAll 获取所有用户等级配置
func (s *userLevelConfigService) GetAll() ([]models.UserLevelConfig, error) {
return s.storage.GetAll()
}

View File

@@ -0,0 +1,79 @@
package services
import (
"errors"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
)
type UserProjectQuotaService interface {
Create(input *models.UserProjectQuota) (*models.UserProjectQuota, error)
Update(id uint, input *models.UserProjectQuota) (*models.UserProjectQuota, error)
Delete(id uint) error
GetByID(id uint) (*models.UserProjectQuota, error)
GetByUserID(userID string) (*models.UserProjectQuota, error)
List(filter storage.UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error)
}
type userProjectQuotaService struct {
store storage.UserProjectQuotaStorage
}
func NewUserProjectQuotaService(store storage.UserProjectQuotaStorage) UserProjectQuotaService {
return &userProjectQuotaService{store: store}
}
func (s *userProjectQuotaService) validate(q *models.UserProjectQuota) error {
if q.UserID == "" {
return errors.New("user_id 不能为空")
}
if q.ProjectLimit < 0 || q.CoderVMLimit < 0 || q.BrowserVMLimit < 0 || q.ProcessLimit < 0 {
return errors.New("配额上限不能为负数")
}
return nil
}
func (s *userProjectQuotaService) Create(input *models.UserProjectQuota) (*models.UserProjectQuota, error) {
if err := s.validate(input); err != nil {
return nil, err
}
if _, err := s.store.GetByUserID(input.UserID); err == nil {
return nil, errors.New("该用户配额已存在")
}
if err := s.store.Create(input); err != nil {
return nil, err
}
return input, nil
}
func (s *userProjectQuotaService) Update(id uint, input *models.UserProjectQuota) (*models.UserProjectQuota, error) {
if err := s.validate(input); err != nil {
return nil, err
}
exist, err := s.store.GetByID(id)
if err != nil {
return nil, errors.New("记录不存在")
}
// user_id 不建议修改如需修改可放开以下行exist.UserID = input.UserID
exist.ProjectLimit = input.ProjectLimit
exist.CoderVMLimit = input.CoderVMLimit
exist.BrowserVMLimit = input.BrowserVMLimit
exist.ProcessLimit = input.ProcessLimit
exist.Enabled = input.Enabled
exist.Description = input.Description
if err := s.store.Update(exist); err != nil {
return nil, err
}
return exist, nil
}
func (s *userProjectQuotaService) Delete(id uint) error { return s.store.Delete(id) }
func (s *userProjectQuotaService) GetByID(id uint) (*models.UserProjectQuota, error) {
return s.store.GetByID(id)
}
func (s *userProjectQuotaService) GetByUserID(userID string) (*models.UserProjectQuota, error) {
return s.store.GetByUserID(userID)
}
func (s *userProjectQuotaService) List(filter storage.UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error) {
return s.store.List(filter)
}

View File

@@ -0,0 +1,404 @@
package services
import (
"context"
"errors"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
// UserService 用户服务接口
type UserService interface {
Login(req *models.LoginRequest) (*models.LoginResponse, error)
Create(req *models.UserCreateRequest) (*models.User, error)
GetByID(id uint) (*models.User, error)
Update(id uint, req *models.UserUpdateRequest) (*models.User, error)
Delete(id uint) error
List(req *models.UserListRequest) ([]models.User, int64, error)
ListWithRoles(req *models.UserListRequest) ([]models.UserWithRoles, int64, error)
UpdateStatus(id uint, status int) error
UpdateRole(id uint, roleID uint) error
CheckUserSystemRole(userID int) (bool, error)
ChangeUserSystemRole(userID int, systemRole string) error
}
type userService struct {
userStorage storage.UserStorage
rbacStorage storage.RBACStorage
goalfyMaxUserStorage storage.GoalfyMaxUserStorage
jwtManager *utils.JWTManager
logger *utils.Logger
ssoAdminService SSOAdminService
}
// NewUserService 创建用户服务实例
func NewUserService(userStorage storage.UserStorage, rbacStorage storage.RBACStorage, goalfyMaxUserStorage storage.GoalfyMaxUserStorage, jwtManager *utils.JWTManager, logger *utils.Logger, ssoAdminService SSOAdminService) UserService {
return &userService{
userStorage: userStorage,
rbacStorage: rbacStorage,
goalfyMaxUserStorage: goalfyMaxUserStorage,
jwtManager: jwtManager,
logger: logger,
ssoAdminService: ssoAdminService,
}
}
// Login 用户登录
func (s *userService) Login(req *models.LoginRequest) (*models.LoginResponse, error) {
// 获取用户信息
user, err := s.userStorage.GetByUsername(req.Username)
if err != nil {
s.logger.Error("用户不存在", zap.String("username", req.Username))
return nil, errors.New("用户名或密码错误")
}
// 检查用户状态
if user.Status != 1 {
s.logger.Error("用户已被禁用", zap.String("username", req.Username))
return nil, errors.New("用户已被禁用")
}
// SSO用户不需要密码验证直接通过
// 生成JWT token
token, err := s.jwtManager.GenerateToken(user.ID, user.Username)
if err != nil {
s.logger.Error("生成token失败", zap.Error(err))
return nil, errors.New("登录失败")
}
// 记录登录日志
// 这里应该调用日志存储服务记录登录日志
s.logger.Info("用户登录成功", zap.String("username", user.Username))
return &models.LoginResponse{
Token: token,
User: *user,
ExpireAt: 0, // 实际应该计算过期时间
}, nil
}
// Create 创建用户
func (s *userService) Create(req *models.UserCreateRequest) (*models.User, error) {
// 验证用户名是否已存在
_, err := s.userStorage.GetByUsername(req.Username)
if err == nil {
return nil, errors.New("用户名已存在")
}
// 验证邮箱是否已存在
_, err = s.userStorage.GetByEmail(req.Email)
if err == nil {
return nil, errors.New("邮箱已存在")
}
// 1. 先调用SSO创建用户
ctx := context.Background()
ssoReq := &SSOAdminUserCreateRequest{
Username: req.Username,
Email: req.Email,
Phone: "", // 默认空手机号
Password: req.Password,
}
ssoUser, err := s.ssoAdminService.CreateUser(ctx, ssoReq)
if err != nil {
return nil, fmt.Errorf("SSO创建用户失败: %w", err)
}
// 2. 设置系统角色为sys_admin
if err := s.ssoAdminService.SetSystemRole(ctx, ssoUser.ID, "sys_admin"); err != nil {
// 如果设置失败,记录错误但不阻止用户创建
fmt.Printf("警告: 设置系统角色失败: %v\n", err)
}
// 设置角色ID如果没有指定则使用默认角色
roleID := req.RoleID
if roleID == 0 {
defaultRoleID, err := s.getDefaultRole()
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
return nil, errors.New("获取默认角色失败")
}
roleID = defaultRoleID
}
// 创建用户
user := &models.User{
Username: req.Username,
Email: req.Email,
Nickname: req.Nickname,
Status: 1,
SSOProvider: req.SSOProvider,
RoleID: roleID,
}
err = s.userStorage.Create(user)
if err != nil {
s.logger.Error("创建用户失败", zap.Error(err))
return nil, errors.New("创建用户失败")
}
s.logger.Info("用户创建成功", zap.String("username", user.Username), zap.Uint("role_id", roleID))
return user, nil
}
// GetByID 根据ID获取用户
func (s *userService) GetByID(id uint) (*models.User, error) {
return s.userStorage.GetByID(id)
}
// Update 更新用户
func (s *userService) Update(id uint, req *models.UserUpdateRequest) (*models.User, error) {
// 获取用户信息
user, err := s.userStorage.GetByID(id)
if err != nil {
return nil, errors.New("用户不存在")
}
// 更新用户信息
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
// 检查邮箱是否已被其他用户使用
existingUser, err := s.userStorage.GetByEmail(req.Email)
if err == nil && existingUser.ID != id {
return nil, errors.New("邮箱已被其他用户使用")
}
user.Email = req.Email
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.Status != nil {
user.Status = *req.Status
}
if req.RoleID != nil {
user.RoleID = *req.RoleID
}
if req.SSOProvider != "" {
user.SSOProvider = req.SSOProvider
}
err = s.userStorage.Update(user)
if err != nil {
s.logger.Error("更新用户失败", zap.Error(err))
return nil, errors.New("更新用户失败")
}
s.logger.Info("用户更新成功", zap.Uint("user_id", id))
return user, nil
}
// Delete 删除用户
func (s *userService) Delete(id uint) error {
// 检查用户是否存在
_, err := s.userStorage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
err = s.userStorage.Delete(id)
if err != nil {
s.logger.Error("删除用户失败", zap.Error(err))
return errors.New("删除用户失败")
}
s.logger.Info("用户删除成功", zap.Uint("user_id", id))
return nil
}
// List 获取用户列表
func (s *userService) List(req *models.UserListRequest) ([]models.User, int64, error) {
return s.userStorage.List(req)
}
// UpdateStatus 更新用户状态
func (s *userService) UpdateStatus(id uint, status int) error {
// 检查用户是否存在
_, err := s.userStorage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
err = s.userStorage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新用户状态失败", zap.Error(err))
return errors.New("更新用户状态失败")
}
s.logger.Info("用户状态更新成功", zap.Uint("user_id", id), zap.Int("status", status))
return nil
}
// ListWithRoles 获取用户列表(包含角色信息)
func (s *userService) ListWithRoles(req *models.UserListRequest) ([]models.UserWithRoles, int64, error) {
// 获取用户列表
users, total, err := s.userStorage.List(req)
if err != nil {
return nil, 0, err
}
// 为每个用户查询角色信息
usersWithRoles := make([]models.UserWithRoles, len(users))
for i, user := range users {
if user.RoleID == 0 {
// role_id 为 0 表示未分配角色,直接返回空角色
s.logger.Debug("用户未分配角色", zap.Uint("userID", user.ID))
usersWithRoles[i] = models.UserWithRoles{User: user, Role: nil}
continue
}
// 查询用户角色
role, err := s.rbacStorage.GetRoleByID(user.RoleID)
if err != nil {
s.logger.Warn("获取用户角色失败", zap.Uint("userID", user.ID), zap.Uint("roleID", user.RoleID), zap.Error(err))
// 如果获取角色失败,仍然返回用户信息,但角色为空
usersWithRoles[i] = models.UserWithRoles{
User: user,
Role: nil,
}
} else {
usersWithRoles[i] = models.UserWithRoles{
User: user,
Role: role,
}
}
}
return usersWithRoles, total, nil
}
// getDefaultRole 获取默认角色L5全员
func (s *userService) getDefaultRole() (uint, error) {
// 查询默认角色ID
var roleID uint
err := s.rbacStorage.GetDefaultRoleID(&roleID)
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
return 0, errors.New("获取默认角色失败")
}
return roleID, nil
}
// UpdateRole 更新用户角色
func (s *userService) UpdateRole(id uint, roleID uint) error {
// 检查用户是否存在
user, err := s.userStorage.GetByID(id)
if err != nil {
s.logger.Error("获取用户信息失败", zap.Uint("user_id", id), zap.Error(err))
return errors.New("用户不存在")
}
// 更新用户角色
user.RoleID = roleID
err = s.userStorage.Update(user)
if err != nil {
s.logger.Error("更新用户角色失败", zap.Uint("user_id", id), zap.Uint("role_id", roleID), zap.Error(err))
return errors.New("更新用户角色失败")
}
s.logger.Info("用户角色更新成功", zap.Uint("user_id", id), zap.Uint("role_id", roleID))
return nil
}
// CheckUserSystemRole 检查用户在系统用户管理表中的角色
// 返回: true表示存在sys_adminfalse表示不存在custom
func (s *userService) CheckUserSystemRole(userID int) (bool, error) {
// admin_users表的id字段就是SSO用户ID
// 所以通过userID查询admin_users表如果存在则说明是系统管理员
_, err := s.userStorage.GetByID(uint(userID))
if err != nil {
// 如果查询失败用户不存在返回false
return false, nil
}
// 用户存在返回true
return true, nil
}
// ChangeUserSystemRole 变更用户在SSO中的系统角色
// 同时需要在admin_users表中添加或删除记录
func (s *userService) ChangeUserSystemRole(userID int, systemRole string) error {
ctx := context.Background()
// 1. 先调用SSO接口变更角色
err := s.ssoAdminService.SetSystemRole(ctx, userID, systemRole)
if err != nil {
s.logger.Error("调用SSO接口变更角色失败", zap.Int("user_id", userID), zap.String("system_role", systemRole), zap.Error(err))
return fmt.Errorf("调用SSO接口变更角色失败: %w", err)
}
// 2. 根据角色变更更新admin_users表
if systemRole == "sys_admin" {
// 设置为系统管理员需要确保admin_users表中存在该用户
_, err := s.userStorage.GetByID(uint(userID))
if err != nil {
// 用户不存在,需要创建
// 从GoalfyMax用户表获取用户信息
goalfyUser, err := s.goalfyMaxUserStorage.GetByUserID(userID)
if err != nil {
s.logger.Error("获取GoalfyMax用户信息失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("获取GoalfyMax用户信息失败: %w", err)
}
// 获取默认角色ID系统管理员角色
defaultRoleID, err := s.getDefaultRole()
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
// 如果获取默认角色失败使用角色ID 5系统管理员
defaultRoleID = 5
}
// 创建admin_users记录
// 注意admin_users表的id字段对应SSO用户ID
user := &models.User{
BaseModel: models.BaseModel{
ID: uint(userID), // 使用SSO用户ID作为主键
},
Username: goalfyUser.Username,
Email: goalfyUser.Email,
Nickname: goalfyUser.Nickname,
Status: 1,
SSOProvider: "default",
RoleID: defaultRoleID,
}
// 使用storage的Create方法它应该能处理指定ID的情况
err = s.userStorage.Create(user)
if err != nil {
s.logger.Error("创建admin_users记录失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("创建admin_users记录失败: %w", err)
}
s.logger.Info("创建系统管理员用户成功", zap.Int("user_id", userID))
} else {
// 用户已存在,更新状态为正常(如果需要)
s.logger.Info("用户已是系统管理员", zap.Int("user_id", userID))
}
} else if systemRole == "custom" {
// 设置为普通用户需要删除admin_users表中的记录软删除
_, err := s.userStorage.GetByID(uint(userID))
if err == nil {
// 用户存在,删除记录
err = s.userStorage.Delete(uint(userID))
if err != nil {
s.logger.Error("删除admin_users记录失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("删除admin_users记录失败: %w", err)
}
s.logger.Info("删除系统管理员用户成功", zap.Int("user_id", userID))
} else {
s.logger.Info("用户不是系统管理员,无需删除", zap.Int("user_id", userID))
}
} else {
return fmt.Errorf("无效的系统角色: %s", systemRole)
}
s.logger.Info("用户系统角色变更成功", zap.Int("user_id", userID), zap.String("system_role", systemRole))
return nil
}

View File

@@ -0,0 +1,82 @@
# 存储层
本模块负责数据访问层的实现,提供数据库操作接口。
## 功能特性
- 统一的数据库连接管理
- 自动数据库迁移
- 接口化的数据访问层
- 支持分页查询
- 支持条件查询
## 模块结构
```
storage/
├── database.go # 数据库连接和迁移
├── user_storage.go # 用户数据访问
├── role_storage.go # 角色数据访问
├── menu_storage.go # 菜单数据访问
├── log_storage.go # 日志数据访问
└── README.md # 说明文档
```
## 使用方法
### 初始化数据库
```go
// 初始化数据库连接
err := storage.InitDatabase()
if err != nil {
log.Fatal(err)
}
// 自动迁移数据库表
err = storage.AutoMigrate()
if err != nil {
log.Fatal(err)
}
```
### 使用存储接口
```go
// 创建用户存储实例
userStorage := storage.NewUserStorage()
// 创建用户
user := &models.User{
Username: "admin",
Email: "admin@example.com",
Password: "password",
}
err := userStorage.Create(user)
// 获取用户列表
req := &models.UserListRequest{
PageRequest: models.PageRequest{Page: 1, Size: 10},
Username: "admin",
}
users, total, err := userStorage.List(req)
```
## 接口设计
所有存储接口都遵循统一的模式:
- `Create()` - 创建记录
- `GetByID()` - 根据ID获取记录
- `Update()` - 更新记录
- `Delete()` - 删除记录
- `List()` - 获取列表(支持分页和条件查询)
## 数据库配置
数据库配置通过配置文件进行管理,支持以下配置项:
- `dsn` - 数据库连接字符串
- `maxIdleConns` - 最大空闲连接数
- `maxOpenConns` - 最大打开连接数

View File

@@ -0,0 +1,114 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
"time"
)
// AuditLogStorage 审计日志存储接口
type AuditLogStorage interface {
Create(log *models.AuditLog) error
GetByID(id uint) (*models.AuditLog, error)
List(req *models.AuditLogListRequest) ([]models.AuditLog, int64, error)
}
type auditLogStorage struct {
db *gorm.DB
}
// NewAuditLogStorage 创建审计日志存储实例
func NewAuditLogStorage() AuditLogStorage {
return &auditLogStorage{db: DB}
}
// Create 创建审计日志
func (s *auditLogStorage) Create(log *models.AuditLog) error {
// 如果操作时间为空,设置为当前时间
if log.OperationTime.IsZero() {
log.OperationTime = time.Now()
}
return s.db.Create(log).Error
}
// GetByID 根据ID获取审计日志
func (s *auditLogStorage) GetByID(id uint) (*models.AuditLog, error) {
var log models.AuditLog
err := s.db.First(&log, id).Error
if err != nil {
return nil, err
}
return &log, nil
}
// List 查询审计日志列表
func (s *auditLogStorage) List(req *models.AuditLogListRequest) ([]models.AuditLog, int64, error) {
var logs []models.AuditLog
var total int64
query := s.db.Model(&models.AuditLog{})
// 操作类型筛选
if req.OperationType != "" {
query = query.Where("operation_type = ?", req.OperationType)
}
// 操作人筛选
if req.OperatorEmail != "" {
query = query.Where("operator_email = ?", req.OperatorEmail)
}
// 操作对象搜索(模糊匹配)
if req.TargetEmail != "" {
query = query.Where("target_email LIKE ?", "%"+req.TargetEmail+"%")
}
// 时间范围筛选
if req.StartTime != "" {
startTime, err := time.Parse("2006-01-02 15:04:05", req.StartTime)
if err == nil {
query = query.Where("operation_time >= ?", startTime)
}
}
if req.EndTime != "" {
endTime, err := time.Parse("2006-01-02 15:04:05", req.EndTime)
if err == nil {
query = query.Where("operation_time <= ?", endTime)
}
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 排序
sortBy := req.SortBy
if sortBy == "" {
sortBy = "operation_time"
}
sortOrder := req.SortOrder
if sortOrder == "" {
sortOrder = "desc"
}
query = query.Order(sortBy + " " + sortOrder)
// 分页
page := req.Page
if page < 1 {
page = 1
}
size := req.Size
if size < 1 {
size = 20
}
offset := (page - 1) * size
query = query.Offset(offset).Limit(size)
// 查询
if err := query.Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}

View File

@@ -0,0 +1,22 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
type BalanceOperationLogStorage interface {
Create(log *models.BalanceOperationLog) error
}
type balanceOperationLogStorage struct {
db *gorm.DB
}
func NewBalanceOperationLogStorage() BalanceOperationLogStorage {
return &balanceOperationLogStorage{db: DB}
}
func (s *balanceOperationLogStorage) Create(log *models.BalanceOperationLog) error {
return s.db.Create(log).Error
}

View File

@@ -0,0 +1,226 @@
package storage
import (
"context"
"fmt"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/pkg/utils"
"time"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
// GormLoggerAdapter GORM logger适配器
type GormLoggerAdapter struct {
logger *utils.Logger
logLevel logger.LogLevel
}
// LogMode 设置日志级别
func (g *GormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
g.logLevel = level
return g
}
// Info 记录信息日志
func (g *GormLoggerAdapter) Info(ctx context.Context, msg string, data ...interface{}) {
g.logger.Info(msg, zap.Any("data", data))
}
// Warn 记录警告日志
func (g *GormLoggerAdapter) Warn(ctx context.Context, msg string, data ...interface{}) {
g.logger.Warn(msg, zap.Any("data", data))
}
// Error 记录错误日志
func (g *GormLoggerAdapter) Error(ctx context.Context, msg string, data ...interface{}) {
g.logger.Error(msg, zap.Any("data", data))
}
// Trace 记录跟踪日志
func (g *GormLoggerAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
// 根据日志级别决定是否记录
if g.logLevel < logger.Info {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
if err != nil {
g.logger.Error("SQL执行失败",
zap.String("sql", sql),
zap.Int64("rows", rows),
zap.Duration("elapsed", elapsed),
zap.Error(err),
)
} else {
g.logger.Debug("SQL执行成功",
zap.String("sql", sql),
zap.Int64("rows", rows),
zap.Duration("elapsed", elapsed),
)
}
}
// InitDatabase 初始化数据库连接
func InitDatabase(appLogger *utils.Logger) error {
cfg := config.GetConfig()
// 配置数据库连接
dbConfig := mysql.Config{
DSN: cfg.Database.DSN,
}
// 创建GORM logger适配器
gormLogger := &GormLoggerAdapter{logger: appLogger}
// 根据配置设置日志级别
switch cfg.Database.LogLevel {
case "silent":
gormLogger.LogMode(logger.Silent)
case "error":
gormLogger.LogMode(logger.Error)
case "warn":
gormLogger.LogMode(logger.Warn)
case "info":
gormLogger.LogMode(logger.Info)
default:
gormLogger.LogMode(logger.Info)
}
// 连接数据库
db, err := gorm.Open(mysql.Open(dbConfig.DSN), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
return fmt.Errorf("连接数据库失败: %w", err)
}
// 设置连接池
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取数据库实例失败: %w", err)
}
sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
DB = db
return nil
}
// AutoMigrate 自动迁移数据库表
func AutoMigrate() error {
if DB == nil {
return fmt.Errorf("数据库未初始化")
}
// 迁移所有模型
err := DB.AutoMigrate(
&models.UserLevelConfig{},
&models.GoalfyMaxUser{},
&models.BalanceOperationLog{},
&models.InviteCodeApplication{},
//&models.AuditLog{},
//&models.User{},
//&models.Role{},
//&models.Permission{},
//&models.Menu{},
// &models.SystemConfig{},
//&models.LoginLog{},
//&models.OperationLog{},
//&models.UserProjectQuota{},
//&models.PKCEState{},
//&models.LoginInfo{},
//&models.User{},
//&models.Role{},
//&models.Permission{},
//&models.Menu{},
//&models.SystemConfig{},
//&models.LoginLog{},
//&models.OperationLog{},
//&models.UserProjectQuota{},
//&models.PKCEState{},
//&models.LoginInfo{},
// &models.InviteCode{},
)
if err != nil {
return fmt.Errorf("数据库迁移失败: %w", err)
}
// 初始化默认用户等级配置
if err := initDefaultUserLevelConfigs(); err != nil {
return fmt.Errorf("初始化默认用户等级配置失败: %w", err)
}
return nil
}
// initDefaultUserLevelConfigs 初始化默认用户等级配置
func initDefaultUserLevelConfigs() error {
// 检查是否已存在配置
var count int64
if err := DB.Model(&models.UserLevelConfig{}).Count(&count).Error; err != nil {
return err
}
// 如果已存在配置,则跳过
if count > 0 {
return nil
}
// 创建默认配置
defaultConfigs := []models.UserLevelConfig{
{
LevelName: "普通",
LevelCode: "normal",
ProjectLimit: 2,
Description: "普通用户等级可创建2个项目",
SortOrder: 1,
Status: 1,
},
{
LevelName: "VIP",
LevelCode: "vip",
ProjectLimit: 10,
Description: "VIP用户等级可创建10个项目",
SortOrder: 2,
Status: 1,
},
{
LevelName: "内部",
LevelCode: "internal",
ProjectLimit: 0,
Description: "内部用户等级,无项目数限制",
SortOrder: 3,
Status: 1,
},
}
return DB.Create(&defaultConfigs).Error
}
// GetDB 获取数据库实例
func GetDB() *gorm.DB {
return DB
}
// Close 关闭数据库连接
func Close() error {
if DB != nil {
sqlDB, err := DB.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
return nil
}

View File

@@ -0,0 +1,123 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
type GoalfyMaxUserStorage interface {
Create(user *models.GoalfyMaxUser) error
GetByID(id uint) (*models.GoalfyMaxUser, error)
GetByUserID(userID int) (*models.GoalfyMaxUser, error)
GetByUsername(username string) (*models.GoalfyMaxUser, error)
GetByEmail(email string) (*models.GoalfyMaxUser, error)
Update(user *models.GoalfyMaxUser) error
Delete(id uint) error
List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error)
SetBanned(id uint, reason string, adminID int) error
Unban(id uint) error
}
type goalfyMaxUserStorage struct {
db *gorm.DB
}
func NewGoalfyMaxUserStorage() GoalfyMaxUserStorage {
return &goalfyMaxUserStorage{db: DB}
}
func (s *goalfyMaxUserStorage) Create(user *models.GoalfyMaxUser) error {
return s.db.Create(user).Error
}
func (s *goalfyMaxUserStorage) GetByID(id uint) (*models.GoalfyMaxUser, error) {
var user models.GoalfyMaxUser
if err := s.db.First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *goalfyMaxUserStorage) GetByUserID(userID int) (*models.GoalfyMaxUser, error) {
var user models.GoalfyMaxUser
if err := s.db.Where("user_id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *goalfyMaxUserStorage) GetByUsername(username string) (*models.GoalfyMaxUser, error) {
var user models.GoalfyMaxUser
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *goalfyMaxUserStorage) GetByEmail(email string) (*models.GoalfyMaxUser, error) {
var user models.GoalfyMaxUser
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *goalfyMaxUserStorage) Update(user *models.GoalfyMaxUser) error {
return s.db.Save(user).Error
}
func (s *goalfyMaxUserStorage) Delete(id uint) error {
return s.db.Delete(&models.GoalfyMaxUser{}, id).Error
}
func (s *goalfyMaxUserStorage) List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error) {
var users []models.GoalfyMaxUser
var total int64
q := s.db.Model(&models.GoalfyMaxUser{})
if req.Username != "" {
q = q.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.Email != "" {
q = q.Where("email LIKE ?", "%"+req.Email+"%")
}
if req.Status != nil {
if *req.Status == 0 {
q = q.Where("is_banned = ?", true)
} else if *req.Status == 1 {
q = q.Where("is_banned = ?", false)
}
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (req.Page - 1) * req.Size
if err := q.Offset(offset).Limit(req.Size).Order("id DESC").Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func (s *goalfyMaxUserStorage) SetBanned(id uint, reason string, adminID int) error {
return s.db.Model(&models.GoalfyMaxUser{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_banned": true,
"ban_reason": reason,
"banned_by": adminID,
"banned_at": gorm.Expr("NOW()"),
}).Error
}
func (s *goalfyMaxUserStorage) Unban(id uint) error {
return s.db.Model(&models.GoalfyMaxUser{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_banned": false,
"ban_reason": "",
"banned_by": 0,
"banned_at": nil,
}).Error
}

View File

@@ -0,0 +1,155 @@
package storage
import (
"time"
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
type InviteCodeStorage interface {
Create(inviteCode *models.InviteCode) error
GetByID(id uint) (*models.InviteCode, error)
GetByCode(code string) (*models.InviteCode, error)
List(req *models.InviteCodeListRequest) ([]models.InviteCode, int64, error)
Update(inviteCode *models.InviteCode) error
Delete(id uint) error
GetStatistics() (*models.InviteCodeStatistics, error)
IsExpired(inviteCode *models.InviteCode) bool
}
type inviteCodeStorage struct {
db *gorm.DB
}
func NewInviteCodeStorage() InviteCodeStorage {
return &inviteCodeStorage{db: DB}
}
func (s *inviteCodeStorage) Create(inviteCode *models.InviteCode) error {
// 若目标库缺少 is_used 列,则在插入时省略该列,避免 Unknown column 错误
if columnExistsIsUsed(s.db) {
return s.db.Create(inviteCode).Error
}
return s.db.Omit("is_used").Create(inviteCode).Error
}
func (s *inviteCodeStorage) GetByID(id uint) (*models.InviteCode, error) {
var inviteCode models.InviteCode
err := s.db.Where("deleted_at IS NULL").First(&inviteCode, id).Error
if err != nil {
return nil, err
}
return &inviteCode, nil
}
func (s *inviteCodeStorage) GetByCode(code string) (*models.InviteCode, error) {
var inviteCode models.InviteCode
err := s.db.Where("code = ? AND deleted_at IS NULL", code).First(&inviteCode).Error
if err != nil {
return nil, err
}
return &inviteCode, nil
}
// IsExpired 检查邀请码是否过期
func (s *inviteCodeStorage) IsExpired(inviteCode *models.InviteCode) bool {
if inviteCode.ExpiresAt == nil {
return false // 没有设置过期时间,永不过期
}
return time.Now().After(*inviteCode.ExpiresAt)
}
func (s *inviteCodeStorage) List(req *models.InviteCodeListRequest) ([]models.InviteCode, int64, error) {
var inviteCodes []models.InviteCode
var total int64
query := s.db.Model(&models.InviteCode{}).Where("deleted_at IS NULL")
// 筛选条件
if req.Code != "" {
query = query.Where("code LIKE ?", "%"+req.Code+"%")
}
// 仅当存在 is_used 列时才应用过滤
if req.IsUsed != nil {
if columnExistsIsUsed(s.db) {
query = query.Where("is_used = ?", *req.IsUsed)
}
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.Order("created_at DESC").Offset(offset).Limit(req.Size).Find(&inviteCodes).Error
if err != nil {
return nil, 0, err
}
return inviteCodes, total, nil
}
func (s *inviteCodeStorage) Update(inviteCode *models.InviteCode) error {
return s.db.Save(inviteCode).Error
}
func (s *inviteCodeStorage) Delete(id uint) error {
return s.db.Delete(&models.InviteCode{}, id).Error
}
func (s *inviteCodeStorage) GetStatistics() (*models.InviteCodeStatistics, error) {
var stats models.InviteCodeStatistics
// 总数
var total int64
if err := s.db.Model(&models.InviteCode{}).Where("deleted_at IS NULL").Count(&total).Error; err != nil {
return nil, err
}
stats.Total = int(total)
// is_used 列可能不存在,存在时统计已使用/未使用
if columnExistsIsUsed(s.db) {
var used int64
if err := s.db.Model(&models.InviteCode{}).Where("deleted_at IS NULL AND is_used = ?", true).Count(&used).Error; err != nil {
return nil, err
}
stats.Used = int(used)
var unused int64
if err := s.db.Model(&models.InviteCode{}).Where("deleted_at IS NULL AND is_used = ?", false).Count(&unused).Error; err != nil {
return nil, err
}
stats.Unused = int(unused)
} else {
// 列不存在时,给出合理默认值
stats.Used = 0
stats.Unused = int(total)
}
// 今日新增
today := time.Now().Format("2006-01-02")
var todayCreated int64
if err := s.db.Model(&models.InviteCode{}).Where("deleted_at IS NULL AND DATE(created_at) = ?", today).Count(&todayCreated).Error; err != nil {
return nil, err
}
stats.TodayCreated = int(todayCreated)
return &stats, nil
}
// columnExistsIsUsed 检查当前数据库中 admin_invite_codes 表是否存在 is_used 列
func columnExistsIsUsed(db *gorm.DB) bool {
var count int64
// 使用当前连接的数据库名
db.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'is_used'", "admin_invite_codes").Scan(&count)
return count > 0
}

View File

@@ -0,0 +1,120 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// LogStorage 日志存储接口
type LogStorage interface {
CreateLoginLog(log *models.LoginLog) error
CreateOperationLog(log *models.OperationLog) error
GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error)
GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error)
DeleteLoginLogs(beforeDate string) error
DeleteOperationLogs(beforeDate string) error
}
type logStorage struct {
db *gorm.DB
}
// NewLogStorage 创建日志存储实例
func NewLogStorage() LogStorage {
return &logStorage{db: DB}
}
// CreateLoginLog 创建登录日志
func (s *logStorage) CreateLoginLog(log *models.LoginLog) error {
return s.db.Create(log).Error
}
// CreateOperationLog 创建操作日志
func (s *logStorage) CreateOperationLog(log *models.OperationLog) error {
return s.db.Create(log).Error
}
// GetLoginLogs 获取登录日志列表
func (s *logStorage) GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error) {
var logs []models.LoginLog
var total int64
query := s.db.Model(&models.LoginLog{})
// 构建查询条件
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.IP != "" {
query = query.Where("ip LIKE ?", "%"+req.IP+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.Order("created_at DESC").Offset(offset).Limit(req.Size).Find(&logs).Error
return logs, total, err
}
// GetOperationLogs 获取操作日志列表
func (s *logStorage) GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error) {
var logs []models.OperationLog
var total int64
query := s.db.Model(&models.OperationLog{})
// 构建查询条件
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.Module != "" {
query = query.Where("module LIKE ?", "%"+req.Module+"%")
}
if req.Operation != "" {
query = query.Where("operation LIKE ?", "%"+req.Operation+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.Order("created_at DESC").Offset(offset).Limit(req.Size).Find(&logs).Error
return logs, total, err
}
// DeleteLoginLogs 删除指定日期之前的登录日志
func (s *logStorage) DeleteLoginLogs(beforeDate string) error {
return s.db.Where("created_at < ?", beforeDate).Delete(&models.LoginLog{}).Error
}
// DeleteOperationLogs 删除指定日期之前的操作日志
func (s *logStorage) DeleteOperationLogs(beforeDate string) error {
return s.db.Where("created_at < ?", beforeDate).Delete(&models.OperationLog{}).Error
}

View File

@@ -0,0 +1,154 @@
package storage
import (
"context"
"encoding/json"
"fmt"
"goalfymax-admin/internal/models"
"time"
"gorm.io/gorm"
)
// MessagePushStorage 消息推送存储接口
type MessagePushStorage interface {
Create(ctx context.Context, log *models.MessagePushLog) error
List(ctx context.Context, req *models.MessagePushListRequest) ([]models.MessagePushLog, int64, error)
GetByID(ctx context.Context, id int64) (*models.MessagePushLog, error)
UpdateStatus(ctx context.Context, id int64, status int, successCount, failCount int, errorMessage string) error
SearchUsers(ctx context.Context, keyword string, limit int) ([]models.UserSearchItem, error)
}
type messagePushStorage struct {
db *gorm.DB
}
// NewMessagePushStorage 创建消息推送存储实例
func NewMessagePushStorage() MessagePushStorage {
return &messagePushStorage{db: DB}
}
// Create 创建推送记录
func (s *messagePushStorage) Create(ctx context.Context, log *models.MessagePushLog) error {
if err := s.db.WithContext(ctx).Create(log).Error; err != nil {
return fmt.Errorf("创建推送记录失败: %w", err)
}
return nil
}
// List 获取推送记录列表
func (s *messagePushStorage) List(ctx context.Context, req *models.MessagePushListRequest) ([]models.MessagePushLog, int64, error) {
var logs []models.MessagePushLog
var total int64
query := s.db.WithContext(ctx).Model(&models.MessagePushLog{})
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
if req.SenderID != nil {
query = query.Where("sender_id = ?", *req.SenderID)
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("查询总数失败: %w", err)
}
if req.Page > 0 && req.PageSize > 0 {
offset := (req.Page - 1) * req.PageSize
query = query.Offset(offset).Limit(req.PageSize)
}
if err := query.Order("created_at DESC").Find(&logs).Error; err != nil {
return nil, 0, fmt.Errorf("查询推送记录失败: %w", err)
}
return logs, total, nil
}
// GetByID 根据ID获取推送记录
func (s *messagePushStorage) GetByID(ctx context.Context, id int64) (*models.MessagePushLog, error) {
var log models.MessagePushLog
if err := s.db.WithContext(ctx).First(&log, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("推送记录不存在")
}
return nil, fmt.Errorf("获取推送记录失败: %w", err)
}
return &log, nil
}
// UpdateStatus 更新推送状态
func (s *messagePushStorage) UpdateStatus(ctx context.Context, id int64, status int, successCount, failCount int, errorMessage string) error {
updates := map[string]interface{}{
"status": status,
"success_count": successCount,
"fail_count": failCount,
}
if errorMessage != "" {
updates["error_message"] = errorMessage
}
if status == 2 || status == 3 { // 发送成功或失败时更新发送时间
now := time.Now()
updates["sent_at"] = &now
}
if err := s.db.WithContext(ctx).Model(&models.MessagePushLog{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return fmt.Errorf("更新推送状态失败: %w", err)
}
return nil
}
// SearchUsers 搜索用户
func (s *messagePushStorage) SearchUsers(ctx context.Context, keyword string, limit int) ([]models.UserSearchItem, error) {
var users []models.UserSearchItem
// 从admin_goalfymax_users表搜索用户
query := s.db.WithContext(ctx).Table("admin_goalfymax_users").
Select("user_id as id, username, email").
Where("deleted_at IS NULL") // 排除已删除的用户
// 如果有关键词,添加搜索条件
if keyword != "" {
query = query.Where("username LIKE ? OR email LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
query = query.Limit(limit)
// 添加调试日志
fmt.Printf("🔍 [SearchUsers] 搜索关键词: %s, 限制: %d\n", keyword, limit)
if err := query.Find(&users).Error; err != nil {
fmt.Printf("❌ [SearchUsers] 查询失败: %v\n", err)
return nil, fmt.Errorf("搜索用户失败: %w", err)
}
fmt.Printf("✅ [SearchUsers] 找到 %d 个用户\n", len(users))
return users, nil
}
// ParseTargetUsers 解析目标用户JSON
func ParseTargetUsers(targetUsersJSON string) ([]int, error) {
var userIDs []int
if err := json.Unmarshal([]byte(targetUsersJSON), &userIDs); err != nil {
return nil, fmt.Errorf("解析目标用户失败: %w", err)
}
return userIDs, nil
}
// SerializeTargetUsers 序列化目标用户为JSON
func SerializeTargetUsers(userIDs []int) (string, error) {
jsonData, err := json.Marshal(userIDs)
if err != nil {
return "", fmt.Errorf("序列化目标用户失败: %w", err)
}
return string(jsonData), nil
}

View File

@@ -0,0 +1,101 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// PageStorage 页面存储接口
type PageStorage interface {
Create(page *models.Page) error
GetByID(id uint) (*models.Page, error)
GetByPath(path string) (*models.Page, error)
Update(page *models.Page) error
Delete(id uint) error
List(req *models.PageListRequest) ([]models.Page, int64, error)
}
type pageStorage struct {
db *gorm.DB
}
// NewPageStorage 创建页面存储实例
func NewPageStorage() PageStorage {
return &pageStorage{
db: DB,
}
}
// Create 创建页面
func (s *pageStorage) Create(page *models.Page) error {
return s.db.Create(page).Error
}
// GetByID 根据ID获取页面
func (s *pageStorage) GetByID(id uint) (*models.Page, error) {
var page models.Page
err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&page).Error
if err != nil {
return nil, err
}
return &page, nil
}
// GetByPath 根据路径获取页面
func (s *pageStorage) GetByPath(path string) (*models.Page, error) {
var page models.Page
err := s.db.Where("path = ? AND deleted_at IS NULL", path).First(&page).Error
if err != nil {
return nil, err
}
return &page, nil
}
// Update 更新页面
func (s *pageStorage) Update(page *models.Page) error {
return s.db.Save(page).Error
}
// Delete 删除页面
func (s *pageStorage) Delete(id uint) error {
return s.db.Where("id = ?", id).Delete(&models.Page{}).Error
}
// List 获取页面列表
func (s *pageStorage) List(req *models.PageListRequest) ([]models.Page, int64, error) {
var pages []models.Page
var total int64
query := s.db.Model(&models.Page{}).Where("deleted_at IS NULL")
// 应用过滤条件
if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%")
}
if req.Path != "" {
query = query.Where("path LIKE ?", "%"+req.Path+"%")
}
if req.IsActive != nil {
query = query.Where("is_active = ?", *req.IsActive)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 应用分页(如果没有指定分页参数,返回所有数据)
if req.Page > 0 && req.Size > 0 {
offset := (req.Page - 1) * req.Size
err = query.Order("sort_order ASC, id ASC").Offset(offset).Limit(req.Size).Find(&pages).Error
} else {
// 不分页,返回所有数据
err = query.Order("sort_order ASC, id ASC").Find(&pages).Error
}
if err != nil {
return nil, 0, err
}
return pages, total, nil
}

View File

@@ -0,0 +1,69 @@
package storage
import (
"fmt"
"log"
"os"
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/pkg/utils"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var PG *gorm.DB
// InitPostgres 初始化PostgreSQL连接
func InitPostgres(appLogger *utils.Logger) error {
cfg := config.GetConfig()
pg := cfg.PostgreSQL
// 兼容 DSN 或字段拼接
dsn := pg.DSN
if dsn == "" {
ssl := pg.SSLMode
if ssl == "" {
ssl = "disable"
}
dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=UTC",
pg.Host, pg.Port, pg.User, pg.Password, pg.DBName, ssl,
)
}
lw := log.New(os.Stdout, "", log.LstdFlags)
gormLogger := logger.New(lw, logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: true,
ParameterizedQueries: true,
Colorful: false,
})
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger})
if err != nil {
return fmt.Errorf("连接PostgreSQL失败: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取PostgreSQL实例失败: %w", err)
}
if pg.MaxOpenConns > 0 {
sqlDB.SetMaxOpenConns(pg.MaxOpenConns)
}
if pg.MaxIdleConns > 0 {
sqlDB.SetMaxIdleConns(pg.MaxIdleConns)
}
if pg.ConnMaxLifetime > 0 {
sqlDB.SetConnMaxLifetime(pg.ConnMaxLifetime)
}
if pg.ConnMaxIdleTime > 0 {
sqlDB.SetConnMaxIdleTime(pg.ConnMaxIdleTime)
}
PG = db
return nil
}
func GetPG() *gorm.DB { return PG }

View File

@@ -0,0 +1,117 @@
package storage
import (
"fmt"
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// RBACStorage 简化的RBAC存储接口
type RBACStorage interface {
// 角色页面权限管理
AssignRolePagePermissions(roleID uint, pageIDs []uint) error
RemoveRolePagePermissions(roleID uint, pageIDs []uint) error
GetRolePagePermissions(roleID uint) ([]models.Page, error)
GetRolePagePermissionIDs(roleID uint) ([]uint, error)
// 页面权限检查
CheckUserRolePagePermission(userID uint, pagePath string) (bool, error)
GetUserRoleAccessiblePages(userID uint) ([]string, error)
// 角色管理
GetDefaultRoleID(roleID *uint) error
GetRoleByID(roleID uint) (*models.Role, error)
}
type rbacStorage struct {
db *gorm.DB
}
// NewRBACStorage 创建RBAC存储实例
func NewRBACStorage() RBACStorage {
return &rbacStorage{db: DB}
}
// AssignRolePagePermissions 分配角色页面权限
func (s *rbacStorage) AssignRolePagePermissions(roleID uint, pageIDs []uint) error {
var rolePagePermissions []models.RolePagePermission
for _, pageID := range pageIDs {
rolePagePermissions = append(rolePagePermissions, models.RolePagePermission{
RoleID: roleID,
PageID: pageID,
})
}
return s.db.Create(&rolePagePermissions).Error
}
// RemoveRolePagePermissions 移除角色页面权限
func (s *rbacStorage) RemoveRolePagePermissions(roleID uint, pageIDs []uint) error {
return s.db.Where("role_id = ? AND page_id IN ?", roleID, pageIDs).Delete(&models.RolePagePermission{}).Error
}
// GetRolePagePermissions 获取角色页面权限
func (s *rbacStorage) GetRolePagePermissions(roleID uint) ([]models.Page, error) {
var pages []models.Page
err := s.db.Table("admin_pages").
Joins("JOIN admin_role_page_permissions ON admin_pages.id = admin_role_page_permissions.page_id").
Where("admin_role_page_permissions.role_id = ? AND admin_role_page_permissions.deleted_at IS NULL", roleID).
Find(&pages).Error
return pages, err
}
// GetRolePagePermissionIDs 获取角色页面权限ID列表
func (s *rbacStorage) GetRolePagePermissionIDs(roleID uint) ([]uint, error) {
var pageIDs []uint
err := s.db.Model(&models.RolePagePermission{}).
Where("role_id = ?", roleID).
Pluck("page_id", &pageIDs).Error
return pageIDs, err
}
// CheckUserRolePagePermission 检查用户基于角色的页面权限
func (s *rbacStorage) CheckUserRolePagePermission(userID uint, pagePath string) (bool, error) {
var count int64
err := s.db.Table("admin_users").
Joins("JOIN admin_role_page_permissions ON admin_users.role_id = admin_role_page_permissions.role_id").
Joins("JOIN admin_pages ON admin_role_page_permissions.page_id = admin_pages.id").
Where("admin_users.id = ? AND admin_pages.path = ? AND admin_pages.is_active = TRUE AND admin_users.deleted_at IS NULL AND admin_role_page_permissions.deleted_at IS NULL", userID, pagePath).
Count(&count).Error
return count > 0, err
}
// GetUserRoleAccessiblePages 获取用户基于角色的可访问页面
func (s *rbacStorage) GetUserRoleAccessiblePages(userID uint) ([]string, error) {
var pages []string
// 添加调试日志
fmt.Printf("🔍 [RBACStorage] 查询用户 %d 的可访问页面\n", userID)
err := s.db.Table("admin_users").
Joins("JOIN admin_role_page_permissions ON admin_users.role_id = admin_role_page_permissions.role_id").
Joins("JOIN admin_pages ON admin_role_page_permissions.page_id = admin_pages.id").
Where("admin_users.id = ? AND admin_pages.is_active = TRUE AND admin_users.deleted_at IS NULL AND admin_role_page_permissions.deleted_at IS NULL", userID).
Select("DISTINCT admin_pages.path").
Pluck("admin_pages.path", &pages).Error
fmt.Printf("🔍 [RBACStorage] 用户 %d 可访问页面: %v\n", userID, pages)
return pages, err
}
// GetDefaultRoleID 获取默认角色ID
func (s *rbacStorage) GetDefaultRoleID(roleID *uint) error {
return s.db.Table("admin_roles").
Where("is_default = TRUE AND deleted_at IS NULL").
Select("id").
First(roleID).Error
}
// GetRoleByID 根据ID获取角色
func (s *rbacStorage) GetRoleByID(roleID uint) (*models.Role, error) {
var role models.Role
err := s.db.Where("id = ? AND deleted_at IS NULL", roleID).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}

View File

@@ -0,0 +1,93 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// RoleStorage 角色存储接口
type RoleStorage interface {
Create(role *models.Role) error
GetByID(id uint) (*models.Role, error)
GetByName(name string) (*models.Role, error)
Update(role *models.Role) error
Delete(id uint) error
List(req *models.RoleListRequest) ([]models.Role, int64, error)
UpdateStatus(id uint, status int) error
}
type roleStorage struct {
db *gorm.DB
}
// NewRoleStorage 创建角色存储实例
func NewRoleStorage() RoleStorage {
return &roleStorage{db: DB}
}
// Create 创建角色
func (s *roleStorage) Create(role *models.Role) error {
return s.db.Create(role).Error
}
// GetByID 根据ID获取角色
func (s *roleStorage) GetByID(id uint) (*models.Role, error) {
var role models.Role
err := s.db.First(&role, id).Error
if err != nil {
return nil, err
}
return &role, nil
}
// GetByName 根据名称获取角色
func (s *roleStorage) GetByName(name string) (*models.Role, error) {
var role models.Role
err := s.db.Where("name = ?", name).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
// Update 更新角色
func (s *roleStorage) Update(role *models.Role) error {
return s.db.Save(role).Error
}
// Delete 删除角色
func (s *roleStorage) Delete(id uint) error {
return s.db.Delete(&models.Role{}, id).Error
}
// List 获取角色列表
func (s *roleStorage) List(req *models.RoleListRequest) ([]models.Role, int64, error) {
var roles []models.Role
var total int64
query := s.db.Model(&models.Role{})
// 构建查询条件
if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.Offset(offset).Limit(req.Size).Find(&roles).Error
return roles, total, err
}
// UpdateStatus 更新角色状态
func (s *roleStorage) UpdateStatus(id uint, status int) error {
return s.db.Model(&models.Role{}).Where("id = ?", id).Update("status", status).Error
}

View File

@@ -0,0 +1,124 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// PKCEStateStorage PKCE状态存储接口
type PKCEStateStorage interface {
Create(pkceState *models.PKCEState) error
GetByState(state string) (*models.PKCEState, error)
DeleteByState(state string) error
CleanExpired() error
}
// LoginInfoStorage 登录信息存储接口
type LoginInfoStorage interface {
Create(loginInfo *models.LoginInfo) error
GetByUserID(userID int) (*models.LoginInfo, error)
GetByUserIDAndUUID(userID int, uuid string) (*models.LoginInfo, error)
Update(loginInfo *models.LoginInfo) error
SetUserOffline(userID int) error
ListOnlineUsers() ([]*models.LoginInfo, error)
CountOnlineUsers() (int64, error)
DeleteByUserID(userID int) error
}
type pkceStateStorage struct {
db *gorm.DB
}
// NewPKCEStateStorage 创建PKCE状态存储实例
func NewPKCEStateStorage() PKCEStateStorage {
return &pkceStateStorage{db: DB}
}
// Create 创建PKCE状态
func (s *pkceStateStorage) Create(pkceState *models.PKCEState) error {
return s.db.Create(pkceState).Error
}
// GetByState 根据状态获取PKCE状态
func (s *pkceStateStorage) GetByState(state string) (*models.PKCEState, error) {
var pkceState models.PKCEState
err := s.db.Where("state = ?", state).First(&pkceState).Error
if err != nil {
return nil, err
}
return &pkceState, nil
}
// DeleteByState 根据状态删除PKCE状态
func (s *pkceStateStorage) DeleteByState(state string) error {
return s.db.Where("state = ?", state).Delete(&models.PKCEState{}).Error
}
// CleanExpired 清理过期的PKCE状态
func (s *pkceStateStorage) CleanExpired() error {
// 删除创建时间超过1小时的记录
return s.db.Where("created_at < ?", "NOW() - INTERVAL 1 HOUR").Delete(&models.PKCEState{}).Error
}
type loginInfoStorage struct {
db *gorm.DB
}
// NewLoginInfoStorage 创建登录信息存储实例
func NewLoginInfoStorage() LoginInfoStorage {
return &loginInfoStorage{db: DB}
}
// Create 创建登录信息
func (s *loginInfoStorage) Create(loginInfo *models.LoginInfo) error {
return s.db.Create(loginInfo).Error
}
// GetByUserID 根据用户ID获取登录信息
func (s *loginInfoStorage) GetByUserID(userID int) (*models.LoginInfo, error) {
var loginInfo models.LoginInfo
err := s.db.Where("user_id = ?", userID).First(&loginInfo).Error
if err != nil {
return nil, err
}
return &loginInfo, nil
}
// GetByUserIDAndUUID 根据用户ID和UUID获取登录信息
func (s *loginInfoStorage) GetByUserIDAndUUID(userID int, uuid string) (*models.LoginInfo, error) {
var loginInfo models.LoginInfo
err := s.db.Where("user_id = ? AND uuid = ?", userID, uuid).First(&loginInfo).Error
if err != nil {
return nil, err
}
return &loginInfo, nil
}
// Update 更新登录信息
func (s *loginInfoStorage) Update(loginInfo *models.LoginInfo) error {
return s.db.Save(loginInfo).Error
}
// SetUserOffline 设置用户离线
func (s *loginInfoStorage) SetUserOffline(userID int) error {
return s.db.Model(&models.LoginInfo{}).Where("user_id = ?", userID).Update("is_online", false).Error
}
// ListOnlineUsers 获取在线用户列表
func (s *loginInfoStorage) ListOnlineUsers() ([]*models.LoginInfo, error) {
var loginInfos []*models.LoginInfo
err := s.db.Where("is_online = ?", true).Find(&loginInfos).Error
return loginInfos, err
}
// CountOnlineUsers 获取在线用户数量
func (s *loginInfoStorage) CountOnlineUsers() (int64, error) {
var count int64
err := s.db.Model(&models.LoginInfo{}).Where("is_online = ?", true).Count(&count).Error
return count, err
}
// DeleteByUserID 根据用户ID删除登录信息
func (s *loginInfoStorage) DeleteByUserID(userID int) error {
return s.db.Where("user_id = ?", userID).Delete(&models.LoginInfo{}).Error
}

View File

@@ -0,0 +1,106 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// SystemConfigStorage 系统配置存储接口
type SystemConfigStorage interface {
Create(config *models.SystemConfig) error
GetByID(id uint) (*models.SystemConfig, error)
GetByKey(key string) (*models.SystemConfig, error)
Update(config *models.SystemConfig) error
Delete(id uint) error
List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.SystemConfig, error)
}
type systemConfigStorage struct {
db *gorm.DB
}
// NewSystemConfigStorage 创建系统配置存储实例
func NewSystemConfigStorage() SystemConfigStorage {
return &systemConfigStorage{db: DB}
}
// Create 创建系统配置
func (s *systemConfigStorage) Create(config *models.SystemConfig) error {
return s.db.Create(config).Error
}
// GetByID 根据ID获取系统配置
func (s *systemConfigStorage) GetByID(id uint) (*models.SystemConfig, error) {
var config models.SystemConfig
err := s.db.First(&config, id).Error
if err != nil {
return nil, err
}
return &config, nil
}
// GetByKey 根据配置标识获取配置
func (s *systemConfigStorage) GetByKey(key string) (*models.SystemConfig, error) {
var config models.SystemConfig
err := s.db.Where("`key` = ?", key).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// Update 更新系统配置
func (s *systemConfigStorage) Update(config *models.SystemConfig) error {
return s.db.Save(config).Error
}
// Delete 删除系统配置
func (s *systemConfigStorage) Delete(id uint) error {
return s.db.Delete(&models.SystemConfig{}, id).Error
}
// List 获取系统配置列表
func (s *systemConfigStorage) List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error) {
var configs []models.SystemConfig
var total int64
query := s.db.Model(&models.SystemConfig{})
// 构建查询条件
if req.Key != "" {
query = query.Where("`key` LIKE ?", "%"+req.Key+"%")
}
if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
if err := query.Order("id DESC").Offset(offset).Limit(req.Size).Find(&configs).Error; err != nil {
return nil, 0, err
}
return configs, total, nil
}
// UpdateStatus 更新状态
func (s *systemConfigStorage) UpdateStatus(id uint, status int) error {
return s.db.Model(&models.SystemConfig{}).Where("id = ?", id).Update("status", status).Error
}
// GetAll 获取所有系统配置(不分页)
func (s *systemConfigStorage) GetAll() ([]models.SystemConfig, error) {
var configs []models.SystemConfig
err := s.db.Where("status = ?", 1).Order("id DESC").Find(&configs).Error
return configs, err
}

View File

@@ -0,0 +1,188 @@
package storage
import (
"context"
"fmt"
"goalfymax-admin/internal/models"
"time"
"gorm.io/gorm"
)
// UserFeedbackStorage 用户反馈存储层
type UserFeedbackStorage struct {
db *gorm.DB
}
// NewUserFeedbackStorage 创建用户反馈存储实例
func NewUserFeedbackStorage() *UserFeedbackStorage {
return &UserFeedbackStorage{db: DB}
}
// List 获取用户反馈列表
func (s *UserFeedbackStorage) List(ctx context.Context, req *models.UserFeedbackListRequest) ([]models.UserFeedback, int64, error) {
var feedbacks []models.UserFeedback
var total int64
query := s.db.WithContext(ctx).Model(&models.UserFeedback{})
// 状态筛选
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 用户ID筛选
if req.UserID != nil {
query = query.Where("uid = ?", *req.UserID)
}
// 关键词搜索
if req.Keyword != "" {
query = query.Where("content LIKE ?", "%"+req.Keyword+"%")
}
// 时间范围筛选
if req.StartTime != "" {
if startTime, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
query = query.Where("created_at >= ?", startTime)
}
}
if req.EndTime != "" {
if endTime, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
query = query.Where("created_at <= ?", endTime)
}
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("获取反馈总数失败: %w", err)
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
if err := query.Order("created_at DESC").
Offset(offset).
Limit(req.PageSize).
Find(&feedbacks).Error; err != nil {
return nil, 0, fmt.Errorf("获取反馈列表失败: %w", err)
}
return feedbacks, total, nil
}
// GetByID 根据ID获取用户反馈
func (s *UserFeedbackStorage) GetByID(ctx context.Context, id int64) (*models.UserFeedback, error) {
var feedback models.UserFeedback
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&feedback).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, fmt.Errorf("获取用户反馈失败: %w", err)
}
return &feedback, nil
}
// MarkHandled 标记为已处理或未处理(切换状态)
func (s *UserFeedbackStorage) MarkHandled(ctx context.Context, id int64, handledBy int, note string) error {
// 先获取当前状态
feedback, err := s.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("获取反馈信息失败: %w", err)
}
if feedback == nil {
return fmt.Errorf("反馈不存在")
}
// 切换状态:如果当前是已处理(1),则改为未处理(0);如果当前是未处理(0),则改为已处理(1)
newStatus := 0
var updates map[string]interface{}
if feedback.Status == 0 {
// 从未处理改为已处理
newStatus = 1
now := time.Now()
updates = map[string]interface{}{
"status": newStatus,
"handled_by": handledBy,
"handled_at": now,
}
} else {
// 从已处理改为未处理
newStatus = 0
updates = map[string]interface{}{
"status": newStatus,
"handled_by": nil,
"handled_at": nil,
}
}
result := s.db.WithContext(ctx).Model(&models.UserFeedback{}).
Where("id = ?", id).
Updates(updates)
if result.Error != nil {
return fmt.Errorf("切换状态失败: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("切换状态失败")
}
return nil
}
// Delete 删除用户反馈
func (s *UserFeedbackStorage) Delete(ctx context.Context, id int64) error {
if err := s.db.WithContext(ctx).Where("id = ?", id).Delete(&models.UserFeedback{}).Error; err != nil {
return fmt.Errorf("删除用户反馈失败: %w", err)
}
return nil
}
// Create 创建用户反馈(如果需要)
func (s *UserFeedbackStorage) Create(ctx context.Context, feedback *models.UserFeedback) error {
if err := s.db.WithContext(ctx).Create(feedback).Error; err != nil {
return fmt.Errorf("创建用户反馈失败: %w", err)
}
return nil
}
// GetStatistics 获取反馈统计信息
func (s *UserFeedbackStorage) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
var stats struct {
Total int64 `json:"total"`
Unhandled int64 `json:"unhandled"`
Handled int64 `json:"handled"`
TodayCount int64 `json:"today_count"`
}
// 总数
if err := s.db.WithContext(ctx).Model(&models.UserFeedback{}).Count(&stats.Total).Error; err != nil {
return nil, fmt.Errorf("获取总数失败: %w", err)
}
// 未处理数
if err := s.db.WithContext(ctx).Model(&models.UserFeedback{}).Where("status = 0").Count(&stats.Unhandled).Error; err != nil {
return nil, fmt.Errorf("获取未处理数失败: %w", err)
}
// 已处理数
if err := s.db.WithContext(ctx).Model(&models.UserFeedback{}).Where("status = 1").Count(&stats.Handled).Error; err != nil {
return nil, fmt.Errorf("获取已处理数失败: %w", err)
}
// 今日新增
today := time.Now().Format("2006-01-02")
if err := s.db.WithContext(ctx).Model(&models.UserFeedback{}).
Where("DATE(created_at) = ?", today).
Count(&stats.TodayCount).Error; err != nil {
return nil, fmt.Errorf("获取今日新增数失败: %w", err)
}
return map[string]interface{}{
"total": stats.Total,
"unhandled": stats.Unhandled,
"handled": stats.Handled,
"today_count": stats.TodayCount,
}, nil
}

View File

@@ -0,0 +1,103 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// UserLevelConfigStorage 用户等级配置存储接口
type UserLevelConfigStorage interface {
Create(config *models.UserLevelConfig) error
GetByID(id uint) (*models.UserLevelConfig, error)
GetByLevelCode(levelCode string) (*models.UserLevelConfig, error)
Update(config *models.UserLevelConfig) error
Delete(id uint) error
List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.UserLevelConfig, error)
}
type userLevelConfigStorage struct {
db *gorm.DB
}
// NewUserLevelConfigStorage 创建用户等级配置存储实例
func NewUserLevelConfigStorage() UserLevelConfigStorage {
return &userLevelConfigStorage{db: DB}
}
// Create 创建用户等级配置
func (s *userLevelConfigStorage) Create(config *models.UserLevelConfig) error {
return s.db.Create(config).Error
}
// GetByID 根据ID获取用户等级配置
func (s *userLevelConfigStorage) GetByID(id uint) (*models.UserLevelConfig, error) {
var config models.UserLevelConfig
err := s.db.First(&config, id).Error
if err != nil {
return nil, err
}
return &config, nil
}
// GetByLevelCode 根据等级代码获取配置
func (s *userLevelConfigStorage) GetByLevelCode(levelCode string) (*models.UserLevelConfig, error) {
var config models.UserLevelConfig
err := s.db.Where("level_code = ?", levelCode).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// Update 更新用户等级配置
func (s *userLevelConfigStorage) Update(config *models.UserLevelConfig) error {
return s.db.Save(config).Error
}
// Delete 删除用户等级配置
func (s *userLevelConfigStorage) Delete(id uint) error {
return s.db.Delete(&models.UserLevelConfig{}, id).Error
}
// List 获取用户等级配置列表
func (s *userLevelConfigStorage) List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error) {
var configs []models.UserLevelConfig
var total int64
query := s.db.Model(&models.UserLevelConfig{})
// 构建查询条件
if req.LevelName != "" {
query = query.Where("level_name LIKE ?", "%"+req.LevelName+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
if err := query.Order("sort_order ASC, id DESC").Offset(offset).Limit(req.Size).Find(&configs).Error; err != nil {
return nil, 0, err
}
return configs, total, nil
}
// UpdateStatus 更新状态
func (s *userLevelConfigStorage) UpdateStatus(id uint, status int) error {
return s.db.Model(&models.UserLevelConfig{}).Where("id = ?", id).Update("status", status).Error
}
// GetAll 获取所有用户等级配置(不分页)
func (s *userLevelConfigStorage) GetAll() ([]models.UserLevelConfig, error) {
var configs []models.UserLevelConfig
err := s.db.Where("status = ?", 1).Order("sort_order ASC").Find(&configs).Error
return configs, err
}

View File

@@ -0,0 +1,75 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
type UserProjectQuotaFilter struct {
UserID string
Enabled *bool
Page int
Size int
}
type UserProjectQuotaStorage interface {
Create(q *models.UserProjectQuota) error
Update(q *models.UserProjectQuota) error
Delete(id uint) error
GetByID(id uint) (*models.UserProjectQuota, error)
GetByUserID(userID string) (*models.UserProjectQuota, error)
List(filter UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error)
}
type userProjectQuotaStorage struct{ db *gorm.DB }
func NewUserProjectQuotaStorage() UserProjectQuotaStorage { return &userProjectQuotaStorage{db: DB} }
func (s *userProjectQuotaStorage) Create(q *models.UserProjectQuota) error {
return s.db.Create(q).Error
}
func (s *userProjectQuotaStorage) Update(q *models.UserProjectQuota) error { return s.db.Save(q).Error }
func (s *userProjectQuotaStorage) Delete(id uint) error {
return s.db.Delete(&models.UserProjectQuota{}, id).Error
}
func (s *userProjectQuotaStorage) GetByID(id uint) (*models.UserProjectQuota, error) {
var out models.UserProjectQuota
if err := s.db.First(&out, id).Error; err != nil {
return nil, err
}
return &out, nil
}
func (s *userProjectQuotaStorage) GetByUserID(userID string) (*models.UserProjectQuota, error) {
var out models.UserProjectQuota
if err := s.db.Where("user_id = ?", userID).First(&out).Error; err != nil {
return nil, err
}
return &out, nil
}
func (s *userProjectQuotaStorage) List(filter UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error) {
var (
items []models.UserProjectQuota
total int64
)
q := s.db.Model(&models.UserProjectQuota{})
if filter.UserID != "" {
q = q.Where("user_id LIKE ?", "%"+filter.UserID+"%")
}
if filter.Enabled != nil {
q = q.Where("enabled = ?", *filter.Enabled)
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
page, size := filter.Page, filter.Size
if page <= 0 {
page = 1
}
if size <= 0 || size > 200 {
size = 20
}
if err := q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}

View File

@@ -0,0 +1,107 @@
package storage
import (
"goalfymax-admin/internal/models"
"gorm.io/gorm"
)
// UserStorage 用户存储接口
type UserStorage interface {
Create(user *models.User) error
GetByID(id uint) (*models.User, error)
GetByUsername(username string) (*models.User, error)
GetByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
List(req *models.UserListRequest) ([]models.User, int64, error)
UpdateStatus(id uint, status int) error
}
type userStorage struct {
db *gorm.DB
}
// NewUserStorage 创建用户存储实例
func NewUserStorage() UserStorage {
return &userStorage{db: DB}
}
// Create 创建用户
func (s *userStorage) Create(user *models.User) error {
return s.db.Create(user).Error
}
// GetByID 根据ID获取用户
func (s *userStorage) GetByID(id uint) (*models.User, error) {
var user models.User
err := s.db.First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
// GetByUsername 根据用户名获取用户
func (s *userStorage) GetByUsername(username string) (*models.User, error) {
var user models.User
err := s.db.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// GetByEmail 根据邮箱获取用户
func (s *userStorage) GetByEmail(email string) (*models.User, error) {
var user models.User
err := s.db.Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// Update 更新用户
func (s *userStorage) Update(user *models.User) error {
return s.db.Save(user).Error
}
// Delete 删除用户
func (s *userStorage) Delete(id uint) error {
return s.db.Delete(&models.User{}, id).Error
}
// List 获取用户列表
func (s *userStorage) List(req *models.UserListRequest) ([]models.User, int64, error) {
var users []models.User
var total int64
query := s.db.Model(&models.User{})
// 构建查询条件
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.Email != "" {
query = query.Where("email LIKE ?", "%"+req.Email+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.Offset(offset).Limit(req.Size).Find(&users).Error
return users, total, err
}
// UpdateStatus 更新用户状态
func (s *userStorage) UpdateStatus(id uint, status int) error {
return s.db.Model(&models.User{}).Where("id = ?", id).Update("status", status).Error
}