feat():learning后台管理项目初始化
This commit is contained in:
179
internal/api/README.md
Normal file
179
internal/api/README.md
Normal 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
|
||||
|
||||
73
internal/api/handlers/audit_log_handler.go
Normal file
73
internal/api/handlers/audit_log_handler.go
Normal 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)
|
||||
}
|
||||
633
internal/api/handlers/finance_handler.go
Normal file
633
internal/api/handlers/finance_handler.go
Normal 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_id,project/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": "创建成功"})
|
||||
}
|
||||
245
internal/api/handlers/goalfymax_user_handler.go
Normal file
245
internal/api/handlers/goalfymax_user_handler.go
Normal 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": "减少余额成功"})
|
||||
}
|
||||
254
internal/api/handlers/invite_code_application_handler.go
Normal file
254
internal/api/handlers/invite_code_application_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
458
internal/api/handlers/invite_code_handler.go
Normal file
458
internal/api/handlers/invite_code_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
186
internal/api/handlers/mcp_provider_handler.go
Normal file
186
internal/api/handlers/mcp_provider_handler.go
Normal 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, "使用状态更新成功")
|
||||
}
|
||||
134
internal/api/handlers/message_push_handler.go
Normal file
134
internal/api/handlers/message_push_handler.go
Normal 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)
|
||||
}
|
||||
144
internal/api/handlers/page_handler.go
Normal file
144
internal/api/handlers/page_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
117
internal/api/handlers/quota_handler.go
Normal file
117
internal/api/handlers/quota_handler.go
Normal 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})
|
||||
}
|
||||
182
internal/api/handlers/rbac_handler.go
Normal file
182
internal/api/handlers/rbac_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
228
internal/api/handlers/role_handler.go
Normal file
228
internal/api/handlers/role_handler.go
Normal 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})
|
||||
}
|
||||
267
internal/api/handlers/sso_handler.go
Normal file
267
internal/api/handlers/sso_handler.go
Normal 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, "批量登出成功")
|
||||
}
|
||||
189
internal/api/handlers/system_config_handler.go
Normal file
189
internal/api/handlers/system_config_handler.go
Normal 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)
|
||||
}
|
||||
195
internal/api/handlers/user_feedback_handler.go
Normal file
195
internal/api/handlers/user_feedback_handler.go
Normal 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://")
|
||||
}
|
||||
285
internal/api/handlers/user_handler.go
Normal file
285
internal/api/handlers/user_handler.go
Normal 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": "角色变更成功"})
|
||||
}
|
||||
171
internal/api/handlers/user_level_config_handler.go
Normal file
171
internal/api/handlers/user_level_config_handler.go
Normal 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)
|
||||
}
|
||||
97
internal/api/handlers/user_project_quota_handler.go
Normal file
97
internal/api/handlers/user_project_quota_handler.go
Normal 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)
|
||||
}
|
||||
170
internal/api/handlers/vendor_model_pricing_handler.go
Normal file
170
internal/api/handlers/vendor_model_pricing_handler.go
Normal 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)
|
||||
}
|
||||
403
internal/api/handlers/vm_pricing_handler.go
Normal file
403
internal/api/handlers/vm_pricing_handler.go
Normal 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, "默认模板设置成功")
|
||||
}
|
||||
141
internal/api/middlewares/api_log_middleware.go
Normal file
141
internal/api/middlewares/api_log_middleware.go
Normal 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"
|
||||
}
|
||||
47
internal/api/middlewares/logging.go
Normal file
47
internal/api/middlewares/logging.go
Normal 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
370
internal/api/routes/routes.go
Normal file
370
internal/api/routes/routes.go
Normal 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
47
internal/config/README.md
Normal 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
322
internal/config/config.go
Normal 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 ""
|
||||
}
|
||||
49
internal/jobs/mcp_usage_job.go
Normal file
49
internal/jobs/mcp_usage_job.go
Normal 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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
49
internal/jobs/model_token_job.go
Normal file
49
internal/jobs/model_token_job.go
Normal 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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
114
internal/models/audit_log.go
Normal file
114
internal/models/audit_log.go
Normal 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"`
|
||||
}
|
||||
28
internal/models/balance_operation_log.go
Normal file
28
internal/models/balance_operation_log.go
Normal 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
104
internal/models/common.go
Normal 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"
|
||||
}
|
||||
80
internal/models/goalfymax_user.go
Normal file
80
internal/models/goalfymax_user.go
Normal 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"`
|
||||
}
|
||||
65
internal/models/invite_code.go
Normal file
65
internal/models/invite_code.go
Normal 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"`
|
||||
}
|
||||
92
internal/models/invite_code_application.go
Normal file
92
internal/models/invite_code_application.go
Normal 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"
|
||||
)
|
||||
78
internal/models/message_push.go
Normal file
78
internal/models/message_push.go
Normal 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"`
|
||||
}
|
||||
65
internal/models/quota_models.go
Normal file
65
internal/models/quota_models.go
Normal 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
52
internal/models/rbac.go
Normal 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
141
internal/models/request.go
Normal 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"`
|
||||
}
|
||||
99
internal/models/response.go
Normal file
99
internal/models/response.go
Normal 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
196
internal/models/sso.go
Normal 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"`
|
||||
}
|
||||
63
internal/models/user_feedback.go
Normal file
63
internal/models/user_feedback.go
Normal 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"`
|
||||
}
|
||||
59
internal/models/user_level_config.go
Normal file
59
internal/models/user_level_config.go
Normal 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"`
|
||||
}
|
||||
27
internal/models/user_project_quota.go
Normal file
27
internal/models/user_project_quota.go
Normal 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"
|
||||
}
|
||||
327
internal/notifier/notifier.go
Normal file
327
internal/notifier/notifier.go
Normal 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
100
internal/oss/s3.go
Normal 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
170
internal/services/README.md
Normal 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,
|
||||
})
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有服务都遵循统一的错误处理模式:
|
||||
|
||||
- 业务逻辑错误返回具体的错误信息
|
||||
- 数据库错误记录日志并返回通用错误信息
|
||||
- 参数验证错误返回具体的验证信息
|
||||
|
||||
## 日志记录
|
||||
|
||||
服务层会自动记录关键操作的日志:
|
||||
|
||||
- 用户登录/登出
|
||||
- 用户创建/更新/删除
|
||||
- 角色创建/更新/删除
|
||||
- 菜单创建/更新/删除
|
||||
- 系统操作日志
|
||||
|
||||
70
internal/services/audit_log_service.go
Normal file
70
internal/services/audit_log_service.go
Normal 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
|
||||
}
|
||||
443
internal/services/email_service.go
Normal file
443
internal/services/email_service.go
Normal 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
|
||||
}
|
||||
2513
internal/services/finance_service.go
Normal file
2513
internal/services/finance_service.go
Normal file
File diff suppressed because it is too large
Load Diff
321
internal/services/gateway_client.go
Normal file
321
internal/services/gateway_client.go
Normal 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
|
||||
}
|
||||
512
internal/services/goalfymax_user_service.go
Normal file
512
internal/services/goalfymax_user_service.go
Normal 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
|
||||
}
|
||||
310
internal/services/invite_code_application_service.go
Normal file
310
internal/services/invite_code_application_service.go
Normal 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
|
||||
}
|
||||
291
internal/services/invite_code_service.go
Normal file
291
internal/services/invite_code_service.go
Normal 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))
|
||||
}
|
||||
85
internal/services/log_service.go
Normal file
85
internal/services/log_service.go
Normal 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
|
||||
}
|
||||
393
internal/services/message_push_service.go
Normal file
393
internal/services/message_push_service.go
Normal 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
|
||||
}
|
||||
131
internal/services/page_service.go
Normal file
131
internal/services/page_service.go
Normal 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
|
||||
}
|
||||
128
internal/services/quota_service.go
Normal file
128
internal/services/quota_service.go
Normal 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 "aService{
|
||||
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
|
||||
}
|
||||
165
internal/services/rbac_service.go
Normal file
165
internal/services/rbac_service.go
Normal 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)
|
||||
}
|
||||
137
internal/services/role_service.go
Normal file
137
internal/services/role_service.go
Normal 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
|
||||
}
|
||||
170
internal/services/sso_admin_service.go
Normal file
170
internal/services/sso_admin_service.go
Normal 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
|
||||
}
|
||||
618
internal/services/sso_service.go
Normal file
618
internal/services/sso_service.go
Normal 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")
|
||||
}
|
||||
|
||||
// 计算本次登录应赋予的角色ID:sys_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_id(sys_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 {
|
||||
// 获取默认角色ID(L5全员)
|
||||
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
|
||||
}
|
||||
145
internal/services/system_config_service.go
Normal file
145
internal/services/system_config_service.go
Normal 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()
|
||||
}
|
||||
260
internal/services/user_feedback_service.go
Normal file
260
internal/services/user_feedback_service.go
Normal 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 已废弃,改为强制预签名
|
||||
145
internal/services/user_level_config_service.go
Normal file
145
internal/services/user_level_config_service.go
Normal 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()
|
||||
}
|
||||
79
internal/services/user_project_quota_service.go
Normal file
79
internal/services/user_project_quota_service.go
Normal 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)
|
||||
}
|
||||
404
internal/services/user_service.go
Normal file
404
internal/services/user_service.go
Normal 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_admin),false表示不存在(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
|
||||
}
|
||||
82
internal/storage/README.md
Normal file
82
internal/storage/README.md
Normal 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` - 最大打开连接数
|
||||
|
||||
114
internal/storage/audit_log_storage.go
Normal file
114
internal/storage/audit_log_storage.go
Normal 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
|
||||
}
|
||||
22
internal/storage/balance_operation_log_storage.go
Normal file
22
internal/storage/balance_operation_log_storage.go
Normal 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
|
||||
}
|
||||
226
internal/storage/database.go
Normal file
226
internal/storage/database.go
Normal 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
|
||||
}
|
||||
123
internal/storage/goalfymax_user_storage.go
Normal file
123
internal/storage/goalfymax_user_storage.go
Normal 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
|
||||
}
|
||||
155
internal/storage/invite_code.go
Normal file
155
internal/storage/invite_code.go
Normal 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
|
||||
}
|
||||
120
internal/storage/log_storage.go
Normal file
120
internal/storage/log_storage.go
Normal 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
|
||||
}
|
||||
154
internal/storage/message_push_storage.go
Normal file
154
internal/storage/message_push_storage.go
Normal 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
|
||||
}
|
||||
101
internal/storage/page_storage.go
Normal file
101
internal/storage/page_storage.go
Normal 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
|
||||
}
|
||||
69
internal/storage/postgres.go
Normal file
69
internal/storage/postgres.go
Normal 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 }
|
||||
117
internal/storage/rbac_storage.go
Normal file
117
internal/storage/rbac_storage.go
Normal 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
|
||||
}
|
||||
93
internal/storage/role_storage.go
Normal file
93
internal/storage/role_storage.go
Normal 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
|
||||
}
|
||||
124
internal/storage/sso_storage.go
Normal file
124
internal/storage/sso_storage.go
Normal 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
|
||||
}
|
||||
106
internal/storage/system_config_storage.go
Normal file
106
internal/storage/system_config_storage.go
Normal 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
|
||||
}
|
||||
188
internal/storage/user_feedback_storage.go
Normal file
188
internal/storage/user_feedback_storage.go
Normal 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
|
||||
}
|
||||
103
internal/storage/user_level_config_storage.go
Normal file
103
internal/storage/user_level_config_storage.go
Normal 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
|
||||
}
|
||||
75
internal/storage/user_project_quota_storage.go
Normal file
75
internal/storage/user_project_quota_storage.go
Normal 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
|
||||
}
|
||||
107
internal/storage/user_storage.go
Normal file
107
internal/storage/user_storage.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user