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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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