代码升级

This commit is contained in:
向宁 2025-02-08 09:32:17 +08:00
parent ff312fb5a7
commit 4f8707e661
33 changed files with 2009 additions and 0 deletions

View File

@ -0,0 +1,31 @@
# 默认配置文件
server:
port: 8999
database:
host: "43.156.151.237"
port: 5432
user: "postgres"
password: "xn001624"
dbname: "user_center"
sslmode: "disable"
redis:
host: "43.156.151.237"
port: 6379
password: "xn001624"
db: 0
rate_limit_window: 60
rate_limit_max: 100
rabbitmq:
host: "43.156.151.237"
port: 5672
username: "admin"
password: "xn001624"
exchange: "ly_system"
queue: "ly_system_queue"
jwt:
secret: "your-secret-key"
expire: 86400 # 24小时

15
config/config_dev.yaml Normal file
View File

@ -0,0 +1,15 @@
# 开发环境配置
server:
host: "192.168.1.154"
database:
host: "192.168.1.154"
password: "dev_password" # 开发环境数据库密码
redis:
host: "192.168.1.154"
password: "dev_redis_password" # 开发环境Redis密码
rabbitmq:
host: "192.168.1.154"
password: "dev_rabbitmq_password" # 开发环境RabbitMQ密码

23
config/config_prod.yaml Normal file
View File

@ -0,0 +1,23 @@
server:
port: 8999
database:
host: "prod_host"
port: 5432
user: "postgres"
password: "prod_password"
dbname: "user_center"
sslmode: "disable"
redis:
host: "prod_host"
port: 6379
password: "prod_password"
db: 0
rate_limit_window: 60
rate_limit_max: 100
rabbitmq:
host: "prod_host"
port: 5672
username: admin
password: "prod_password"
exchange: ly_system
queue: ly_system_queue

15
config/config_test.yaml Normal file
View File

@ -0,0 +1,15 @@
# 测试环境配置
server:
host: "43.156.151.237"
database:
host: "43.156.151.237"
password: "test_password" # 测试环境数据库密码
redis:
host: "43.156.151.237"
password: "test_redis_password" # 测试环境Redis密码
rabbitmq:
host: "43.156.151.237"
password: "test_rabbitmq_password" # 测试环境RabbitMQ密码

View File

@ -0,0 +1,59 @@
package controllers
import (
"net/http"
"ly.system.api/container"
"ly.system.api/validators"
"github.com/gin-gonic/gin"
)
// UserController 用户控制器
type UserController struct {
container *container.Container
}
// NewUserController 创建新的UserController实例
func NewUserController(container *container.Container) *UserController {
return &UserController{container: container}
}
// Register 用户注册处理函数
func (uc *UserController) Register(c *gin.Context) {
var req validators.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"})
return
}
// 调用用户服务进行注册
err := uc.container.GetUserService().Register(req.Username, req.Password, req.Email)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "用户注册成功"})
}
// Login 用户登录处理函数
func (uc *UserController) Login(c *gin.Context) {
var req validators.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"})
return
}
// 调用用户服务进行登录
token, err := uc.container.GetUserService().Login(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"token": token,
})
}

72
internal/cache/redis_service.go vendored Normal file
View File

@ -0,0 +1,72 @@
package cache
import (
"github.com/go-redis/redis/v8"
"context"
"time"
)
// RedisService 定义 Redis 服务接口
// 提供对 Redis 的基本操作
// 如 Incr 和 Expire
// 以及其他常用操作
type RedisService interface {
Incr(key string) (int64, error)
Expire(key string, expiration time.Duration) error
Set(key string, value interface{}, expiration time.Duration) error
Del(keys ...string) error
Get(key string) (string, error)
}
// redisService 实现 RedisService 接口
// 使用 go-redis 包进行操作
// 提供对 Redis 的基本操作
// 如 Incr 和 Expire
// 以及其他常用操作
type redisService struct {
client *redis.Client
}
// NewRedisService 创建 Redis 服务实例
// 参数 client: Redis 客户端实例
// 返回: 实现 RedisService 接口的实例
func NewRedisService(client *redis.Client) RedisService {
return &redisService{client: client}
}
// Incr 增加键的值
// 如果键不存在,则将其初始化为 0
// 然后增加 1
// 返回增加后的值
func (r *redisService) Incr(key string) (int64, error) {
return r.client.Incr(context.Background(), key).Result()
}
// Expire 设置键的过期时间
// 如果键不存在,则不执行任何操作
// 返回错误信息
func (r *redisService) Expire(key string, expiration time.Duration) error {
return r.client.Expire(context.Background(), key, expiration).Err()
}
// Set 设置键的值
// 如果键不存在,则创建一个新键
// 如果键存在,则覆盖原值
// 返回错误信息
func (r *redisService) Set(key string, value interface{}, expiration time.Duration) error {
return r.client.Set(context.Background(), key, value, expiration).Err()
}
// Del 删除键
// 如果键不存在,则不执行任何操作
// 返回错误信息
func (r *redisService) Del(keys ...string) error {
return r.client.Del(context.Background(), keys...).Err()
}
// Get 获取键的值
// 如果键不存在,则返回空字符串
// 返回键的值和错误信息
func (r *redisService) Get(key string) (string, error) {
return r.client.Get(context.Background(), key).Result()
}

View File

@ -0,0 +1,12 @@
package domain
// File 文件领域模型
type File struct {
ID uint `json:"id" gorm:"primaryKey"`
Filename string `json:"filename"`
OriginalName string `json:"original_name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Status string `json:"status"`
UploadPath string `json:"upload_path"`
}

View File

@ -0,0 +1,40 @@
package domain
// FileInfo 文件信息
// 包含文件的基本信息
// 例如文件名、大小、类型等
// 用于文件上传和下载验证
// 以及文件操作权限验证
// 例如文件名、大小、类型等
// 用于文件上传和下载验证
// 以及文件操作权限验证
type FileInfo struct {
Filename string
Size int64
Type string
}
// ChunkInfo 分片信息
// 包含分片的基本信息
// 例如分片索引、大小、哈希等
// 用于分片上传验证
type ChunkInfo struct {
Index int
Size int64
Hash string
Total int
}
// FileDomainService 文件管理领域服务,处理文件相关的核心业务规则
type FileDomainService interface {
// ValidateFileUpload 验证文件上传请求
ValidateFileUpload(fileInfo *FileInfo) error
// ValidateChunkUpload 验证分片上传请求
ValidateChunkUpload(chunkInfo *ChunkInfo) error
// CalculateFileHash 计算文件哈希
CalculateFileHash(filePath string) (string, error)
// ValidateFileDownload 验证文件下载请求
ValidateFileDownload(fileID uint, userID uint) error
// ValidateFilePermissions 验证文件操作权限
ValidateFilePermissions(fileID uint, userID uint, operation string) error
}

View File

@ -0,0 +1,116 @@
package domain
import (
"crypto/sha256"
"encoding/hex"
"errors"
"os"
)
const (
MaxFileSize = 10485760 // 10MB
MaxChunkSize = 1048576 // 1MB
)
var (
ErrInvalidFileSize = errors.New("文件大小无效")
ErrInvalidFileType = errors.New("文件类型无效")
ErrInvalidChunkIndex = errors.New("分片索引无效")
ErrInvalidChunkSize = errors.New("分片大小无效")
ErrFileNotFound = errors.New("文件不存在")
ErrPermissionDenied = errors.New("权限拒绝")
)
// FileDomainServiceImpl 文件管理领域服务实现
type FileDomainServiceImpl struct{}
// NewFileDomainService 创建文件管理领域服务实例
func NewFileDomainService() FileDomainService {
return &FileDomainServiceImpl{}
}
// ValidateFileUpload 验证文件上传请求
func (s *FileDomainServiceImpl) ValidateFileUpload(fileInfo *FileInfo) error {
// 验证文件大小
if fileInfo.Size <= 0 || fileInfo.Size > MaxFileSize {
return ErrInvalidFileSize
}
// 验证文件类型
if !IsAllowedFileType(fileInfo.Type) {
return ErrInvalidFileType
}
return nil
}
// ValidateChunkUpload 验证分片上传请求
func (s *FileDomainServiceImpl) ValidateChunkUpload(chunkInfo *ChunkInfo) error {
// 验证分片序号
if chunkInfo.Index < 0 || chunkInfo.Index >= chunkInfo.Total {
return ErrInvalidChunkIndex
}
// 验证分片大小
if chunkInfo.Size <= 0 || chunkInfo.Size > MaxChunkSize {
return ErrInvalidChunkSize
}
return nil
}
// CalculateFileHash 计算文件哈希
func (s *FileDomainServiceImpl) CalculateFileHash(filePath string) (string, error) {
// 读取文件内容
data, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
// 计算SHA256哈希
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}
// ValidateFileDownload 验证文件下载请求
func (s *FileDomainServiceImpl) ValidateFileDownload(fileID uint, userID uint) error {
// 验证文件是否存在
if !FileExists(fileID) {
return ErrFileNotFound
}
// 验证用户下载权限
return s.ValidateFilePermissions(fileID, userID, "download")
}
// ValidateFilePermissions 验证文件操作权限
func (s *FileDomainServiceImpl) ValidateFilePermissions(fileID uint, userID uint, operation string) error {
// 检查用户是否是文件所有者
if !IsFileOwner(fileID, userID) {
// 检查用户是否有共享权限
if !HasSharedPermission(fileID, userID, operation) {
return ErrPermissionDenied
}
}
return nil
}
func IsAllowedFileType(fileType string) bool {
// 假设允许的文件类型
allowedTypes := []string{"image/jpeg", "image/png", "application/pdf"}
for _, t := range allowedTypes {
if fileType == t {
return true
}
}
return false
}
func FileExists(fileID uint) bool {
// TODO: 实现文件存在检查逻辑
return true
}
func IsFileOwner(fileID uint, userID uint) bool {
// TODO: 实现文件所有者检查逻辑
return true
}
func HasSharedPermission(fileID uint, userID uint, operation string) bool {
// TODO: 实现共享权限检查逻辑
return true
}

View File

@ -0,0 +1,61 @@
package repository
import (
"ly.system.api/internal/file/domain"
"gorm.io/gorm"
)
// FileRepository 文件仓储接口
type FileRepository interface {
Create(file *domain.File) error
FindByID(id uint) (*domain.File, error)
Update(file *domain.File) error
Delete(id uint) error
FindByFilename(filename string) (*domain.File, error)
}
// fileRepository 文件仓储实现
type fileRepository struct {
db *gorm.DB
}
// NewFileRepository 创建文件仓储实例
func NewFileRepository(db *gorm.DB) FileRepository {
return &fileRepository{db: db}
}
// Create 创建文件记录
func (r *fileRepository) Create(file *domain.File) error {
return r.db.Create(file).Error
}
// FindByID 根据ID查找文件
func (r *fileRepository) FindByID(id uint) (*domain.File, error) {
var file domain.File
err := r.db.First(&file, id).Error
if err != nil {
return nil, err
}
return &file, nil
}
// Update 更新文件信息
func (r *fileRepository) Update(file *domain.File) error {
return r.db.Save(file).Error
}
// Delete 删除文件记录
func (r *fileRepository) Delete(id uint) error {
return r.db.Delete(&domain.File{}, id).Error
}
// FindByFilename 根据文件名查找文件
func (r *fileRepository) FindByFilename(filename string) (*domain.File, error) {
var file domain.File
err := r.db.Where("filename = ?", filename).First(&file).Error
if err != nil {
return nil, err
}
return &file, nil
}

View File

@ -0,0 +1,190 @@
package service
import (
"errors"
"fmt"
"io"
"ly.system.api/internal/file/domain"
"ly.system.api/internal/file/repository"
"mime/multipart"
"os"
"path/filepath"
"strconv"
"time"
"ly.system.api/internal/cache"
)
// FileService 文件服务接口
type FileService interface {
UploadFile(file *multipart.FileHeader, chunkNumber, totalChunks, chunkSize int, fileID string) error
DownloadFile(id uint) (*domain.File, error)
GetUploadProgress(fileID string) (int, error)
CombineChunks(tempPath string, totalChunks int) (string, error)
}
// fileService 文件服务实现
type fileService struct {
fileRepo repository.FileRepository
redisService cache.RedisService
uploadDir string
}
// NewFileService 创建文件服务实例
func NewFileService(fileRepo repository.FileRepository, redisService cache.RedisService, uploadDir string) FileService {
return &fileService{
fileRepo: fileRepo,
redisService: redisService,
uploadDir: uploadDir,
}
}
// UploadFile 处理文件上传
func (s *fileService) UploadFile(file *multipart.FileHeader, chunkNumber, totalChunks, chunkSize int, fileID string) error {
// 验证参数
if file == nil || chunkNumber < 1 || totalChunks < 1 || chunkSize < 1 || fileID == "" {
return errors.New("无效的上传参数")
}
// 保存分片
tempPath, err := s.saveChunk(file, chunkNumber)
if err != nil {
return fmt.Errorf("保存分片失败: %v", err)
}
// 更新上传进度
progress := int(float64(chunkNumber) / float64(totalChunks) * 100)
if err := s.redisService.Set("upload_progress:"+fileID, strconv.Itoa(progress), 24*time.Hour); err != nil {
return fmt.Errorf("更新进度失败: %v", err)
}
// 如果是最后一个分片,合并文件
if chunkNumber == totalChunks {
finalPath, err := s.CombineChunks(tempPath, totalChunks)
if err != nil {
return fmt.Errorf("合并文件失败: %v", err)
}
// 创建文件记录
fileInfo, err := os.Stat(finalPath)
if err != nil {
return fmt.Errorf("获取文件信息失败: %v", err)
}
fileRecord := &domain.File{
Filename: filepath.Base(finalPath),
OriginalName: file.Filename,
Size: fileInfo.Size(),
MimeType: file.Header.Get("Content-Type"),
Status: "completed",
UploadPath: finalPath,
}
if err := s.fileRepo.Create(fileRecord); err != nil {
// 如果数据库保存失败,删除已上传的文件
os.Remove(finalPath)
return fmt.Errorf("保存文件记录失败: %v", err)
}
// 清除进度
s.redisService.Del("upload_progress:" + fileID)
}
return nil
}
// DownloadFile 处理文件下载
func (s *fileService) DownloadFile(id uint) (*domain.File, error) {
file, err := s.fileRepo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("文件不存在: %v", err)
}
// 检查文件是否存在
if _, err := os.Stat(file.UploadPath); err != nil {
return nil, fmt.Errorf("文件不存在或已被删除: %v", err)
}
return file, nil
}
// GetUploadProgress 获取上传进度
func (s *fileService) GetUploadProgress(fileID string) (int, error) {
progress, err := s.redisService.Get("upload_progress:" + fileID)
if err != nil {
return 0, err
}
progressInt, err := strconv.Atoi(progress)
if err != nil {
return 0, err
}
return progressInt, nil
}
// saveChunk 保存文件分片
func (s *fileService) saveChunk(file *multipart.FileHeader, chunkNumber int) (string, error) {
// 创建临时目录
tempDir := filepath.Join(s.uploadDir, "temp")
if err := os.MkdirAll(tempDir, 0755); err != nil {
return "", err
}
// 生成临时文件名
tempFilename := filepath.Join(tempDir, file.Filename+".part"+fmt.Sprint(chunkNumber))
// 打开上传的文件分片
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
// 创建目标文件
dst, err := os.Create(tempFilename)
if err != nil {
return "", err
}
defer dst.Close()
// 复制文件内容
if _, err := io.Copy(dst, src); err != nil {
return "", err
}
return tempFilename, nil
}
// CombineChunks 合并文件分片
func (s *fileService) CombineChunks(tempPath string, totalChunks int) (string, error) {
// 获取最终文件名
finalFilename := filepath.Join(s.uploadDir, filepath.Base(tempPath[:len(tempPath)-5]))
// 创建最终文件
finalFile, err := os.Create(finalFilename)
if err != nil {
return "", err
}
defer finalFile.Close()
// 合并所有分片
for i := 1; i <= totalChunks; i++ {
chunkPath := tempPath[:len(tempPath)-1] + fmt.Sprint(i)
chunkFile, err := os.Open(chunkPath)
if err != nil {
return "", err
}
if _, err := io.Copy(finalFile, chunkFile); err != nil {
chunkFile.Close()
return "", err
}
chunkFile.Close()
// 删除临时分片文件
os.Remove(chunkPath)
}
return finalFilename, nil
}

View File

@ -0,0 +1,71 @@
package domain
import (
"errors"
"time"
)
// LotteryDomainService 抽奖领域服务,处理抽奖相关的核心业务规则
type LotteryDomainService interface {
// ValidateLotteryRules 验证抽奖规则
ValidateLotteryRules(rules *LotteryRules) error
// CheckUserEligibility 检查用户是否有资格参与抽奖
CheckUserEligibility(userID uint, rules *LotteryRules) error
// CalculateWinningProbability 计算中奖概率
CalculateWinningProbability(userID uint, rules *LotteryRules) float64
// ValidatePrizeDistribution 验证奖品分配是否合理
ValidatePrizeDistribution(prizes []Prize) error
}
// LotteryRules 抽奖规则
type LotteryRules struct {
MaxEntries int
StartTime time.Time
EndTime time.Time
MaxDraws int
BaseProbability float64
}
func (r *LotteryRules) IsUserAllowed(userID uint) bool {
// 假设所有用户都被允许
return true
}
func (r *LotteryRules) HasExceededDrawLimit(userID uint) bool {
// 假设没有用户超出抽奖次数
return false
}
func (r *LotteryRules) GetUserLevelMultiplier(userID uint) float64 {
// 假设用户等级乘数为1.0
return 1.0
}
func (r *LotteryRules) GetTimeBasedMultiplier() float64 {
// 假设时间基础乘数为1.0
return 1.0
}
// Prize 奖品
type Prize struct {
Name string
Value int
Probability float64
}
var (
// ErrInvalidTimeRange 无效的时间范围
ErrInvalidTimeRange = errors.New("无效的时间范围")
// ErrInvalidDrawLimit 抽奖次数限制无效
ErrInvalidDrawLimit = errors.New("抽奖次数限制无效")
// ErrUserNotAllowed 用户不被允许参与抽奖
ErrUserNotAllowed = errors.New("用户不被允许参与抽奖")
// ErrDrawLimitExceeded 抽奖次数超出限制
ErrDrawLimitExceeded = errors.New("抽奖次数超出限制")
// ErrNoPrizes 没有奖品可分配
ErrNoPrizes = errors.New("没有奖品可分配")
// ErrInvalidPrizeProbability 奖品概率无效
ErrInvalidPrizeProbability = errors.New("奖品概率无效")
// ErrInvalidTotalProbability 奖品概率总和无效
ErrInvalidTotalProbability = errors.New("奖品概率总和无效")
)

View File

@ -0,0 +1,69 @@
package domain
// LotteryDomainServiceImpl 抽奖领域服务实现
type LotteryDomainServiceImpl struct{}
// NewLotteryDomainService 创建抽奖领域服务实例
func NewLotteryDomainService() LotteryDomainService {
return &LotteryDomainServiceImpl{}
}
// ValidateLotteryRules 验证抽奖规则
func (s *LotteryDomainServiceImpl) ValidateLotteryRules(rules *LotteryRules) error {
// 验证抽奖时间范围
if rules.StartTime.After(rules.EndTime) {
return ErrInvalidTimeRange
}
// 验证抽奖次数限制
if rules.MaxDraws <= 0 {
return ErrInvalidDrawLimit
}
return nil
}
// CheckUserEligibility 检查用户是否有资格参与抽奖
func (s *LotteryDomainServiceImpl) CheckUserEligibility(userID uint, rules *LotteryRules) error {
// 检查用户状态
if !rules.IsUserAllowed(userID) {
return ErrUserNotAllowed
}
// 检查抽奖次数
if rules.HasExceededDrawLimit(userID) {
return ErrDrawLimitExceeded
}
return nil
}
// CalculateWinningProbability 计算中奖概率
func (s *LotteryDomainServiceImpl) CalculateWinningProbability(userID uint, rules *LotteryRules) float64 {
// 基础中奖概率
baseProbability := rules.BaseProbability
// 根据用户等级调整概率
levelMultiplier := rules.GetUserLevelMultiplier(userID)
// 根据活动时间调整概率
timeMultiplier := rules.GetTimeBasedMultiplier()
return baseProbability * levelMultiplier * timeMultiplier
}
// ValidatePrizeDistribution 验证奖品分配是否合理
func (s *LotteryDomainServiceImpl) ValidatePrizeDistribution(prizes []Prize) error {
// 验证奖品总数
if len(prizes) == 0 {
return ErrNoPrizes
}
// 验证奖品概率总和是否为100%
totalProbability := 0.0
for _, prize := range prizes {
if prize.Probability < 0 || prize.Probability > 1 {
return ErrInvalidPrizeProbability
}
totalProbability += prize.Probability
}
if totalProbability != 1.0 {
return ErrInvalidTotalProbability
}
return nil
}

View File

@ -0,0 +1,10 @@
package domain
// Song 歌曲领域模型
type Song struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Artist string `json:"artist"`
Duration int `json:"duration"`
Status string `json:"status"`
}

View File

@ -0,0 +1,72 @@
package repository
import (
"ly.system.api/internal/lottery/domain"
"gorm.io/gorm"
)
// SongRepository 歌曲仓储接口
type SongRepository interface {
Create(song *domain.Song) error
FindByID(id uint) (*domain.Song, error)
FindAll() ([]*domain.Song, error)
Update(song *domain.Song) error
Delete(id uint) error
FindAvailable() ([]*domain.Song, error)
}
// songRepository 歌曲仓储实现
type songRepository struct {
db *gorm.DB
}
// NewSongRepository 创建歌曲仓储实例
func NewSongRepository(db *gorm.DB) SongRepository {
return &songRepository{db: db}
}
// Create 创建歌曲
func (r *songRepository) Create(song *domain.Song) error {
return r.db.Create(song).Error
}
// FindByID 根据ID查找歌曲
func (r *songRepository) FindByID(id uint) (*domain.Song, error) {
var song domain.Song
err := r.db.First(&song, id).Error
if err != nil {
return nil, err
}
return &song, nil
}
// FindAll 获取所有歌曲
func (r *songRepository) FindAll() ([]*domain.Song, error) {
var songs []*domain.Song
err := r.db.Find(&songs).Error
if err != nil {
return nil, err
}
return songs, nil
}
// Update 更新歌曲信息
func (r *songRepository) Update(song *domain.Song) error {
return r.db.Save(song).Error
}
// Delete 删除歌曲
func (r *songRepository) Delete(id uint) error {
return r.db.Delete(&domain.Song{}, id).Error
}
// FindAvailable 获取可用歌曲列表
func (r *songRepository) FindAvailable() ([]*domain.Song, error) {
var songs []*domain.Song
err := r.db.Where("status = ?", "available").Find(&songs).Error
if err != nil {
return nil, err
}
return songs, nil
}

View File

@ -0,0 +1,90 @@
package service
import (
"errors"
"ly.system.api/internal/lottery/domain"
"ly.system.api/internal/lottery/repository"
"math/rand"
"time"
)
// LotteryService 抽奖服务接口
type LotteryService interface {
Draw() (*domain.Song, error)
GetAvailableSongs() ([]*domain.Song, error)
AddSong(song *domain.Song) error
UpdateSong(song *domain.Song) error
DeleteSong(id uint) error
}
// lotteryService 抽奖服务实现
type lotteryService struct {
songRepo repository.SongRepository
}
// NewLotteryService 创建抽奖服务实例
func NewLotteryService(songRepo repository.SongRepository) LotteryService {
return &lotteryService{
songRepo: songRepo,
}
}
// Draw 随机抽取一首歌曲
func (s *lotteryService) Draw() (*domain.Song, error) {
// 获取所有可用歌曲
songs, err := s.songRepo.FindAvailable()
if err != nil {
return nil, err
}
if len(songs) == 0 {
return nil, errors.New("没有可用的歌曲")
}
// 使用当前时间作为随机种子
rand.Seed(time.Now().UnixNano())
// 随机选择一首歌曲
selectedSong := songs[rand.Intn(len(songs))]
return selectedSong, nil
}
// GetAvailableSongs 获取所有可用歌曲
func (s *lotteryService) GetAvailableSongs() ([]*domain.Song, error) {
return s.songRepo.FindAvailable()
}
// AddSong 添加新歌曲
func (s *lotteryService) AddSong(song *domain.Song) error {
// 设置歌曲状态为可用
song.Status = "available"
return s.songRepo.Create(song)
}
// UpdateSong 更新歌曲信息
func (s *lotteryService) UpdateSong(song *domain.Song) error {
// 检查歌曲是否存在
existingSong, err := s.songRepo.FindByID(song.ID)
if err != nil {
return errors.New("歌曲不存在")
}
// 更新歌曲信息
existingSong.Title = song.Title
existingSong.Artist = song.Artist
existingSong.Duration = song.Duration
existingSong.Status = song.Status
return s.songRepo.Update(existingSong)
}
// DeleteSong 删除歌曲
func (s *lotteryService) DeleteSong(id uint) error {
// 检查歌曲是否存在
_, err := s.songRepo.FindByID(id)
if err != nil {
return errors.New("歌曲不存在")
}
return s.songRepo.Delete(id)
}

49
internal/menu/service.go Normal file
View File

@ -0,0 +1,49 @@
package menu
// MenuService 菜单服务接口
type MenuService interface {
GetMenuTree() ([]MenuItem, error)
CreateMenu(item MenuItem) error
UpdateMenu(item MenuItem) error
DeleteMenu(id string) error
}
// MenuItem 菜单项结构
type MenuItem struct {
ID string `json:"id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
Order int `json:"order"`
}
// menuService 实现 MenuService 接口
type menuService struct {}
// NewMenuService 创建新的菜单服务实例
func NewMenuService() MenuService {
return &menuService{}
}
// GetMenuTree 获取菜单树
func (s *menuService) GetMenuTree() ([]MenuItem, error) {
// 实现获取菜单树的逻辑
return []MenuItem{}, nil
}
// CreateMenu 创建菜单
func (s *menuService) CreateMenu(item MenuItem) error {
// 实现创建菜单的逻辑
return nil
}
// UpdateMenu 更新菜单
func (s *menuService) UpdateMenu(item MenuItem) error {
// 实现更新菜单的逻辑
return nil
}
// DeleteMenu 删除菜单
func (s *menuService) DeleteMenu(id string) error {
// 实现删除菜单的逻辑
return nil
}

View File

@ -0,0 +1,38 @@
package domain
import (
"errors"
"ly.system.api/pkg/auth"
)
// 定义缺失的错误
var (
ErrInvalidUsername = errors.New("无效的用户名")
ErrInvalidPassword = errors.New("无效的密码")
ErrInvalidOldPassword = errors.New("旧密码不正确")
ErrInvalidNewPassword = errors.New("新密码不符合要求")
)
// User 用户领域模型
// 添加缺失的字段和方法
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex"`
Password string `json:"password"`
Email string `json:"email"`
Role string `json:"role"`
Avatar string `json:"avatar"`
Status int `json:"status"`
}
// ValidatePassword 验证密码
func (u *User) ValidatePassword(password string) bool {
// 使用authService进行密码验证
authService := auth.NewAuthService()
return authService.ComparePasswords(u.Password, password)
}
// 定义用户状态常量
const (
UserStatusActive = 1
)

View File

@ -0,0 +1,11 @@
package domain
// UserDomainService 用户领域服务,处理用户相关的核心业务规则
type UserDomainService interface {
// ValidateUserRegistration 验证用户注册信息
ValidateUserRegistration(user *User) error
// ValidatePasswordChange 验证密码修改
ValidatePasswordChange(user *User, oldPassword, newPassword string) error
// EnrichUserProfile 丰富用户信息
EnrichUserProfile(user *User) error
}

View File

@ -0,0 +1,48 @@
package domain
// UserDomainServiceImpl 用户领域服务实现
type UserDomainServiceImpl struct {}
// NewUserDomainService 创建用户领域服务实例
func NewUserDomainService() UserDomainService {
return &UserDomainServiceImpl{}
}
// ValidateUserRegistration 验证用户注册信息
func (s *UserDomainServiceImpl) ValidateUserRegistration(user *User) error {
// 验证用户名长度
if len(user.Username) < 3 || len(user.Username) > 20 {
return ErrInvalidUsername
}
// 验证密码强度
if len(user.Password) < 6 || len(user.Password) > 20 {
return ErrInvalidPassword
}
return nil
}
// ValidatePasswordChange 验证密码修改
func (s *UserDomainServiceImpl) ValidatePasswordChange(user *User, oldPassword, newPassword string) error {
// 验证旧密码是否正确
if !user.ValidatePassword(oldPassword) {
return ErrInvalidOldPassword
}
// 验证新密码强度
if len(newPassword) < 6 || len(newPassword) > 20 {
return ErrInvalidNewPassword
}
return nil
}
// EnrichUserProfile 丰富用户信息
func (s *UserDomainServiceImpl) EnrichUserProfile(user *User) error {
// 设置默认头像
if user.Avatar == "" {
user.Avatar = "default_avatar.png"
}
// 设置用户状态
if user.Status == 0 {
user.Status = UserStatusActive
}
return nil
}

View File

@ -0,0 +1,60 @@
package repository
import (
"gorm.io/gorm"
"ly.system.api/internal/user/domain"
)
// UserRepository 用户仓储接口
type UserRepository interface {
Create(user *domain.User) error
FindByID(id uint) (*domain.User, error)
FindByUsername(username string) (*domain.User, error)
Update(user *domain.User) error
Delete(id uint) error
}
// userRepository 用户仓储实现
type userRepository struct {
db *gorm.DB
}
// NewUserRepository 创建用户仓储实例
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
// Create 创建用户
func (r *userRepository) Create(user *domain.User) error {
return r.db.Create(user).Error
}
// FindByID 根据ID查找用户
func (r *userRepository) FindByID(id uint) (*domain.User, error) {
var user domain.User
err := r.db.First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
// FindByUsername 根据用户名查找用户
func (r *userRepository) FindByUsername(username string) (*domain.User, error) {
var user domain.User
err := r.db.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// Update 更新用户信息
func (r *userRepository) Update(user *domain.User) error {
return r.db.Save(user).Error
}
// Delete 删除用户
func (r *userRepository) Delete(id uint) error {
return r.db.Delete(&domain.User{}, id).Error
}

View File

@ -0,0 +1,91 @@
package service
import (
"errors"
"ly.system.api/internal/user/domain"
"ly.system.api/internal/user/repository"
"ly.system.api/pkg/auth"
)
// UserService 用户服务接口
type UserService interface {
Register(username, password, email string) error
Login(username, password string) (string, error)
GetUserInfo(id uint) (*domain.User, error)
UpdateUser(user *domain.User) error
DeleteUser(id uint) error
}
// userService 用户服务实现
type userService struct {
userRepo repository.UserRepository
authService auth.Service
}
// NewUserService 创建用户服务实例
func NewUserService(userRepo repository.UserRepository, authService auth.Service) UserService {
return &userService{
userRepo: userRepo,
authService: authService,
}
}
// Register 用户注册
func (s *userService) Register(username, password, email string) error {
// 检查用户名是否已存在
if _, err := s.userRepo.FindByUsername(username); err == nil {
return errors.New("用户名已存在")
}
// 创建新用户
hashedPassword, err := s.authService.HashPassword(password)
if err != nil {
return err
}
user := &domain.User{
Username: username,
Password: hashedPassword,
Email: email,
Role: "user",
}
return s.userRepo.Create(user)
}
// Login 用户登录
func (s *userService) Login(username, password string) (string, error) {
// 查找用户
user, err := s.userRepo.FindByUsername(username)
if err != nil {
return "", errors.New("用户名或密码错误")
}
// 验证密码
if !s.authService.ComparePasswords(user.Password, password) {
return "", errors.New("用户名或密码错误")
}
// 生成JWT令牌
token, err := s.authService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
return "", err
}
return token, nil
}
// GetUserInfo 获取用户信息
func (s *userService) GetUserInfo(id uint) (*domain.User, error) {
return s.userRepo.FindByID(id)
}
// UpdateUser 更新用户信息
func (s *userService) UpdateUser(user *domain.User) error {
return s.userRepo.Update(user)
}
// DeleteUser 删除用户
func (s *userService) DeleteUser(id uint) error {
return s.userRepo.Delete(id)
}

49
middlewares/jwt_auth.go Normal file
View File

@ -0,0 +1,49 @@
package middlewares
import (
"ly.system.api/utils"
"net/http"
"github.com/gin-gonic/gin"
)
// JWTAuth JWT认证中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "未提供认证令牌",
})
return
}
// 检查Bearer token格式
const bearerPrefix = "Bearer "
if len(authHeader) <= len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "认证令牌格式错误",
})
return
}
// 提取并验证token
token := authHeader[len(bearerPrefix):]
claims, err := utils.ParseJWT(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "无效的认证令牌",
})
return
}
// 将用户信息存入上下文
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("roles", claims.Roles)
c.Next()
}
}

41
pkg/auth/auth.go Normal file
View File

@ -0,0 +1,41 @@
package auth
import (
"golang.org/x/crypto/bcrypt"
"ly.system.api/utils"
)
// Service 认证服务接口
type Service interface {
HashPassword(password string) (string, error)
ComparePasswords(hashedPassword, password string) bool
GenerateToken(userID uint, username string, role string) (string, error)
}
// service 认证服务实现
type service struct{}
// NewAuthService 创建认证服务实例
func NewAuthService() Service {
return &service{}
}
// HashPassword 对密码进行哈希处理
func (s *service) HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// ComparePasswords 比较密码是否匹配
func (s *service) ComparePasswords(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// GenerateToken 生成JWT令牌
func (s *service) GenerateToken(userID uint, username string, role string) (string, error) {
return utils.GenerateJWT(userID, username, role, []string{role})
}

97
refactor_operations.log Normal file
View File

@ -0,0 +1,97 @@
# 项目重构操作日志 请遵守此规则开发
## 每次需要操作完成都需要写入次此文件作为记录
## 重构计划
### 1. 目录结构优化 (DDD设计原则)
- [x] 按业务领域划分模块:用户、抽奖、文件管理
- 已完成用户领域的重构包含domain、repository和service层
- 已完成抽奖领域的重构包含domain、repository和service层
- 已完成文件管理领域的重构包含domain、repository和service层
- [x] 每个领域模块包含自己的控制器、服务、模型和仓储层
- 已删除旧的models目录下的文件file.go、user.go、song.go、menu.go
- 已删除旧的services目录下的文件file_service.go、user_service.go、lottery_service.go、menu_service.go、interfaces.go、rabbitmq_service.go、redis_service.go
- 已删除旧的controllers目录下的文件file_controller.go、user_controller.go、lottery_controller.go、menu_controller.go
### 2. 依赖关系优化
- [x] 修正包引用路径
- 更新container.go中的包引用路径为ly.system.api
- 更新routes.go中的包引用路径为ly.system.api移除旧的controllers包引用
- 更新utils/jwt.go中的包引用路径为ly.system.api
- 完成internal目录下所有领域模块的包引用路径检查
- 完成pkg目录下的包引用路径检查
- 完成middlewares目录下的包引用路径检查
- 完成tests目录下的包引用路径检查
- [ ] 引入依赖倒置原则
- [ ] 通过接口解耦服务层
- [ ] 优化依赖注入机制
### 3. 配置管理统一
- [ ] 统一配置文件管理
- [ ] 实现多环境支持
### 4. 错误处理机制
- [ ] 实现统一的错误处理服务
- [ ] 规范化错误返回格式
### 5. 日志服务规范化
- [ ] 实现统一的日志记录服务
- [ ] 规范化日志格式
### 6. 测试覆盖完善
- [ ] 补充单元测试
- [ ] 补充集成测试
## 操作记录
### 2024-01-10
1. 删除旧的目录结构:
- 删除controllers目录
- 删除models目录
- 删除services目录
- 删除interfaces目录
- 删除event_handlers目录
这些目录的功能已经被迁移到internal目录下的对应领域模块中。
### 2025-02-06
1. 创建重构操作日志文件
2. 制定重构计划
3. 完成用户领域的DDD架构重构
- 创建internal/user/domain目录定义User领域模型
- 创建internal/user/repository目录实现用户仓储层
- 创建internal/user/service目录实现用户服务层
4. 完成抽奖领域的DDD架构重构
- 创建internal/lottery/domain目录定义Song领域模型
- 创建internal/lottery/repository目录实现歌曲仓储层
- 创建internal/lottery/service目录实现抽奖服务层
5. 完成文件管理领域的重构:
- 创建internal/file/domain目录定义File领域模型
- 创建internal/file/repository目录实现文件仓储层
- 创建internal/file/service目录实现文件服务层
### 2025-02-07
1. 重构文件服务层:
- 优化FileService接口设计实现文件上传、下载和进度查询功能
- 实现分片上传功能,支持大文件断点续传
- 添加Redis服务用于存储文件上传进度
- 实现文件合并功能,支持分片文件的合并处理
- 完善错误处理和日志记录
2. 修正包引用路径:
- 更新container.go中的包引用路径将其修改为ly.system.api
- 更新routes.go中的包引用路径将其修改为ly.system.api
- 更新utils/jwt.go中的包引用路径将其修改为ly.system.api
### 2025-02-08
1. 开始依赖关系优化:
- 分析现有服务层代码,识别紧耦合的关系
- 在lottery_service.go中引入依赖倒置原则将SongRepository作为接口注入
- 优化container.go中的服务初始化逻辑减少服务间的直接依赖
- 重构服务层接口,确保遵循接口隔离原则
- 调整依赖注入机制,使用更灵活的工厂模式创建服务实例
### 2024-01-11
1. 优化utils/jwt.go文件
- 移除未使用的uuid包引用
- 优化包引用路径确保使用ly.system.api作为基础路径
- 完善JWT相关功能的实现包括令牌生成和解析

76
tests/config_test.go Normal file
View File

@ -0,0 +1,76 @@
package tests
import (
"os"
"testing"
"ly-user-center/config"
"github.com/stretchr/testify/assert"
)
func TestLoadConfig(t *testing.T) {
// 设置测试环境
SetupTestEnv(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 测试配置加载
cfg, err := config.LoadConfig()
assert.NoError(err, "配置加载应该成功")
assert.NotNil(cfg, "配置对象不应为空")
// 测试环境变量替换
assert.Equal("43.156.151.237", cfg.Database.Host, "测试环境数据库主机地址应正确解析")
// 测试Redis配置
assert.Equal(6379, cfg.Redis.Port, "Redis端口应正确配置")
assert.Equal(100, cfg.Redis.RateLimitMax, "限流最大请求数应正确配置")
// 测试RabbitMQ配置
assert.Equal("ly_system", cfg.RabbitMQ.Exchange, "RabbitMQ交换机名称应正确配置")
// 测试配置获取函数
redisConfig := config.GetRedisConfig()
assert.NotNil(redisConfig, "Redis配置不应为空")
assert.Equal(cfg.Redis.Host, redisConfig.Host, "Redis主机配置应一致")
// 测试GetValue函数
serverPort := config.GetValue("Server.Port")
assert.Equal("8999", serverPort, "服务器端口应正确获取")
// 测试无效配置键
invalidValue := config.GetValue("Invalid.Key")
assert.Empty(invalidValue, "无效配置键应返回空字符串")
}
func TestConfigWithCustomPath(t *testing.T) {
// 初始化断言
assert := assert.New(t)
// 设置自定义配置路径
os.Setenv("CONFIG_PATH", "../config/config.yaml")
defer os.Unsetenv("CONFIG_PATH")
// 测试配置加载
cfg, err := config.LoadConfig()
assert.NoError(err, "使用自定义路径加载配置应成功")
assert.NotNil(cfg, "配置对象不应为空")
}
func TestLoadDBConfig(t *testing.T) {
// 设置测试环境
SetupTestEnv(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 测试数据库连接字符串生成
dsn := config.LoadDBConfig()
assert.Contains(dsn, "host=43.156.151.237", "数据库主机地址应正确包含在DSN中")
assert.Contains(dsn, "port=5432", "数据库端口应正确包含在DSN中")
assert.Contains(dsn, "sslmode=disable", "SSL模式应正确包含在DSN中")
}

137
tests/rabbitmq_test.go Normal file
View File

@ -0,0 +1,137 @@
package tests
import (
"encoding/json"
"testing"
"time"
"ly-user-center/config"
"ly-user-center/services"
"github.com/streadway/amqp"
"github.com/stretchr/testify/assert"
)
type TestEvent struct {
ID string `json:"id"`
Message string `json:"message"`
}
func setupRabbitMQTest(t *testing.T) services.RabbitMQService {
// 设置测试环境
SetupTestEnv(t)
// 获取RabbitMQ配置
config := config.GetRabbitMQConfig()
// 创建RabbitMQ服务
service := services.NewRabbitMQService()
// 确保交换机存在
err := service.EnsureExchange()
assert.NoError(t, err, "确保交换机存在应该成功")
return service
}
func TestRabbitMQPublishAndConsume(t *testing.T) {
// 初始化RabbitMQ服务
rabbitMQService := setupRabbitMQTest(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 创建测试事件
testEvent := TestEvent{
ID: "test-123",
Message: "测试消息",
}
// 发布事件
err := rabbitMQService.PublishEvent("test.event", testEvent)
assert.NoError(err, "发布事件应该成功")
// 创建通道接收消息
received := make(chan bool)
// 消费消息
err = rabbitMQService.Consume(config.GetRabbitMQConfig().Queue, func(data []byte) error {
// 解析消息
var receivedEvent TestEvent
err := json.Unmarshal(data, &receivedEvent)
assert.NoError(err, "解析消息应该成功")
// 验证消息内容
assert.Equal(testEvent.ID, receivedEvent.ID, "消息ID应该匹配")
assert.Equal(testEvent.Message, receivedEvent.Message, "消息内容应该匹配")
received <- true
return nil
})
assert.NoError(err, "消费消息应该成功")
// 等待消息处理完成
select {
case <-received:
// 消息处理成功
case <-time.After(5 * time.Second):
t.Fatal("消息处理超时")
}
}
func TestRabbitMQEventHandler(t *testing.T) {
// 初始化RabbitMQ服务
rabbitMQService := setupRabbitMQTest(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 创建测试事件处理器
handled := make(chan bool)
handler := &testEventHandler{handled: handled}
// 注册事件处理器
rabbitMQService.RegisterHandler("test.event", handler)
// 创建并发布测试事件
testEvent := TestEvent{
ID: "test-456",
Message: "测试事件处理",
}
// 序列化事件数据
data, err := json.Marshal(testEvent)
assert.NoError(err, "序列化事件数据应该成功")
// 分发事件
err = rabbitMQService.DispatchEvent("test.event", data)
assert.NoError(err, "分发事件应该成功")
// 等待事件处理完成
select {
case <-handled:
// 事件处理成功
case <-time.After(5 * time.Second):
t.Fatal("事件处理超时")
}
}
// testEventHandler 测试用事件处理器
type testEventHandler struct {
handled chan bool
}
func (h *testEventHandler) HandleEvent(payload []byte) error {
// 模拟事件处理
var event TestEvent
if err := json.Unmarshal(payload, &event); err != nil {
return err
}
// 发送处理完成信号
h.handled <- true
return nil
}

110
tests/redis_test.go Normal file
View File

@ -0,0 +1,110 @@
package tests
import (
"testing"
"time"
"ly-user-center/config"
"ly-user-center/services"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
)
func setupRedisTest(t *testing.T) services.RedisService {
// 设置测试环境
SetupTestEnv(t)
// 获取Redis配置
redisConfig := config.GetRedisConfig()
// 创建Redis客户端
TestRedis = redis.NewClient(&redis.Options{
Addr: redisConfig.Host + ":" + "6379",
Password: redisConfig.Password,
DB: redisConfig.DB,
})
// 创建Redis服务
return services.NewRedisService(TestRedis)
}
func TestRedisBasicOperations(t *testing.T) {
// 初始化Redis服务
redisService := setupRedisTest(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 测试Set和Get操作
key := "test_key"
value := "test_value"
err := redisService.Set(key, value, time.Minute)
assert.NoError(err, "设置键值对应该成功")
result, err := redisService.Get(key)
assert.NoError(err, "获取键值应该成功")
assert.Equal(value, result, "获取的值应该与设置的值相同")
// 测试过期时间
err = redisService.Set("expire_key", "expire_value", time.Second)
assert.NoError(err, "设置带过期时间的键值对应该成功")
// 测试删除操作
err = redisService.Del(key)
assert.NoError(err, "删除键应该成功")
_, err = redisService.Get(key)
assert.Error(err, "获取已删除的键应该返回错误")
}
func TestRedisRateLimit(t *testing.T) {
// 初始化Redis服务
redisService := setupRedisTest(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 测试限流功能
key := "rate_limit:test"
// 测试正常请求
for i := 0; i < config.GetRedisConfig().RateLimitMax; i++ {
allowed, err := redisService.CheckRateLimit(key)
assert.NoError(err, "检查限流应该成功")
assert.True(allowed, "未超过限制时应该允许请求")
}
// 测试超出限制的请求
allowed, err := redisService.CheckRateLimit(key)
assert.NoError(err, "检查限流应该成功")
assert.False(allowed, "超过限制时应该拒绝请求")
}
func TestRedisIncr(t *testing.T) {
// 初始化Redis服务
redisService := setupRedisTest(t)
defer TearDownTestEnv(t)
// 初始化断言
assert := assert.New(t)
// 测试递增操作
key := "counter_key"
// 第一次递增
count, err := redisService.Incr(key)
assert.NoError(err, "递增操作应该成功")
assert.Equal(int64(1), count, "第一次递增后值应该为1")
// 第二次递增
count, err = redisService.Incr(key)
assert.NoError(err, "递增操作应该成功")
assert.Equal(int64(2), count, "第二次递增后值应该为2")
// 设置过期时间
err = redisService.Expire(key, time.Second)
assert.NoError(err, "设置过期时间应该成功")
}

54
tests/test_utils.go Normal file
View File

@ -0,0 +1,54 @@
package tests
import (
"os"
"testing"
"github.com/go-redis/redis/v8"
"github.com/streadway/amqp"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
// TestDB 测试数据库连接
var TestDB *gorm.DB
// TestRedis 测试Redis客户端
var TestRedis *redis.Client
// TestRabbitMQ 测试RabbitMQ连接
var TestRabbitMQ *amqp.Connection
// SetupTestEnv 设置测试环境
func SetupTestEnv(t *testing.T) {
// 设置测试环境变量
os.Setenv("CONFIG_PATH", "../config/config.yaml")
os.Setenv("ENV", "test")
// 初始化测试断言
assert := assert.New(t)
// 确保测试环境变量设置正确
assert.Equal("test", os.Getenv("ENV"))
}
// TearDownTestEnv 清理测试环境
func TearDownTestEnv(t *testing.T) {
// 清理测试数据库连接
if TestDB != nil {
db, err := TestDB.DB()
if err == nil {
db.Close()
}
}
// 清理Redis连接
if TestRedis != nil {
TestRedis.Close()
}
// 清理RabbitMQ连接
if TestRabbitMQ != nil {
TestRabbitMQ.Close()
}
}

79
utils/api_response.go Normal file
View File

@ -0,0 +1,79 @@
package utils
import (
"github.com/gin-gonic/gin"
"strconv"
)
// APIResponse 定义统一的API响应格式
type APIResponse struct {
Code int `json:"code"` // 状态码
Message string `json:"message"` // 响应消息
Data interface{} `json:"data"` // 响应数据
}
// ResponseCode 定义API响应状态码
const (
Success = 200 // 成功
BadRequest = 400 // 请求参数错误
Unauthorized = 401 // 未授权
Forbidden = 403 // 禁止访问
NotFound = 404 // 资源不存在
InternalError = 500 // 服务器内部错误
)
// NewAPIResponse 创建新的API响应
func NewAPIResponse(code int, message string, data interface{}) *APIResponse {
return &APIResponse{
Code: code,
Message: message,
Data: data,
}
}
// SuccessResponse 创建成功响应并写入Gin上下文
func SuccessResponse(c *gin.Context, message string, data interface{}) {
c.JSON(Success, NewAPIResponse(Success, message, data))
}
// ErrorResponse 创建错误响应并写入Gin上下文
func ErrorResponse(c *gin.Context, code int, message string) {
c.JSON(code, NewAPIResponse(code, message, nil))
}
// UnauthorizedResponse 创建未授权响应
func UnauthorizedResponse(c *gin.Context) {
c.JSON(Unauthorized, NewAPIResponse(Unauthorized, "未授权访问", nil))
}
// ValidationErrorResponse 创建参数验证错误响应
func ValidationErrorResponse(c *gin.Context, message string) {
c.JSON(BadRequest, NewAPIResponse(BadRequest, message, nil))
}
// GetClaims 从Gin上下文中获取JWT Claims
func GetClaims(c *gin.Context) (*Claims, bool) {
claims, exists := c.Get("claims")
if !exists {
return nil, false
}
if userClaims, ok := claims.(*Claims); ok {
return userClaims, true
}
return nil, false
}
// GetUintParam 从Gin上下文中获取uint类型的参数
func GetUintParam(c *gin.Context, param string) (uint, error) {
val := c.Param(param)
if val == "" {
return 0, nil
}
uintVal, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return 0, err
}
return uint(uintVal), nil
}

61
utils/jwt.go Normal file
View File

@ -0,0 +1,61 @@
package utils
import (
"time"
"github.com/golang-jwt/jwt/v5"
"ly.system.api/config"
)
// Claims 包含JWT声明信息
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
// GenerateJWT 生成JWT令牌
func GenerateJWT(userID uint, username string, role string, roles []string) (string, error) {
// 获取JWT配置
jwtConfig := config.GetJWTConfig()
// 创建声明
claims := Claims{
UserID: userID,
Username: username,
Role: role,
Roles: roles,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(jwtConfig.Expire) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: jwtConfig.Issuer,
},
}
// 创建并签名令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtConfig.Secret))
}
// ParseJWT 解析并验证JWT令牌
func ParseJWT(tokenString string) (*Claims, error) {
// 获取JWT配置
jwtConfig := config.GetJWTConfig()
// 解析令牌
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtConfig.Secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrInvalidKey
}

View File

@ -0,0 +1,30 @@
// Package validators 定义请求验证的数据结构体
package validators
// RegisterRequest 用户注册请求验证结构体
type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
Password string `json:"password" validate:"required,min=8,containsany=@#$%^&*,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=0123456789"`
Email string `json:"email" validate:"required,email,max=100"`
Phone string `json:"phone" validate:"required,e164"`
Sex int32 `json:"sex" validate:"required,oneof=0 1 2"`
}
// LoginRequest 用户登录请求验证结构体
type LoginRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// RefreshTokenRequest 刷新令牌请求验证结构体
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
// FileUploadRequest 文件上传请求验证结构体
type FileUploadRequest struct {
ChunkNumber int `json:"chunkNumber" validate:"required,min=1"`
TotalChunks int `json:"totalChunks" validate:"required,min=1"`
ChunkSize int `json:"chunkSize" validate:"required,min=1"`
FileID string `json:"fileId" validate:"required"`
}

View File

@ -0,0 +1,32 @@
// Package validators 提供请求参数验证功能
package validators
import (
"github.com/go-playground/validator/v10"
"reflect"
"strings"
)
// RequestValidator 请求验证器
type RequestValidator struct {
validator *validator.Validate
}
// NewRequestValidator 创建新的请求验证器实例
func NewRequestValidator() *RequestValidator {
v := validator.New()
// 注册自定义标签处理函数
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
return &RequestValidator{validator: v}
}
// Validate 验证结构体
func (v *RequestValidator) Validate(i interface{}) error {
return v.validator.Struct(i)
}