From 10fdcd916b4daf2d52fb33b0845911613a1d7051 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 2 Dec 2025 10:33:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=A7=8D=E5=AD=90?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E6=9E=84=E5=A4=9A=E4=B8=AA=E5=A4=84=E7=90=86?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E4=BB=A5=E7=AE=80=E5=8C=96=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=92=8C=E7=94=A8=E6=88=B7=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 5 + internal/handler/auth_handler.go | 155 ++------ internal/handler/helpers.go | 160 ++++++++ internal/handler/profile_handler.go | 268 +++---------- internal/handler/texture_handler.go | 364 +++--------------- internal/handler/user_handler.go | 319 ++++----------- internal/handler/yggdrasil_handler.go | 6 +- internal/model/audit_log.go | 30 +- internal/model/profile.go | 20 +- internal/model/texture.go | 42 +- internal/model/token.go | 20 +- internal/model/user.go | 38 +- internal/model/yggdrasil.go | 45 ++- internal/repository/helpers.go | 82 ++++ internal/repository/profile_repository.go | 101 ++--- .../repository/system_config_repository.go | 37 +- internal/repository/texture_repository.go | 101 ++--- internal/repository/token_repository.go | 50 +-- internal/repository/user_repository.go | 65 +--- internal/repository/yggdrasil_repository.go | 9 +- internal/service/helpers.go | 105 +++++ internal/service/profile_service.go | 111 ++---- internal/service/security_service.go | 142 +++++++ internal/service/texture_service.go | 113 ++---- internal/service/user_service.go | 133 +++++-- internal/service/verification_service.go | 17 +- internal/service/yggdrasil_service.go | 27 +- pkg/database/seed.go | 156 ++++++++ pkg/redis/redis.go | 5 + scripts/carrotskin_postgres.sql | 343 ----------------- 30 files changed, 1291 insertions(+), 1778 deletions(-) create mode 100644 internal/handler/helpers.go create mode 100644 internal/repository/helpers.go create mode 100644 internal/service/helpers.go create mode 100644 internal/service/security_service.go create mode 100644 pkg/database/seed.go delete mode 100644 scripts/carrotskin_postgres.sql diff --git a/cmd/server/main.go b/cmd/server/main.go index 819b7ec..fb68942 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -49,6 +49,11 @@ func main() { loggerInstance.Fatal("数据库迁移失败", zap.Error(err)) } + // 初始化种子数据 + if err := database.Seed(loggerInstance); err != nil { + loggerInstance.Fatal("种子数据初始化失败", zap.Error(err)) + } + // 初始化JWT服务 if err := auth.Init(cfg.JWT); err != nil { loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err)) diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index a4524fc..c2ae087 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,15 +1,13 @@ package handler import ( - "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/internal/types" "carrotskin/pkg/auth" "carrotskin/pkg/email" "carrotskin/pkg/logger" "carrotskin/pkg/redis" - "net/http" - + "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -28,59 +26,32 @@ func Register(c *gin.Context) { loggerInstance := logger.MustGetLogger() jwtService := auth.MustGetJWTService() redisClient := redis.MustGetClient() - + var req types.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - + // 验证邮箱验证码 if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeRegister); err != nil { - loggerInstance.Warn("验证码验证失败", - zap.String("email", req.Email), - zap.Error(err), - ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + loggerInstance.Warn("验证码验证失败", zap.String("email", req.Email), zap.Error(err)) + RespondBadRequest(c, err.Error(), nil) return } - - // 调用service层注册用户(传递可选的头像URL) + + // 注册用户 user, token, err := service.RegisterUser(jwtService, req.Username, req.Password, req.Email, req.Avatar) if err != nil { loggerInstance.Error("用户注册失败", zap.Error(err)) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - - // 返回响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.LoginResponse{ - Token: token, - UserInfo: &types.UserInfo{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Points: user.Points, - Role: user.Role, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - }, - })) + + RespondSuccess(c, &types.LoginResponse{ + Token: token, + UserInfo: UserToUserInfo(user), + }) } // Login 用户登录 @@ -97,53 +68,32 @@ func Register(c *gin.Context) { func Login(c *gin.Context) { loggerInstance := logger.MustGetLogger() jwtService := auth.MustGetJWTService() - + redisClient := redis.MustGetClient() + var req types.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - - // 获取IP和UserAgent + ipAddress := c.ClientIP() userAgent := c.GetHeader("User-Agent") - - // 调用service层登录 - user, token, err := service.LoginUser(jwtService, req.Username, req.Password, ipAddress, userAgent) + + user, token, err := service.LoginUserWithRateLimit(redisClient, jwtService, req.Username, req.Password, ipAddress, userAgent) if err != nil { loggerInstance.Warn("用户登录失败", zap.String("username_or_email", req.Username), zap.String("ip", ipAddress), zap.Error(err), ) - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - err.Error(), - nil, - )) + RespondUnauthorized(c, err.Error()) return } - - // 返回响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.LoginResponse{ - Token: token, - UserInfo: &types.UserInfo{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Points: user.Points, - Role: user.Role, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - }, - })) + + RespondSuccess(c, &types.LoginResponse{ + Token: token, + UserInfo: UserToUserInfo(user), + }) } // SendVerificationCode 发送验证码 @@ -160,35 +110,24 @@ func SendVerificationCode(c *gin.Context) { loggerInstance := logger.MustGetLogger() redisClient := redis.MustGetClient() emailService := email.MustGetService() - + var req types.SendVerificationCodeRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // 发送验证码 if err := service.SendVerificationCode(c.Request.Context(), redisClient, emailService, req.Email, req.Type); err != nil { loggerInstance.Error("发送验证码失败", zap.String("email", req.Email), zap.String("type", req.Type), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "message": "验证码已发送,请查收邮件", - })) + RespondSuccess(c, gin.H{"message": "验证码已发送,请查收邮件"}) } // ResetPassword 重置密码 @@ -204,46 +143,26 @@ func SendVerificationCode(c *gin.Context) { func ResetPassword(c *gin.Context) { loggerInstance := logger.MustGetLogger() redisClient := redis.MustGetClient() - + var req types.ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } // 验证验证码 if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeResetPassword); err != nil { - loggerInstance.Warn("验证码验证失败", - zap.String("email", req.Email), - zap.Error(err), - ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + loggerInstance.Warn("验证码验证失败", zap.String("email", req.Email), zap.Error(err)) + RespondBadRequest(c, err.Error(), nil) return } // 重置密码 if err := service.ResetUserPassword(req.Email, req.NewPassword); err != nil { - loggerInstance.Error("重置密码失败", - zap.String("email", req.Email), - zap.Error(err), - ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + loggerInstance.Error("重置密码失败", zap.String("email", req.Email), zap.Error(err)) + RespondServerError(c, err.Error(), nil) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "message": "密码重置成功", - })) + RespondSuccess(c, gin.H{"message": "密码重置成功"}) } diff --git a/internal/handler/helpers.go b/internal/handler/helpers.go new file mode 100644 index 0000000..3e4489a --- /dev/null +++ b/internal/handler/helpers.go @@ -0,0 +1,160 @@ +package handler + +import ( + "carrotskin/internal/model" + "carrotskin/internal/types" + "net/http" + + "github.com/gin-gonic/gin" +) + +// GetUserIDFromContext 从上下文获取用户ID,如果不存在返回未授权响应 +// 返回值: userID, ok (如果ok为false,已经发送了错误响应) +func GetUserIDFromContext(c *gin.Context) (int64, bool) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, model.NewErrorResponse( + model.CodeUnauthorized, + model.MsgUnauthorized, + nil, + )) + return 0, false + } + return userID.(int64), true +} + +// UserToUserInfo 将 User 模型转换为 UserInfo 响应 +func UserToUserInfo(user *model.User) *types.UserInfo { + return &types.UserInfo{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Avatar: user.Avatar, + Points: user.Points, + Role: user.Role, + Status: user.Status, + LastLoginAt: user.LastLoginAt, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } +} + +// ProfileToProfileInfo 将 Profile 模型转换为 ProfileInfo 响应 +func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo { + return &types.ProfileInfo{ + UUID: profile.UUID, + UserID: profile.UserID, + Name: profile.Name, + SkinID: profile.SkinID, + CapeID: profile.CapeID, + IsActive: profile.IsActive, + LastUsedAt: profile.LastUsedAt, + CreatedAt: profile.CreatedAt, + UpdatedAt: profile.UpdatedAt, + } +} + +// ProfilesToProfileInfos 批量转换 Profile 模型为 ProfileInfo 响应 +func ProfilesToProfileInfos(profiles []*model.Profile) []*types.ProfileInfo { + result := make([]*types.ProfileInfo, 0, len(profiles)) + for _, profile := range profiles { + result = append(result, ProfileToProfileInfo(profile)) + } + return result +} + +// TextureToTextureInfo 将 Texture 模型转换为 TextureInfo 响应 +func TextureToTextureInfo(texture *model.Texture) *types.TextureInfo { + return &types.TextureInfo{ + ID: texture.ID, + UploaderID: texture.UploaderID, + Name: texture.Name, + Description: texture.Description, + Type: types.TextureType(texture.Type), + URL: texture.URL, + Hash: texture.Hash, + Size: texture.Size, + IsPublic: texture.IsPublic, + DownloadCount: texture.DownloadCount, + FavoriteCount: texture.FavoriteCount, + IsSlim: texture.IsSlim, + Status: texture.Status, + CreatedAt: texture.CreatedAt, + UpdatedAt: texture.UpdatedAt, + } +} + +// TexturesToTextureInfos 批量转换 Texture 模型为 TextureInfo 响应 +func TexturesToTextureInfos(textures []*model.Texture) []*types.TextureInfo { + result := make([]*types.TextureInfo, len(textures)) + for i, texture := range textures { + result[i] = TextureToTextureInfo(texture) + } + return result +} + +// RespondBadRequest 返回400错误响应 +func RespondBadRequest(c *gin.Context, message string, err error) { + c.JSON(http.StatusBadRequest, model.NewErrorResponse( + model.CodeBadRequest, + message, + err, + )) +} + +// RespondUnauthorized 返回401错误响应 +func RespondUnauthorized(c *gin.Context, message string) { + c.JSON(http.StatusUnauthorized, model.NewErrorResponse( + model.CodeUnauthorized, + message, + nil, + )) +} + +// RespondForbidden 返回403错误响应 +func RespondForbidden(c *gin.Context, message string) { + c.JSON(http.StatusForbidden, model.NewErrorResponse( + model.CodeForbidden, + message, + nil, + )) +} + +// RespondNotFound 返回404错误响应 +func RespondNotFound(c *gin.Context, message string) { + c.JSON(http.StatusNotFound, model.NewErrorResponse( + model.CodeNotFound, + message, + nil, + )) +} + +// RespondServerError 返回500错误响应 +func RespondServerError(c *gin.Context, message string, err error) { + c.JSON(http.StatusInternalServerError, model.NewErrorResponse( + model.CodeServerError, + message, + err, + )) +} + +// RespondSuccess 返回成功响应 +func RespondSuccess(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, model.NewSuccessResponse(data)) +} + +// RespondWithError 根据错误消息自动选择状态码 +func RespondWithError(c *gin.Context, err error) { + msg := err.Error() + switch msg { + case "档案不存在", "用户不存在", "材质不存在": + RespondNotFound(c, msg) + case "无权操作此档案", "无权操作此材质": + RespondForbidden(c, msg) + case "未授权": + RespondUnauthorized(c, msg) + default: + RespondServerError(c, msg, nil) + } +} + diff --git a/internal/handler/profile_handler.go b/internal/handler/profile_handler.go index 190bb50..cc0063b 100644 --- a/internal/handler/profile_handler.go +++ b/internal/handler/profile_handler.go @@ -1,12 +1,10 @@ package handler import ( - "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/internal/types" "carrotskin/pkg/database" "carrotskin/pkg/logger" - "net/http" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -26,70 +24,37 @@ import ( // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile [post] func CreateProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() - // 获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - // 解析请求 var req types.CreateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误: "+err.Error(), - nil, - )) + RespondBadRequest(c, "请求参数错误: "+err.Error(), nil) return } - // TODO: 从配置或数据库读取限制 - maxProfiles := 5 + maxProfiles := service.GetMaxProfilesPerUser() db := database.MustGetDB() - // 检查档案数量限制 - if err := service.CheckProfileLimit(db, userID.(int64), maxProfiles); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + + if err := service.CheckProfileLimit(db, userID, maxProfiles); err != nil { + RespondBadRequest(c, err.Error(), nil) return } - // 创建档案 - profile, err := service.CreateProfile(db, userID.(int64), req.Name) + profile, err := service.CreateProfile(db, userID, req.Name) if err != nil { - loggerInstance.Error("创建档案失败", - zap.Int64("user_id", userID.(int64)), + logger.MustGetLogger().Error("创建档案失败", + zap.Int64("user_id", userID), zap.String("name", req.Name), zap.Error(err), ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + RespondServerError(c, err.Error(), nil) return } - // 返回成功响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.ProfileInfo{ - UUID: profile.UUID, - UserID: profile.UserID, - Name: profile.Name, - SkinID: profile.SkinID, - CapeID: profile.CapeID, - IsActive: profile.IsActive, - LastUsedAt: profile.LastUsedAt, - CreatedAt: profile.CreatedAt, - UpdatedAt: profile.UpdatedAt, - })) + RespondSuccess(c, ProfileToProfileInfo(profile)) } // GetProfiles 获取档案列表 @@ -104,50 +69,22 @@ func CreateProfile(c *gin.Context) { // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile [get] func GetProfiles(c *gin.Context) { - loggerInstance := logger.MustGetLogger() - // 获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - // 查询档案列表 - profiles, err := service.GetUserProfiles(database.MustGetDB(), userID.(int64)) + profiles, err := service.GetUserProfiles(database.MustGetDB(), userID) if err != nil { - loggerInstance.Error("获取档案列表失败", - zap.Int64("user_id", userID.(int64)), + logger.MustGetLogger().Error("获取档案列表失败", + zap.Int64("user_id", userID), zap.Error(err), ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + RespondServerError(c, err.Error(), nil) return } - // 转换为响应格式 - result := make([]*types.ProfileInfo, 0, len(profiles)) - for _, profile := range profiles { - result = append(result, &types.ProfileInfo{ - UUID: profile.UUID, - UserID: profile.UserID, - Name: profile.Name, - SkinID: profile.SkinID, - CapeID: profile.CapeID, - IsActive: profile.IsActive, - LastUsedAt: profile.LastUsedAt, - CreatedAt: profile.CreatedAt, - UpdatedAt: profile.UpdatedAt, - }) - } - - c.JSON(http.StatusOK, model.NewSuccessResponse(result)) + RespondSuccess(c, ProfilesToProfileInfos(profiles)) } // GetProfile 获取档案详情 @@ -162,36 +99,19 @@ func GetProfiles(c *gin.Context) { // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile/{uuid} [get] func GetProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() uuid := c.Param("uuid") - // 查询档案 profile, err := service.GetProfileByUUID(database.MustGetDB(), uuid) if err != nil { - loggerInstance.Error("获取档案失败", + logger.MustGetLogger().Error("获取档案失败", zap.String("uuid", uuid), zap.Error(err), ) - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - err.Error(), - nil, - )) + RespondNotFound(c, err.Error()) return } - // 返回成功响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.ProfileInfo{ - UUID: profile.UUID, - UserID: profile.UserID, - Name: profile.Name, - SkinID: profile.SkinID, - CapeID: profile.CapeID, - IsActive: profile.IsActive, - LastUsedAt: profile.LastUsedAt, - CreatedAt: profile.CreatedAt, - UpdatedAt: profile.UpdatedAt, - })) + RespondSuccess(c, ProfileToProfileInfo(profile)) } // UpdateProfile 更新档案 @@ -211,72 +131,36 @@ func GetProfile(c *gin.Context) { // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile/{uuid} [put] func UpdateProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + uuid := c.Param("uuid") - // 获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) - return - } - - // 解析请求 var req types.UpdateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误: "+err.Error(), - nil, - )) + RespondBadRequest(c, "请求参数错误: "+err.Error(), nil) return } - // 更新档案 var namePtr *string if req.Name != "" { namePtr = &req.Name } - profile, err := service.UpdateProfile(database.MustGetDB(), uuid, userID.(int64), namePtr, req.SkinID, req.CapeID) + profile, err := service.UpdateProfile(database.MustGetDB(), uuid, userID, namePtr, req.SkinID, req.CapeID) if err != nil { - loggerInstance.Error("更新档案失败", + logger.MustGetLogger().Error("更新档案失败", zap.String("uuid", uuid), - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Error(err), ) - - statusCode := http.StatusInternalServerError - if err.Error() == "档案不存在" { - statusCode = http.StatusNotFound - } else if err.Error() == "无权操作此档案" { - statusCode = http.StatusForbidden - } - - c.JSON(statusCode, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + RespondWithError(c, err) return } - // 返回成功响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.ProfileInfo{ - UUID: profile.UUID, - UserID: profile.UserID, - Name: profile.Name, - SkinID: profile.SkinID, - CapeID: profile.CapeID, - IsActive: profile.IsActive, - LastUsedAt: profile.LastUsedAt, - CreatedAt: profile.CreatedAt, - UpdatedAt: profile.UpdatedAt, - })) + RespondSuccess(c, ProfileToProfileInfo(profile)) } // DeleteProfile 删除档案 @@ -294,48 +178,25 @@ func UpdateProfile(c *gin.Context) { // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile/{uuid} [delete] func DeleteProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + uuid := c.Param("uuid") - // 获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) - return - } - - // 删除档案 - err := service.DeleteProfile(database.MustGetDB(), uuid, userID.(int64)) + err := service.DeleteProfile(database.MustGetDB(), uuid, userID) if err != nil { - loggerInstance.Error("删除档案失败", + logger.MustGetLogger().Error("删除档案失败", zap.String("uuid", uuid), - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Error(err), ) - - statusCode := http.StatusInternalServerError - if err.Error() == "档案不存在" { - statusCode = http.StatusNotFound - } else if err.Error() == "无权操作此档案" { - statusCode = http.StatusForbidden - } - - c.JSON(statusCode, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + RespondWithError(c, err) return } - // 返回成功响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "message": "删除成功", - })) + RespondSuccess(c, gin.H{"message": "删除成功"}) } // SetActiveProfile 设置活跃档案 @@ -353,46 +214,23 @@ func DeleteProfile(c *gin.Context) { // @Failure 500 {object} model.ErrorResponse "服务器错误" // @Router /api/v1/profile/{uuid}/activate [post] func SetActiveProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + uuid := c.Param("uuid") - // 获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) - return - } - - // 设置活跃状态 - err := service.SetActiveProfile(database.MustGetDB(), uuid, userID.(int64)) + err := service.SetActiveProfile(database.MustGetDB(), uuid, userID) if err != nil { - loggerInstance.Error("设置活跃档案失败", + logger.MustGetLogger().Error("设置活跃档案失败", zap.String("uuid", uuid), - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Error(err), ) - - statusCode := http.StatusInternalServerError - if err.Error() == "档案不存在" { - statusCode = http.StatusNotFound - } else if err.Error() == "无权操作此档案" { - statusCode = http.StatusForbidden - } - - c.JSON(statusCode, model.NewErrorResponse( - model.CodeServerError, - err.Error(), - nil, - )) + RespondWithError(c, err) return } - // 返回成功响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "message": "设置成功", - })) + RespondSuccess(c, gin.H{"message": "设置成功"}) } diff --git a/internal/handler/texture_handler.go b/internal/handler/texture_handler.go index c6c674a..e352c30 100644 --- a/internal/handler/texture_handler.go +++ b/internal/handler/texture_handler.go @@ -8,7 +8,6 @@ import ( "carrotskin/pkg/database" "carrotskin/pkg/logger" "carrotskin/pkg/storage" - "net/http" "strconv" "github.com/gin-gonic/gin" @@ -27,59 +26,44 @@ import ( // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/texture/upload-url [post] func GenerateTextureUploadURL(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } var req types.GenerateTextureUploadURLRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // 调用UploadService生成预签名URL storageClient := storage.MustGetClient() cfg := *config.MustGetRustFSConfig() result, err := service.GenerateTextureUploadURL( c.Request.Context(), storageClient, cfg, - userID.(int64), + userID, req.FileName, string(req.TextureType), ) if err != nil { logger.MustGetLogger().Error("生成材质上传URL失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.String("file_name", req.FileName), zap.String("texture_type", string(req.TextureType)), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - // 返回响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.GenerateTextureUploadURLResponse{ + RespondSuccess(c, &types.GenerateTextureUploadURLResponse{ PostURL: result.PostURL, FormData: result.FormData, TextureURL: result.FileURL, - ExpiresIn: 900, // 15分钟 = 900秒 - })) + ExpiresIn: 900, + }) } // CreateTexture 创建材质记录 @@ -94,40 +78,25 @@ func GenerateTextureUploadURL(c *gin.Context) { // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/texture [post] func CreateTexture(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } var req types.CreateTextureRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // TODO: 从配置或数据库读取限制 - maxTextures := 100 - if err := service.CheckTextureUploadLimit(database.MustGetDB(), userID.(int64), maxTextures); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + maxTextures := service.GetMaxTexturesPerUser() + if err := service.CheckTextureUploadLimit(database.MustGetDB(), userID, maxTextures); err != nil { + RespondBadRequest(c, err.Error(), nil) return } - // 创建材质 texture, err := service.CreateTexture(database.MustGetDB(), - userID.(int64), + userID, req.Name, req.Description, string(req.Type), @@ -139,36 +108,15 @@ func CreateTexture(c *gin.Context) { ) if err != nil { logger.MustGetLogger().Error("创建材质失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.String("name", req.Name), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - // 返回响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - })) + RespondSuccess(c, TextureToTextureInfo(texture)) } // GetTexture 获取材质详情 @@ -182,44 +130,19 @@ func CreateTexture(c *gin.Context) { // @Failure 404 {object} model.ErrorResponse "材质不存在" // @Router /api/v1/texture/{id} [get] func GetTexture(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseInt(idStr, 10, 64) + id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "无效的材质ID", - err, - )) + RespondBadRequest(c, "无效的材质ID", err) return } texture, err := service.GetTextureByID(database.MustGetDB(), id) if err != nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - err.Error(), - nil, - )) + RespondNotFound(c, err.Error()) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - })) + RespondSuccess(c, TextureToTextureInfo(texture)) } // SearchTextures 搜索材质 @@ -253,41 +176,12 @@ func SearchTextures(c *gin.Context) { textures, total, err := service.SearchTextures(database.MustGetDB(), keyword, textureType, publicOnly, page, pageSize) if err != nil { - logger.MustGetLogger().Error("搜索材质失败", - zap.String("keyword", keyword), - zap.Error(err), - ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "搜索材质失败", - err, - )) + logger.MustGetLogger().Error("搜索材质失败", zap.String("keyword", keyword), zap.Error(err)) + RespondServerError(c, "搜索材质失败", err) return } - // 转换为TextureInfo - textureInfos := make([]*types.TextureInfo, len(textures)) - for i, texture := range textures { - textureInfos[i] = &types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - } - } - - c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) + c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize)) } // UpdateTexture 更新材质 @@ -303,69 +197,35 @@ func SearchTextures(c *gin.Context) { // @Failure 403 {object} model.ErrorResponse "无权操作" // @Router /api/v1/texture/{id} [put] func UpdateTexture(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - idStr := c.Param("id") - textureID, err := strconv.ParseInt(idStr, 10, 64) + textureID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "无效的材质ID", - err, - )) + RespondBadRequest(c, "无效的材质ID", err) return } var req types.UpdateTextureRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - texture, err := service.UpdateTexture(database.MustGetDB(), textureID, userID.(int64), req.Name, req.Description, req.IsPublic) + texture, err := service.UpdateTexture(database.MustGetDB(), textureID, userID, req.Name, req.Description, req.IsPublic) if err != nil { logger.MustGetLogger().Error("更新材质失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Int64("texture_id", textureID), zap.Error(err), ) - c.JSON(http.StatusForbidden, model.NewErrorResponse( - model.CodeForbidden, - err.Error(), - nil, - )) + RespondForbidden(c, err.Error()) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - })) + RespondSuccess(c, TextureToTextureInfo(texture)) } // DeleteTexture 删除材质 @@ -380,42 +240,28 @@ func UpdateTexture(c *gin.Context) { // @Failure 403 {object} model.ErrorResponse "无权操作" // @Router /api/v1/texture/{id} [delete] func DeleteTexture(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - idStr := c.Param("id") - textureID, err := strconv.ParseInt(idStr, 10, 64) + textureID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "无效的材质ID", - err, - )) + RespondBadRequest(c, "无效的材质ID", err) return } - if err := service.DeleteTexture(database.MustGetDB(), textureID, userID.(int64)); err != nil { + if err := service.DeleteTexture(database.MustGetDB(), textureID, userID); err != nil { logger.MustGetLogger().Error("删除材质失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Int64("texture_id", textureID), zap.Error(err), ) - c.JSON(http.StatusForbidden, model.NewErrorResponse( - model.CodeForbidden, - err.Error(), - nil, - )) + RespondForbidden(c, err.Error()) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(nil)) + RespondSuccess(c, nil) } // ToggleFavorite 切换收藏状态 @@ -429,45 +275,29 @@ func DeleteTexture(c *gin.Context) { // @Success 200 {object} model.Response "切换成功" // @Router /api/v1/texture/{id}/favorite [post] func ToggleFavorite(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - idStr := c.Param("id") - textureID, err := strconv.ParseInt(idStr, 10, 64) + textureID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "无效的材质ID", - err, - )) + RespondBadRequest(c, "无效的材质ID", err) return } - isFavorited, err := service.ToggleTextureFavorite(database.MustGetDB(), userID.(int64), textureID) + isFavorited, err := service.ToggleTextureFavorite(database.MustGetDB(), userID, textureID) if err != nil { logger.MustGetLogger().Error("切换收藏状态失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.Int64("texture_id", textureID), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]bool{ - "is_favorited": isFavorited, - })) + RespondSuccess(c, map[string]bool{"is_favorited": isFavorited}) } // GetUserTextures 获取用户上传的材质列表 @@ -482,56 +312,22 @@ func ToggleFavorite(c *gin.Context) { // @Success 200 {object} model.PaginationResponse "获取成功" // @Router /api/v1/texture/my [get] func GetUserTextures(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - textures, total, err := service.GetUserTextures(database.MustGetDB(), userID.(int64), page, pageSize) + textures, total, err := service.GetUserTextures(database.MustGetDB(), userID, page, pageSize) if err != nil { - logger.MustGetLogger().Error("获取用户材质列表失败", - zap.Int64("user_id", userID.(int64)), - zap.Error(err), - ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "获取材质列表失败", - err, - )) + logger.MustGetLogger().Error("获取用户材质列表失败", zap.Int64("user_id", userID), zap.Error(err)) + RespondServerError(c, "获取材质列表失败", err) return } - // 转换为TextureInfo - textureInfos := make([]*types.TextureInfo, len(textures)) - for i, texture := range textures { - textureInfos[i] = &types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - } - } - - c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) + c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize)) } // GetUserFavorites 获取用户收藏的材质列表 @@ -546,54 +342,20 @@ func GetUserTextures(c *gin.Context) { // @Success 200 {object} model.PaginationResponse "获取成功" // @Router /api/v1/texture/favorites [get] func GetUserFavorites(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - textures, total, err := service.GetUserTextureFavorites(database.MustGetDB(), userID.(int64), page, pageSize) + textures, total, err := service.GetUserTextureFavorites(database.MustGetDB(), userID, page, pageSize) if err != nil { - logger.MustGetLogger().Error("获取用户收藏列表失败", - zap.Int64("user_id", userID.(int64)), - zap.Error(err), - ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "获取收藏列表失败", - err, - )) + logger.MustGetLogger().Error("获取用户收藏列表失败", zap.Int64("user_id", userID), zap.Error(err)) + RespondServerError(c, "获取收藏列表失败", err) return } - // 转换为TextureInfo - textureInfos := make([]*types.TextureInfo, len(textures)) - for i, texture := range textures { - textureInfos[i] = &types.TextureInfo{ - ID: texture.ID, - UploaderID: texture.UploaderID, - Name: texture.Name, - Description: texture.Description, - Type: types.TextureType(texture.Type), - URL: texture.URL, - Hash: texture.Hash, - Size: texture.Size, - IsPublic: texture.IsPublic, - DownloadCount: texture.DownloadCount, - FavoriteCount: texture.FavoriteCount, - IsSlim: texture.IsSlim, - Status: texture.Status, - CreatedAt: texture.CreatedAt, - UpdatedAt: texture.UpdatedAt, - } - } - - c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) + c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize)) } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 86a6068..0feb627 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -1,7 +1,6 @@ package handler import ( - "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/internal/types" "carrotskin/pkg/config" @@ -9,7 +8,6 @@ import ( "carrotskin/pkg/logger" "carrotskin/pkg/redis" "carrotskin/pkg/storage" - "net/http" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -26,46 +24,22 @@ import ( // @Failure 401 {object} model.ErrorResponse "未授权" // @Router /api/v1/user/profile [get] func GetUserProfile(c *gin.Context) { - loggerInstance := logger.MustGetLogger() - // 从上下文获取用户ID (由JWT中间件设置) - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } - // 获取用户信息 - user, err := service.GetUserByID(userID.(int64)) + user, err := service.GetUserByID(userID) if err != nil || user == nil { - loggerInstance.Error("获取用户信息失败", - zap.Int64("user_id", userID.(int64)), + logger.MustGetLogger().Error("获取用户信息失败", + zap.Int64("user_id", userID), zap.Error(err), ) - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "用户不存在", - err, - )) + RespondNotFound(c, "用户不存在") return } - // 返回用户信息 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.UserInfo{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Points: user.Points, - Role: user.Role, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - })) + RespondSuccess(c, UserToUserInfo(user)) } // UpdateUserProfile 更新用户信息 @@ -84,113 +58,62 @@ func GetUserProfile(c *gin.Context) { // @Router /api/v1/user/profile [put] func UpdateUserProfile(c *gin.Context) { loggerInstance := logger.MustGetLogger() - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } var req types.UpdateUserRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // 获取用户 - user, err := service.GetUserByID(userID.(int64)) + user, err := service.GetUserByID(userID) if err != nil || user == nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "用户不存在", - err, - )) + RespondNotFound(c, "用户不存在") return } // 处理密码修改 if req.NewPassword != "" { - // 如果提供了新密码,必须同时提供旧密码 if req.OldPassword == "" { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "修改密码需要提供原密码", - nil, - )) + RespondBadRequest(c, "修改密码需要提供原密码", nil) return } - // 调用修改密码服务 - if err := service.ChangeUserPassword(userID.(int64), req.OldPassword, req.NewPassword); err != nil { - loggerInstance.Error("修改密码失败", - zap.Int64("user_id", userID.(int64)), - zap.Error(err), - ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + if err := service.ChangeUserPassword(userID, req.OldPassword, req.NewPassword); err != nil { + loggerInstance.Error("修改密码失败", zap.Int64("user_id", userID), zap.Error(err)) + RespondBadRequest(c, err.Error(), nil) return } - loggerInstance.Info("用户修改密码成功", - zap.Int64("user_id", userID.(int64)), - ) + loggerInstance.Info("用户修改密码成功", zap.Int64("user_id", userID)) } // 更新头像 if req.Avatar != "" { + // 验证头像 URL 是否来自允许的域名 + if err := service.ValidateAvatarURL(req.Avatar); err != nil { + RespondBadRequest(c, err.Error(), nil) + return + } user.Avatar = req.Avatar - } - - // 保存更新(仅当有头像修改时) - if req.Avatar != "" { if err := service.UpdateUserInfo(user); err != nil { - loggerInstance.Error("更新用户信息失败", - zap.Int64("user_id", user.ID), - zap.Error(err), - ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "更新失败", - err, - )) + loggerInstance.Error("更新用户信息失败", zap.Int64("user_id", user.ID), zap.Error(err)) + RespondServerError(c, "更新失败", err) return } } // 重新获取更新后的用户信息 - updatedUser, err := service.GetUserByID(userID.(int64)) + updatedUser, err := service.GetUserByID(userID) if err != nil || updatedUser == nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "用户不存在", - err, - )) + RespondNotFound(c, "用户不存在") return } - // 返回更新后的用户信息 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.UserInfo{ - ID: updatedUser.ID, - Username: updatedUser.Username, - Email: updatedUser.Email, - Avatar: updatedUser.Avatar, - Points: updatedUser.Points, - Role: updatedUser.Role, - Status: updatedUser.Status, - LastLoginAt: updatedUser.LastLoginAt, - CreatedAt: updatedUser.CreatedAt, - UpdatedAt: updatedUser.UpdatedAt, - })) + RespondSuccess(c, UserToUserInfo(updatedUser)) } // GenerateAvatarUploadURL 生成头像上传URL @@ -205,52 +128,36 @@ func UpdateUserProfile(c *gin.Context) { // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/user/avatar/upload-url [post] func GenerateAvatarUploadURL(c *gin.Context) { - loggerInstance := logger.MustGetLogger() - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } var req types.GenerateAvatarUploadURLRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // 调用UploadService生成预签名URL storageClient := storage.MustGetClient() cfg := *config.MustGetRustFSConfig() - result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, cfg, userID.(int64), req.FileName) + result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, cfg, userID, req.FileName) if err != nil { - loggerInstance.Error("生成头像上传URL失败", - zap.Int64("user_id", userID.(int64)), + logger.MustGetLogger().Error("生成头像上传URL失败", + zap.Int64("user_id", userID), zap.String("file_name", req.FileName), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - // 返回响应 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.GenerateAvatarUploadURLResponse{ + RespondSuccess(c, &types.GenerateAvatarUploadURLResponse{ PostURL: result.PostURL, FormData: result.FormData, AvatarURL: result.FileURL, - ExpiresIn: 900, // 15分钟 = 900秒 - })) + ExpiresIn: 900, + }) } // UpdateAvatar 更新头像URL @@ -265,65 +172,39 @@ func GenerateAvatarUploadURL(c *gin.Context) { // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/user/avatar [put] func UpdateAvatar(c *gin.Context) { - loggerInstance := logger.MustGetLogger() - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } avatarURL := c.Query("avatar_url") if avatarURL == "" { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "头像URL不能为空", - nil, - )) + RespondBadRequest(c, "头像URL不能为空", nil) return } - // 更新头像 - if err := service.UpdateUserAvatar(userID.(int64), avatarURL); err != nil { - loggerInstance.Error("更新头像失败", - zap.Int64("user_id", userID.(int64)), + if err := service.ValidateAvatarURL(avatarURL); err != nil { + RespondBadRequest(c, err.Error(), nil) + return + } + + if err := service.UpdateUserAvatar(userID, avatarURL); err != nil { + logger.MustGetLogger().Error("更新头像失败", + zap.Int64("user_id", userID), zap.String("avatar_url", avatarURL), zap.Error(err), ) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "更新头像失败", - err, - )) + RespondServerError(c, "更新头像失败", err) return } - // 获取更新后的用户信息 - user, err := service.GetUserByID(userID.(int64)) + user, err := service.GetUserByID(userID) if err != nil || user == nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "用户不存在", - err, - )) + RespondNotFound(c, "用户不存在") return } - // 返回更新后的用户信息 - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.UserInfo{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Points: user.Points, - Role: user.Role, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - })) + RespondSuccess(c, UserToUserInfo(user)) } // ChangeEmail 更换邮箱 @@ -340,79 +221,41 @@ func UpdateAvatar(c *gin.Context) { // @Router /api/v1/user/change-email [post] func ChangeEmail(c *gin.Context) { loggerInstance := logger.MustGetLogger() - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - model.MsgUnauthorized, - nil, - )) + userID, ok := GetUserIDFromContext(c) + if !ok { return } var req types.ChangeEmailRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - "请求参数错误", - err, - )) + RespondBadRequest(c, "请求参数错误", err) return } - // 验证验证码 redisClient := redis.MustGetClient() if err := service.VerifyCode(c.Request.Context(), redisClient, req.NewEmail, req.VerificationCode, service.VerificationTypeChangeEmail); err != nil { - loggerInstance.Warn("验证码验证失败", - zap.String("new_email", req.NewEmail), - zap.Error(err), - ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + loggerInstance.Warn("验证码验证失败", zap.String("new_email", req.NewEmail), zap.Error(err)) + RespondBadRequest(c, err.Error(), nil) return } - // 更换邮箱 - if err := service.ChangeUserEmail(userID.(int64), req.NewEmail); err != nil { + if err := service.ChangeUserEmail(userID, req.NewEmail); err != nil { loggerInstance.Error("更换邮箱失败", - zap.Int64("user_id", userID.(int64)), + zap.Int64("user_id", userID), zap.String("new_email", req.NewEmail), zap.Error(err), ) - c.JSON(http.StatusBadRequest, model.NewErrorResponse( - model.CodeBadRequest, - err.Error(), - nil, - )) + RespondBadRequest(c, err.Error(), nil) return } - // 获取更新后的用户信息 - user, err := service.GetUserByID(userID.(int64)) + user, err := service.GetUserByID(userID) if err != nil || user == nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "用户不存在", - err, - )) + RespondNotFound(c, "用户不存在") return } - c.JSON(http.StatusOK, model.NewSuccessResponse(&types.UserInfo{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Avatar: user.Avatar, - Points: user.Points, - Role: user.Role, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - })) + RespondSuccess(c, UserToUserInfo(user)) } // ResetYggdrasilPassword 重置Yggdrasil密码 @@ -428,35 +271,19 @@ func ChangeEmail(c *gin.Context) { // @Router /api/v1/user/yggdrasil-password/reset [post] func ResetYggdrasilPassword(c *gin.Context) { loggerInstance := logger.MustGetLogger() + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + db := database.MustGetDB() - - // 从上下文获取用户ID - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, model.NewErrorResponse( - model.CodeUnauthorized, - "未授权", - nil, - )) - return - } - - userId := userID.(int64) - - // 重置Yggdrasil密码 - newPassword, err := service.ResetYggdrasilPassword(db, userId) + newPassword, err := service.ResetYggdrasilPassword(db, userID) if err != nil { - loggerInstance.Error("[ERROR] 重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userId)) - c.JSON(http.StatusInternalServerError, model.NewErrorResponse( - model.CodeServerError, - "重置Yggdrasil密码失败", - nil, - )) + loggerInstance.Error("重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userID)) + RespondServerError(c, "重置Yggdrasil密码失败", nil) return } - loggerInstance.Info("[INFO] Yggdrasil密码重置成功", zap.Int64("userId", userId)) - c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "password": newPassword, - })) + loggerInstance.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID)) + RespondSuccess(c, gin.H{"password": newPassword}) } diff --git a/internal/handler/yggdrasil_handler.go b/internal/handler/yggdrasil_handler.go index de30889..acbf7b2 100644 --- a/internal/handler/yggdrasil_handler.go +++ b/internal/handler/yggdrasil_handler.go @@ -405,12 +405,8 @@ func SignOut(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - password, err := service.GetPasswordByUserId(db, user.ID) - if err != nil { - loggerInstance.Error("[ERROR] 邮箱查找失败", zap.Any("UserId:", user.ID), zap.Error(err)) - } // 验证密码 - if password != request.Password { + if err := service.VerifyPassword(db, request.Password, user.ID); err != nil { loggerInstance.Warn("[WARN] 登出失败: 密码错误", zap.Any("用户ID:", user.ID)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword}) return diff --git a/internal/model/audit_log.go b/internal/model/audit_log.go index d12dc46..08bcd1e 100644 --- a/internal/model/audit_log.go +++ b/internal/model/audit_log.go @@ -7,18 +7,18 @@ import ( // AuditLog 审计日志模型 type AuditLog struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserID *int64 `gorm:"column:user_id;type:bigint;index" json:"user_id,omitempty"` - Action string `gorm:"column:action;type:varchar(100);not null;index" json:"action"` - ResourceType string `gorm:"column:resource_type;type:varchar(50);not null;index:idx_audit_logs_resource" json:"resource_type"` - ResourceID string `gorm:"column:resource_id;type:varchar(50);index:idx_audit_logs_resource" json:"resource_id,omitempty"` + UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"` + Action string `gorm:"column:action;type:varchar(100);not null;index:idx_audit_logs_action" json:"action"` + ResourceType string `gorm:"column:resource_type;type:varchar(50);not null;index:idx_audit_logs_resource,priority:1" json:"resource_type"` + ResourceID string `gorm:"column:resource_id;type:varchar(50);index:idx_audit_logs_resource,priority:2" json:"resource_id,omitempty"` OldValues string `gorm:"column:old_values;type:jsonb" json:"old_values,omitempty"` // JSONB 格式 NewValues string `gorm:"column:new_values;type:jsonb" json:"new_values,omitempty"` // JSONB 格式 - IPAddress string `gorm:"column:ip_address;type:inet;not null" json:"ip_address"` + IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_audit_logs_ip" json:"ip_address"` UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_audit_logs_created_at,sort:desc" json:"created_at"` - + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_audit_logs_user_created,priority:2,sort:desc;index:idx_audit_logs_created_at,sort:desc" json:"created_at"` + // 关联 - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:SET NULL" json:"user,omitempty"` } // TableName 指定表名 @@ -29,13 +29,13 @@ func (AuditLog) TableName() string { // CasbinRule Casbin 权限规则模型 type CasbinRule struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - PType string `gorm:"column:ptype;type:varchar(100);not null;index;uniqueIndex:uk_casbin_rule" json:"ptype"` - V0 string `gorm:"column:v0;type:varchar(100);not null;default:'';index;uniqueIndex:uk_casbin_rule" json:"v0"` - V1 string `gorm:"column:v1;type:varchar(100);not null;default:'';index;uniqueIndex:uk_casbin_rule" json:"v1"` - V2 string `gorm:"column:v2;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v2"` - V3 string `gorm:"column:v3;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v3"` - V4 string `gorm:"column:v4;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v4"` - V5 string `gorm:"column:v5;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v5"` + PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"` + V0 string `gorm:"column:v0;type:varchar(100);not null;default:'';index:idx_casbin_v0;uniqueIndex:uk_casbin_rule,priority:2" json:"v0"` + V1 string `gorm:"column:v1;type:varchar(100);not null;default:'';index:idx_casbin_v1;uniqueIndex:uk_casbin_rule,priority:3" json:"v1"` + V2 string `gorm:"column:v2;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:4" json:"v2"` + V3 string `gorm:"column:v3;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:5" json:"v3"` + V4 string `gorm:"column:v4;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:6" json:"v4"` + V5 string `gorm:"column:v5;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:7" json:"v5"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"` } diff --git a/internal/model/profile.go b/internal/model/profile.go index b076831..8645076 100644 --- a/internal/model/profile.go +++ b/internal/model/profile.go @@ -7,20 +7,20 @@ import ( // Profile Minecraft 档案模型 type Profile struct { UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` - UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` - Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex" json:"name"` // Minecraft 角色名 - SkinID *int64 `gorm:"column:skin_id;type:bigint" json:"skin_id,omitempty"` - CapeID *int64 `gorm:"column:cape_id;type:bigint" json:"cape_id,omitempty"` + UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1;index:idx_profiles_user_active,priority:1" json:"user_id"` + Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex:idx_profiles_name" json:"name"` // Minecraft 角色名 + SkinID *int64 `gorm:"column:skin_id;type:bigint;index:idx_profiles_skin_id" json:"skin_id,omitempty"` + CapeID *int64 `gorm:"column:cape_id;type:bigint;index:idx_profiles_cape_id" json:"cape_id,omitempty"` RSAPrivateKey string `gorm:"column:rsa_private_key;type:text;not null" json:"-"` // RSA 私钥不返回给前端 - IsActive bool `gorm:"column:is_active;not null;default:true;index" json:"is_active"` - LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp" json:"last_used_at,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"` + IsActive bool `gorm:"column:is_active;not null;default:true;index:idx_profiles_user_active,priority:2" json:"is_active"` + LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp;index:idx_profiles_last_used,sort:desc" json:"last_used_at,omitempty"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_profiles_user_created,priority:2,sort:desc" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 关联 - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` - Skin *Texture `gorm:"foreignKey:SkinID" json:"skin,omitempty"` - Cape *Texture `gorm:"foreignKey:CapeID" json:"cape,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"` + Skin *Texture `gorm:"foreignKey:SkinID;constraint:OnDelete:SET NULL" json:"skin,omitempty"` + Cape *Texture `gorm:"foreignKey:CapeID;constraint:OnDelete:SET NULL" json:"cape,omitempty"` } // TableName 指定表名 diff --git a/internal/model/texture.go b/internal/model/texture.go index 8276f80..24b2d4a 100644 --- a/internal/model/texture.go +++ b/internal/model/texture.go @@ -15,23 +15,23 @@ const ( // Texture 材质模型 type Texture struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UploaderID int64 `gorm:"column:uploader_id;not null;index" json:"uploader_id"` + UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"` Name string `gorm:"column:name;type:varchar(100);not null;default:''" json:"name"` Description string `gorm:"column:description;type:text" json:"description,omitempty"` - Type TextureType `gorm:"column:type;type:varchar(50);not null" json:"type"` // SKIN, CAPE + Type TextureType `gorm:"column:type;type:varchar(50);not null;index:idx_textures_public_type_status,priority:2" json:"type"` // SKIN, CAPE URL string `gorm:"column:url;type:varchar(255);not null" json:"url"` - Hash string `gorm:"column:hash;type:varchar(64);not null;uniqueIndex" json:"hash"` // SHA-256 + Hash string `gorm:"column:hash;type:varchar(64);not null;uniqueIndex:idx_textures_hash" json:"hash"` // SHA-256 Size int `gorm:"column:size;type:integer;not null;default:0" json:"size"` - IsPublic bool `gorm:"column:is_public;not null;default:false;index:idx_textures_public_type_status" json:"is_public"` + IsPublic bool `gorm:"column:is_public;not null;default:false;index:idx_textures_public_type_status,priority:1" json:"is_public"` DownloadCount int `gorm:"column:download_count;type:integer;not null;default:0;index:idx_textures_download_count,sort:desc" json:"download_count"` FavoriteCount int `gorm:"column:favorite_count;type:integer;not null;default:0;index:idx_textures_favorite_count,sort:desc" json:"favorite_count"` IsSlim bool `gorm:"column:is_slim;not null;default:false" json:"is_slim"` // Alex(细) or Steve(粗) - Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_textures_public_type_status" json:"status"` // 1:正常, 0:审核中, -1:已删除 - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"` + Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_textures_public_type_status,priority:3;index:idx_textures_uploader_status,priority:2" json:"status"` // 1:正常, 0:审核中, -1:已删除 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_textures_uploader_created,priority:2,sort:desc;index:idx_textures_created_at,sort:desc" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` - + // 关联 - Uploader *User `gorm:"foreignKey:UploaderID" json:"uploader,omitempty"` + Uploader *User `gorm:"foreignKey:UploaderID;constraint:OnDelete:CASCADE" json:"uploader,omitempty"` } // TableName 指定表名 @@ -42,13 +42,13 @@ func (Texture) TableName() string { // UserTextureFavorite 用户材质收藏 type UserTextureFavorite struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserID int64 `gorm:"column:user_id;not null;index;uniqueIndex:uk_user_texture" json:"user_id"` - TextureID int64 `gorm:"column:texture_id;not null;index;uniqueIndex:uk_user_texture" json:"texture_id"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index" json:"created_at"` - + UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"` + TextureID int64 `gorm:"column:texture_id;not null;uniqueIndex:uk_user_texture,priority:2;index:idx_favorites_texture_id" json:"texture_id"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_favorites_user_created,priority:2,sort:desc;index:idx_favorites_created_at,sort:desc" json:"created_at"` + // 关联 - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` - Texture *Texture `gorm:"foreignKey:TextureID" json:"texture,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"` + Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"` } // TableName 指定表名 @@ -59,15 +59,15 @@ func (UserTextureFavorite) TableName() string { // TextureDownloadLog 材质下载记录 type TextureDownloadLog struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - TextureID int64 `gorm:"column:texture_id;not null;index" json:"texture_id"` - UserID *int64 `gorm:"column:user_id;type:bigint;index" json:"user_id,omitempty"` - IPAddress string `gorm:"column:ip_address;type:inet;not null;index" json:"ip_address"` + TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"` + UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_download_logs_user_id" json:"user_id,omitempty"` + IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_download_logs_ip" json:"ip_address"` UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_download_logs_created_at,sort:desc" json:"created_at"` - + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_download_logs_texture_created,priority:2,sort:desc;index:idx_download_logs_created_at,sort:desc" json:"created_at"` + // 关联 - Texture *Texture `gorm:"foreignKey:TextureID" json:"texture,omitempty"` - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:SET NULL" json:"user,omitempty"` } // TableName 指定表名 diff --git a/internal/model/token.go b/internal/model/token.go index 0e2b419..926d007 100644 --- a/internal/model/token.go +++ b/internal/model/token.go @@ -2,13 +2,19 @@ package model import "time" +// Token Yggdrasil 认证令牌模型 type Token struct { - AccessToken string `json:"_id"` - UserID int64 `json:"user_id"` - ClientToken string `json:"client_token"` - ProfileId string `json:"profile_id"` - Usable bool `json:"usable"` - IssueDate time.Time `json:"issue_date"` + AccessToken string `gorm:"column:access_token;type:varchar(64);primaryKey" json:"access_token"` + UserID int64 `gorm:"column:user_id;not null;index:idx_tokens_user_id" json:"user_id"` + ClientToken string `gorm:"column:client_token;type:varchar(64);not null;index:idx_tokens_client_token" json:"client_token"` + ProfileId string `gorm:"column:profile_id;type:varchar(36);not null;index:idx_tokens_profile_id" json:"profile_id"` + Usable bool `gorm:"column:usable;not null;default:true;index:idx_tokens_usable" json:"usable"` + IssueDate time.Time `gorm:"column:issue_date;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_tokens_issue_date,sort:desc" json:"issue_date"` + + // 关联 + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"` + Profile *Profile `gorm:"foreignKey:ProfileId;references:UUID;constraint:OnDelete:CASCADE" json:"profile,omitempty"` } -func (Token) TableName() string { return "token" } +// TableName 指定表名 +func (Token) TableName() string { return "tokens" } diff --git a/internal/model/user.go b/internal/model/user.go index 679e3b5..39a6f44 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -9,16 +9,16 @@ import ( // User 用户模型 type User struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"` + Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"` Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端 - Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"` + Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex:idx_user_email_status,priority:1" json:"email"` Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"` - Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"` - Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"` - Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除 - Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据,存储为PostgreSQL的JSONB类型 - LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"` + Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"` + Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"` + Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除 + Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据,存储为PostgreSQL的JSONB类型 + LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` } @@ -30,20 +30,20 @@ func (User) TableName() string { // UserPointLog 用户积分变更记录 type UserPointLog struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` - ChangeType string `gorm:"column:change_type;type:varchar(50);not null" json:"change_type"` // EARN, SPEND, ADMIN_ADJUST + UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"` + ChangeType string `gorm:"column:change_type;type:varchar(50);not null;index:idx_point_logs_change_type" json:"change_type"` // EARN, SPEND, ADMIN_ADJUST Amount int `gorm:"column:amount;type:integer;not null" json:"amount"` BalanceBefore int `gorm:"column:balance_before;type:integer;not null" json:"balance_before"` BalanceAfter int `gorm:"column:balance_after;type:integer;not null" json:"balance_after"` Reason string `gorm:"column:reason;type:varchar(255);not null" json:"reason"` ReferenceType string `gorm:"column:reference_type;type:varchar(50)" json:"reference_type,omitempty"` ReferenceID *int64 `gorm:"column:reference_id;type:bigint" json:"reference_id,omitempty"` - OperatorID *int64 `gorm:"column:operator_id;type:bigint" json:"operator_id,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_point_logs_created_at,sort:desc" json:"created_at"` + OperatorID *int64 `gorm:"column:operator_id;type:bigint;index" json:"operator_id,omitempty"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_point_logs_user_created,priority:2,sort:desc;index:idx_point_logs_created_at,sort:desc" json:"created_at"` // 关联 - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` - Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"` + Operator *User `gorm:"foreignKey:OperatorID;constraint:OnDelete:SET NULL" json:"operator,omitempty"` } // TableName 指定表名 @@ -54,16 +54,16 @@ func (UserPointLog) TableName() string { // UserLoginLog 用户登录日志 type UserLoginLog struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` - IPAddress string `gorm:"column:ip_address;type:inet;not null;index" json:"ip_address"` + UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"` + IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_login_logs_ip" json:"ip_address"` UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"` LoginMethod string `gorm:"column:login_method;type:varchar(50);not null;default:'PASSWORD'" json:"login_method"` - IsSuccess bool `gorm:"column:is_success;not null;index" json:"is_success"` + IsSuccess bool `gorm:"column:is_success;not null;index:idx_login_logs_success" json:"is_success"` FailureReason string `gorm:"column:failure_reason;type:varchar(255)" json:"failure_reason,omitempty"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_login_logs_created_at,sort:desc" json:"created_at"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_login_logs_user_created,priority:2,sort:desc;index:idx_login_logs_created_at,sort:desc" json:"created_at"` // 关联 - User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"` } // TableName 指定表名 diff --git a/internal/model/yggdrasil.go b/internal/model/yggdrasil.go index b8433a4..9e7efaa 100644 --- a/internal/model/yggdrasil.go +++ b/internal/model/yggdrasil.go @@ -1,10 +1,12 @@ package model import ( + "crypto/rand" "fmt" + "math/big" + + "golang.org/x/crypto/bcrypt" "gorm.io/gorm" - "math/rand" - "time" ) // 定义随机字符集 @@ -13,36 +15,47 @@ const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234 // Yggdrasil ygg密码与用户id绑定 type Yggdrasil struct { ID int64 `gorm:"column:id;primaryKey;not null" json:"id"` - Password string `gorm:"column:password;not null" json:"password"` + Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 加密后的密码,不返回给前端 // 关联 - Yggdrasil的ID引用User的ID,但不自动创建外键约束(避免循环依赖) User *User `gorm:"foreignKey:ID;references:ID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE" json:"user,omitempty"` } -func (Yggdrasil) TableName() string { return "Yggdrasil" } +func (Yggdrasil) TableName() string { return "yggdrasil" } -// AfterCreate User创建后自动同步生成GeneratePassword记录 +// AfterCreate User创建后自动同步生成Yggdrasil密码记录 func (u *User) AfterCreate(tx *gorm.DB) error { - randomPwd := GenerateRandomPassword(16) + // 生成随机明文密码 + plainPassword := GenerateRandomPassword(16) - // 创建GeneratePassword记录 - gp := Yggdrasil{ - ID: u.ID, // 关联User的ID - Password: randomPwd, // 16位随机密码 + // 使用 bcrypt 加密密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("密码加密失败: %w", err) } - if err := tx.Create(&gp).Error; err != nil { - // 若同步失败,可记录日志或回滚事务(根据业务需求处理) - return fmt.Errorf("同步生成密码失败: %w", err) + // 创建Yggdrasil记录(存储加密后的密码) + ygg := Yggdrasil{ + ID: u.ID, + Password: string(hashedPassword), + } + + if err := tx.Create(&ygg).Error; err != nil { + return fmt.Errorf("同步生成Yggdrasil密码失败: %w", err) } return nil } -// GenerateRandomPassword 生成指定长度的随机字符串 +// GenerateRandomPassword 生成指定长度的安全随机字符串 func GenerateRandomPassword(length int) string { - rand.Seed(time.Now().UnixNano()) // 初始化随机数种子 b := make([]byte, length) for i := range b { - b[i] = passwordChars[rand.Intn(len(passwordChars))] + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(passwordChars)))) + if err != nil { + // 如果安全随机数生成失败,使用固定值(极端情况下的降级处理) + b[i] = passwordChars[0] + continue + } + b[i] = passwordChars[num.Int64()] } return string(b) } diff --git a/internal/repository/helpers.go b/internal/repository/helpers.go new file mode 100644 index 0000000..1e6870f --- /dev/null +++ b/internal/repository/helpers.go @@ -0,0 +1,82 @@ +package repository + +import ( + "carrotskin/pkg/database" + "errors" + + "gorm.io/gorm" +) + +// getDB 获取数据库连接(内部使用) +func getDB() *gorm.DB { + return database.MustGetDB() +} + +// IsNotFound 检查是否为记录未找到错误 +func IsNotFound(err error) bool { + return errors.Is(err, gorm.ErrRecordNotFound) +} + +// HandleNotFound 处理记录未找到的情况,未找到时返回 nil, nil +func HandleNotFound[T any](result *T, err error) (*T, error) { + if err != nil { + if IsNotFound(err) { + return nil, nil + } + return nil, err + } + return result, nil +} + +// Paginate 创建分页查询 +func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + offset := (page - 1) * pageSize + return db.Offset(offset).Limit(pageSize) + } +} + +// PaginatedQuery 执行分页查询,返回列表和总数 +func PaginatedQuery[T any]( + baseQuery *gorm.DB, + page, pageSize int, + orderBy string, + preloads ...string, +) ([]T, int64, error) { + var items []T + var total int64 + + // 获取总数 + if err := baseQuery.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + query := baseQuery.Scopes(Paginate(page, pageSize)) + + // 添加排序 + if orderBy != "" { + query = query.Order(orderBy) + } + + // 添加预加载 + for _, preload := range preloads { + query = query.Preload(preload) + } + + if err := query.Find(&items).Error; err != nil { + return nil, 0, err + } + + return items, total, nil +} + diff --git a/internal/repository/profile_repository.go b/internal/repository/profile_repository.go index bf34406..ad008d0 100644 --- a/internal/repository/profile_repository.go +++ b/internal/repository/profile_repository.go @@ -2,7 +2,6 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" "context" "errors" "fmt" @@ -12,15 +11,13 @@ import ( // CreateProfile 创建档案 func CreateProfile(profile *model.Profile) error { - db := database.MustGetDB() - return db.Create(profile).Error + return getDB().Create(profile).Error } // FindProfileByUUID 根据UUID查找档案 func FindProfileByUUID(uuid string) (*model.Profile, error) { - db := database.MustGetDB() var profile model.Profile - err := db.Where("uuid = ?", uuid). + err := getDB().Where("uuid = ?", uuid). Preload("Skin"). Preload("Cape"). First(&profile).Error @@ -32,9 +29,8 @@ func FindProfileByUUID(uuid string) (*model.Profile, error) { // FindProfileByName 根据角色名查找档案 func FindProfileByName(name string) (*model.Profile, error) { - db := database.MustGetDB() var profile model.Profile - err := db.Where("name = ?", name).First(&profile).Error + err := getDB().Where("name = ?", name).First(&profile).Error if err != nil { return nil, err } @@ -43,44 +39,36 @@ func FindProfileByName(name string) (*model.Profile, error) { // FindProfilesByUserID 获取用户的所有档案 func FindProfilesByUserID(userID int64) ([]*model.Profile, error) { - db := database.MustGetDB() var profiles []*model.Profile - err := db.Where("user_id = ?", userID). + err := getDB().Where("user_id = ?", userID). Preload("Skin"). Preload("Cape"). Order("created_at DESC"). Find(&profiles).Error - if err != nil { - return nil, err - } - return profiles, nil + return profiles, err } // UpdateProfile 更新档案 func UpdateProfile(profile *model.Profile) error { - db := database.MustGetDB() - return db.Save(profile).Error + return getDB().Save(profile).Error } // UpdateProfileFields 更新指定字段 func UpdateProfileFields(uuid string, updates map[string]interface{}) error { - db := database.MustGetDB() - return db.Model(&model.Profile{}). + return getDB().Model(&model.Profile{}). Where("uuid = ?", uuid). Updates(updates).Error } // DeleteProfile 删除档案 func DeleteProfile(uuid string) error { - db := database.MustGetDB() - return db.Where("uuid = ?", uuid).Delete(&model.Profile{}).Error + return getDB().Where("uuid = ?", uuid).Delete(&model.Profile{}).Error } // CountProfilesByUserID 统计用户的档案数量 func CountProfilesByUserID(userID int64) (int64, error) { - db := database.MustGetDB() var count int64 - err := db.Model(&model.Profile{}). + err := getDB().Model(&model.Profile{}). Where("user_id = ?", userID). Count(&count).Error return count, err @@ -88,30 +76,22 @@ func CountProfilesByUserID(userID int64) (int64, error) { // SetActiveProfile 设置档案为活跃状态(同时将用户的其他档案设置为非活跃) func SetActiveProfile(uuid string, userID int64) error { - db := database.MustGetDB() - return db.Transaction(func(tx *gorm.DB) error { - // 将用户的所有档案设置为非活跃 + return getDB().Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.Profile{}). Where("user_id = ?", userID). Update("is_active", false).Error; err != nil { return err } - // 将指定档案设置为活跃 - if err := tx.Model(&model.Profile{}). + return tx.Model(&model.Profile{}). Where("uuid = ? AND user_id = ?", uuid, userID). - Update("is_active", true).Error; err != nil { - return err - } - - return nil + Update("is_active", true).Error }) } // UpdateProfileLastUsedAt 更新最后使用时间 func UpdateProfileLastUsedAt(uuid string) error { - db := database.MustGetDB() - return db.Model(&model.Profile{}). + return getDB().Model(&model.Profile{}). Where("uuid = ?", uuid). Update("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error } @@ -122,53 +102,40 @@ func FindOneProfileByUserID(userID int64) (*model.Profile, error) { if err != nil { return nil, err } - profile := profiles[0] - return profile, nil + if len(profiles) == 0 { + return nil, errors.New("未找到角色") + } + return profiles[0], nil } func GetProfilesByNames(names []string) ([]*model.Profile, error) { - db := database.MustGetDB() var profiles []*model.Profile - err := db.Where("name in (?)", names).Find(&profiles).Error - if err != nil { - return nil, err - } - return profiles, nil + err := getDB().Where("name in (?)", names).Find(&profiles).Error + return profiles, err } func GetProfileKeyPair(profileId string) (*model.KeyPair, error) { - db := database.MustGetDB() - // 1. 参数校验(保持原逻辑) if profileId == "" { return nil, errors.New("参数不能为空") } - // 2. GORM 查询:只查询 key_pair 字段(对应原 mongo 投影) - var profile *model.Profile - // 条件:id = profileId(PostgreSQL 主键),只选择 key_pair 字段 - result := db.WithContext(context.Background()). - Select("key_pair"). // 只查询需要的字段(投影) - Where("id = ?", profileId). // 查询条件(GORM 自动处理占位符,避免 SQL 注入) - First(&profile) // 查单条记录 + var profile model.Profile + result := getDB().WithContext(context.Background()). + Select("key_pair"). + Where("id = ?", profileId). + First(&profile) - // 3. 错误处理(适配 GORM 错误类型) if result.Error != nil { - // 空结果判断(对应原 mongo.ErrNoDocuments / pgx.ErrNoRows) - if errors.Is(result.Error, gorm.ErrRecordNotFound) { + if IsNotFound(result.Error) { return nil, errors.New("key pair未找到") } - // 保持原错误封装格式 return nil, fmt.Errorf("获取key pair失败: %w", result.Error) } - // 4. JSONB 反序列化为 model.KeyPair - keyPair := &model.KeyPair{} - return keyPair, nil + return &model.KeyPair{}, nil } func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error { - db := database.MustGetDB() - // 仅保留最必要的入参校验(避免无效数据库请求) if profileId == "" { return errors.New("profileId 不能为空") } @@ -176,24 +143,18 @@ func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error { return errors.New("keyPair 不能为 nil") } - // 事务内执行核心更新(保证原子性,出错自动回滚) - return db.Transaction(func(tx *gorm.DB) error { - // 核心更新逻辑:按 profileId 匹配,直接更新 key_pair 相关字段 + return getDB().Transaction(func(tx *gorm.DB) error { result := tx.WithContext(context.Background()). - Table("profiles"). // 目标表名(与 PostgreSQL 表一致) - Where("id = ?", profileId). // 更新条件:profileId 匹配 - // 直接映射字段(无需序列化,依赖 GORM 自动字段匹配) + Table("profiles"). + Where("id = ?", profileId). UpdateColumns(map[string]interface{}{ - "private_key": keyPair.PrivateKey, // 数据库 private_key 字段 - "public_key": keyPair.PublicKey, // 数据库 public_key 字段 - // 若 key_pair 是单个字段(非拆分),替换为:"key_pair": keyPair + "private_key": keyPair.PrivateKey, + "public_key": keyPair.PublicKey, }) - // 仅处理数据库层面的致命错误 if result.Error != nil { return fmt.Errorf("更新 keyPair 失败: %w", result.Error) } - return nil }) } diff --git a/internal/repository/system_config_repository.go b/internal/repository/system_config_repository.go index f3427de..937d518 100644 --- a/internal/repository/system_config_repository.go +++ b/internal/repository/system_config_repository.go @@ -2,56 +2,35 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" - "errors" - - "gorm.io/gorm" ) // GetSystemConfigByKey 根据键获取配置 func GetSystemConfigByKey(key string) (*model.SystemConfig, error) { - db := database.MustGetDB() var config model.SystemConfig - err := db.Where("key = ?", key).First(&config).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, err - } - return &config, nil + err := getDB().Where("key = ?", key).First(&config).Error + return HandleNotFound(&config, err) } // GetPublicSystemConfigs 获取所有公开配置 func GetPublicSystemConfigs() ([]model.SystemConfig, error) { - db := database.MustGetDB() var configs []model.SystemConfig - err := db.Where("is_public = ?", true).Find(&configs).Error - if err != nil { - return nil, err - } - return configs, nil + err := getDB().Where("is_public = ?", true).Find(&configs).Error + return configs, err } // GetAllSystemConfigs 获取所有配置(管理员用) func GetAllSystemConfigs() ([]model.SystemConfig, error) { - db := database.MustGetDB() var configs []model.SystemConfig - err := db.Find(&configs).Error - if err != nil { - return nil, err - } - return configs, nil + err := getDB().Find(&configs).Error + return configs, err } // UpdateSystemConfig 更新配置 func UpdateSystemConfig(config *model.SystemConfig) error { - db := database.MustGetDB() - return db.Save(config).Error + return getDB().Save(config).Error } // UpdateSystemConfigValue 更新配置值 func UpdateSystemConfigValue(key, value string) error { - db := database.MustGetDB() - return db.Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error + return getDB().Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error } diff --git a/internal/repository/texture_repository.go b/internal/repository/texture_repository.go index 69a58c5..0406ff3 100644 --- a/internal/repository/texture_repository.go +++ b/internal/repository/texture_repository.go @@ -2,63 +2,44 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" + "gorm.io/gorm" ) // CreateTexture 创建材质 func CreateTexture(texture *model.Texture) error { - db := database.MustGetDB() - return db.Create(texture).Error + return getDB().Create(texture).Error } // FindTextureByID 根据ID查找材质 func FindTextureByID(id int64) (*model.Texture, error) { - db := database.MustGetDB() var texture model.Texture - err := db.Preload("Uploader").First(&texture, id).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &texture, nil + err := getDB().Preload("Uploader").First(&texture, id).Error + return HandleNotFound(&texture, err) } // FindTextureByHash 根据Hash查找材质 func FindTextureByHash(hash string) (*model.Texture, error) { - db := database.MustGetDB() var texture model.Texture - err := db.Where("hash = ?", hash).First(&texture).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &texture, nil + err := getDB().Where("hash = ?", hash).First(&texture).Error + return HandleNotFound(&texture, err) } // FindTexturesByUploaderID 根据上传者ID查找材质列表 func FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) { - db := database.MustGetDB() + db := getDB() var textures []*model.Texture var total int64 query := db.Model(&model.Texture{}).Where("uploader_id = ? AND status != -1", uploaderID) - // 获取总数 if err := query.Count(&total).Error; err != nil { return nil, 0, err } - // 分页查询 - offset := (page - 1) * pageSize - err := query.Preload("Uploader"). + err := query.Scopes(Paginate(page, pageSize)). + Preload("Uploader"). Order("created_at DESC"). - Offset(offset). - Limit(pageSize). Find(&textures).Error if err != nil { @@ -70,38 +51,29 @@ func FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Te // SearchTextures 搜索材质 func SearchTextures(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) { - db := database.MustGetDB() + db := getDB() var textures []*model.Texture var total int64 query := db.Model(&model.Texture{}).Where("status = 1") - // 公开筛选 if publicOnly { query = query.Where("is_public = ?", true) } - - // 类型筛选 if textureType != "" { query = query.Where("type = ?", textureType) } - - // 关键词搜索 if keyword != "" { query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%") } - // 获取总数 if err := query.Count(&total).Error; err != nil { return nil, 0, err } - // 分页查询 - offset := (page - 1) * pageSize - err := query.Preload("Uploader"). + err := query.Scopes(Paginate(page, pageSize)). + Preload("Uploader"). Order("created_at DESC"). - Offset(offset). - Limit(pageSize). Find(&textures).Error if err != nil { @@ -113,86 +85,72 @@ func SearchTextures(keyword string, textureType model.TextureType, publicOnly bo // UpdateTexture 更新材质 func UpdateTexture(texture *model.Texture) error { - db := database.MustGetDB() - return db.Save(texture).Error + return getDB().Save(texture).Error } // UpdateTextureFields 更新材质指定字段 func UpdateTextureFields(id int64, fields map[string]interface{}) error { - db := database.MustGetDB() - return db.Model(&model.Texture{}).Where("id = ?", id).Updates(fields).Error + return getDB().Model(&model.Texture{}).Where("id = ?", id).Updates(fields).Error } // DeleteTexture 删除材质(软删除) func DeleteTexture(id int64) error { - db := database.MustGetDB() - return db.Model(&model.Texture{}).Where("id = ?", id).Update("status", -1).Error + return getDB().Model(&model.Texture{}).Where("id = ?", id).Update("status", -1).Error } // IncrementTextureDownloadCount 增加下载次数 func IncrementTextureDownloadCount(id int64) error { - db := database.MustGetDB() - return db.Model(&model.Texture{}).Where("id = ?", id). + return getDB().Model(&model.Texture{}).Where("id = ?", id). UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error } // IncrementTextureFavoriteCount 增加收藏次数 func IncrementTextureFavoriteCount(id int64) error { - db := database.MustGetDB() - return db.Model(&model.Texture{}).Where("id = ?", id). + return getDB().Model(&model.Texture{}).Where("id = ?", id). UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error } // DecrementTextureFavoriteCount 减少收藏次数 func DecrementTextureFavoriteCount(id int64) error { - db := database.MustGetDB() - return db.Model(&model.Texture{}).Where("id = ?", id). + return getDB().Model(&model.Texture{}).Where("id = ?", id). UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error } // CreateTextureDownloadLog 创建下载日志 func CreateTextureDownloadLog(log *model.TextureDownloadLog) error { - db := database.MustGetDB() - return db.Create(log).Error + return getDB().Create(log).Error } // IsTextureFavorited 检查是否已收藏 func IsTextureFavorited(userID, textureID int64) (bool, error) { - db := database.MustGetDB() var count int64 - err := db.Model(&model.UserTextureFavorite{}). + err := getDB().Model(&model.UserTextureFavorite{}). Where("user_id = ? AND texture_id = ?", userID, textureID). Count(&count).Error - if err != nil { - return false, err - } - return count > 0, nil + return count > 0, err } // AddTextureFavorite 添加收藏 func AddTextureFavorite(userID, textureID int64) error { - db := database.MustGetDB() favorite := &model.UserTextureFavorite{ UserID: userID, TextureID: textureID, } - return db.Create(favorite).Error + return getDB().Create(favorite).Error } // RemoveTextureFavorite 取消收藏 func RemoveTextureFavorite(userID, textureID int64) error { - db := database.MustGetDB() - return db.Where("user_id = ? AND texture_id = ?", userID, textureID). + return getDB().Where("user_id = ? AND texture_id = ?", userID, textureID). Delete(&model.UserTextureFavorite{}).Error } // GetUserTextureFavorites 获取用户收藏的材质列表 func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture, int64, error) { - db := database.MustGetDB() + db := getDB() var textures []*model.Texture var total int64 - // 子查询获取收藏的材质ID subQuery := db.Model(&model.UserTextureFavorite{}). Select("texture_id"). Where("user_id = ?", userID) @@ -200,17 +158,13 @@ func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture query := db.Model(&model.Texture{}). Where("id IN (?) AND status = 1", subQuery) - // 获取总数 if err := query.Count(&total).Error; err != nil { return nil, 0, err } - // 分页查询 - offset := (page - 1) * pageSize - err := query.Preload("Uploader"). + err := query.Scopes(Paginate(page, pageSize)). + Preload("Uploader"). Order("created_at DESC"). - Offset(offset). - Limit(pageSize). Find(&textures).Error if err != nil { @@ -222,9 +176,8 @@ func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture // CountTexturesByUploaderID 统计用户上传的材质数量 func CountTexturesByUploaderID(uploaderID int64) (int64, error) { - db := database.MustGetDB() var count int64 - err := db.Model(&model.Texture{}). + err := getDB().Model(&model.Texture{}). Where("uploader_id = ? AND status != -1", uploaderID). Count(&count).Error return count, err diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 725b0b6..11d6abd 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -2,48 +2,38 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" ) func CreateToken(token *model.Token) error { - db := database.MustGetDB() - return db.Create(token).Error + return getDB().Create(token).Error } func GetTokensByUserId(userId int64) ([]*model.Token, error) { - db := database.MustGetDB() - tokens := make([]*model.Token, 0) - err := db.Where("user_id = ?", userId).Find(&tokens).Error - if err != nil { - return nil, err - } - return tokens, nil + var tokens []*model.Token + err := getDB().Where("user_id = ?", userId).Find(&tokens).Error + return tokens, err } func BatchDeleteTokens(tokensToDelete []string) (int64, error) { - db := database.MustGetDB() if len(tokensToDelete) == 0 { - return 0, nil // 无需要删除的令牌,直接返回 + return 0, nil } - result := db.Where("access_token IN ?", tokensToDelete).Delete(&model.Token{}) - + result := getDB().Where("access_token IN ?", tokensToDelete).Delete(&model.Token{}) return result.RowsAffected, result.Error } func FindTokenByID(accessToken string) (*model.Token, error) { - db := database.MustGetDB() - var tokens []*model.Token - err := db.Where("_id = ?", accessToken).Find(&tokens).Error + var token model.Token + err := getDB().Where("access_token = ?", accessToken).First(&token).Error if err != nil { return nil, err } - return tokens[0], nil + return &token, nil } func GetUUIDByAccessToken(accessToken string) (string, error) { - db := database.MustGetDB() var token model.Token - err := db.Where("access_token = ?", accessToken).First(&token).Error + err := getDB().Where("access_token = ?", accessToken).First(&token).Error if err != nil { return "", err } @@ -51,9 +41,8 @@ func GetUUIDByAccessToken(accessToken string) (string, error) { } func GetUserIDByAccessToken(accessToken string) (int64, error) { - db := database.MustGetDB() var token model.Token - err := db.Where("access_token = ?", accessToken).First(&token).Error + err := getDB().Where("access_token = ?", accessToken).First(&token).Error if err != nil { return 0, err } @@ -61,9 +50,8 @@ func GetUserIDByAccessToken(accessToken string) (int64, error) { } func GetTokenByAccessToken(accessToken string) (*model.Token, error) { - db := database.MustGetDB() var token model.Token - err := db.Where("access_token = ?", accessToken).First(&token).Error + err := getDB().Where("access_token = ?", accessToken).First(&token).Error if err != nil { return nil, err } @@ -71,19 +59,9 @@ func GetTokenByAccessToken(accessToken string) (*model.Token, error) { } func DeleteTokenByAccessToken(accessToken string) error { - db := database.MustGetDB() - err := db.Where("access_token = ?", accessToken).Delete(&model.Token{}).Error - if err != nil { - return err - } - return nil + return getDB().Where("access_token = ?", accessToken).Delete(&model.Token{}).Error } func DeleteTokenByUserId(userId int64) error { - db := database.MustGetDB() - err := db.Where("user_id = ?", userId).Delete(&model.Token{}).Error - if err != nil { - return err - } - return nil + return getDB().Where("user_id = ?", userId).Delete(&model.Token{}).Error } diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 981c338..52e9cb4 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -2,7 +2,6 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" "errors" "gorm.io/gorm" @@ -10,87 +9,58 @@ import ( // CreateUser 创建用户 func CreateUser(user *model.User) error { - db := database.MustGetDB() - return db.Create(user).Error + return getDB().Create(user).Error } // FindUserByID 根据ID查找用户 func FindUserByID(id int64) (*model.User, error) { - db := database.MustGetDB() var user model.User - err := db.Where("id = ? AND status != -1", id).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, err - } - return &user, nil + err := getDB().Where("id = ? AND status != -1", id).First(&user).Error + return HandleNotFound(&user, err) } // FindUserByUsername 根据用户名查找用户 func FindUserByUsername(username string) (*model.User, error) { - db := database.MustGetDB() var user model.User - err := db.Where("username = ? AND status != -1", username).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, err - } - return &user, nil + err := getDB().Where("username = ? AND status != -1", username).First(&user).Error + return HandleNotFound(&user, err) } // FindUserByEmail 根据邮箱查找用户 func FindUserByEmail(email string) (*model.User, error) { - db := database.MustGetDB() var user model.User - err := db.Where("email = ? AND status != -1", email).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, err - } - return &user, nil + err := getDB().Where("email = ? AND status != -1", email).First(&user).Error + return HandleNotFound(&user, err) } // UpdateUser 更新用户 func UpdateUser(user *model.User) error { - db := database.MustGetDB() - return db.Save(user).Error + return getDB().Save(user).Error } // UpdateUserFields 更新指定字段 func UpdateUserFields(id int64, fields map[string]interface{}) error { - db := database.MustGetDB() - return db.Model(&model.User{}).Where("id = ?", id).Updates(fields).Error + return getDB().Model(&model.User{}).Where("id = ?", id).Updates(fields).Error } // DeleteUser 软删除用户 func DeleteUser(id int64) error { - db := database.MustGetDB() - return db.Model(&model.User{}).Where("id = ?", id).Update("status", -1).Error + return getDB().Model(&model.User{}).Where("id = ?", id).Update("status", -1).Error } // CreateLoginLog 创建登录日志 func CreateLoginLog(log *model.UserLoginLog) error { - db := database.MustGetDB() - return db.Create(log).Error + return getDB().Create(log).Error } // CreatePointLog 创建积分日志 func CreatePointLog(log *model.UserPointLog) error { - db := database.MustGetDB() - return db.Create(log).Error + return getDB().Create(log).Error } // UpdateUserPoints 更新用户积分(事务) func UpdateUserPoints(userID int64, amount int, changeType, reason string) error { - db := database.MustGetDB() - return db.Transaction(func(tx *gorm.DB) error { - // 获取当前用户积分 + return getDB().Transaction(func(tx *gorm.DB) error { var user model.User if err := tx.Where("id = ?", userID).First(&user).Error; err != nil { return err @@ -99,17 +69,14 @@ func UpdateUserPoints(userID int64, amount int, changeType, reason string) error balanceBefore := user.Points balanceAfter := balanceBefore + amount - // 检查积分是否足够 if balanceAfter < 0 { return errors.New("积分不足") } - // 更新用户积分 if err := tx.Model(&user).Update("points", balanceAfter).Error; err != nil { return err } - // 创建积分日志 log := &model.UserPointLog{ UserID: userID, ChangeType: changeType, @@ -125,12 +92,10 @@ func UpdateUserPoints(userID int64, amount int, changeType, reason string) error // UpdateUserAvatar 更新用户头像 func UpdateUserAvatar(userID int64, avatarURL string) error { - db := database.MustGetDB() - return db.Model(&model.User{}).Where("id = ?", userID).Update("avatar", avatarURL).Error + return getDB().Model(&model.User{}).Where("id = ?", userID).Update("avatar", avatarURL).Error } // UpdateUserEmail 更新用户邮箱 func UpdateUserEmail(userID int64, email string) error { - db := database.MustGetDB() - return db.Model(&model.User{}).Where("id = ?", userID).Update("email", email).Error + return getDB().Model(&model.User{}).Where("id = ?", userID).Update("email", email).Error } diff --git a/internal/repository/yggdrasil_repository.go b/internal/repository/yggdrasil_repository.go index 49620e3..4435705 100644 --- a/internal/repository/yggdrasil_repository.go +++ b/internal/repository/yggdrasil_repository.go @@ -2,13 +2,11 @@ package repository import ( "carrotskin/internal/model" - "carrotskin/pkg/database" ) -func GetYggdrasilPasswordById(Id int64) (string, error) { - db := database.MustGetDB() +func GetYggdrasilPasswordById(id int64) (string, error) { var yggdrasil model.Yggdrasil - err := db.Where("id = ?", Id).First(&yggdrasil).Error + err := getDB().Where("id = ?", id).First(&yggdrasil).Error if err != nil { return "", err } @@ -17,6 +15,5 @@ func GetYggdrasilPasswordById(Id int64) (string, error) { // ResetYggdrasilPassword 重置Yggdrasil密码 func ResetYggdrasilPassword(userId int64, newPassword string) error { - db := database.MustGetDB() - return db.Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error + return getDB().Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error } \ No newline at end of file diff --git a/internal/service/helpers.go b/internal/service/helpers.go new file mode 100644 index 0000000..2335c8b --- /dev/null +++ b/internal/service/helpers.go @@ -0,0 +1,105 @@ +package service + +import ( + "carrotskin/internal/model" + "carrotskin/internal/repository" + "errors" + "fmt" + + "gorm.io/gorm" +) + +// 通用错误 +var ( + ErrProfileNotFound = errors.New("档案不存在") + ErrProfileNoPermission = errors.New("无权操作此档案") + ErrTextureNotFound = errors.New("材质不存在") + ErrTextureNoPermission = errors.New("无权操作此材质") + ErrUserNotFound = errors.New("用户不存在") +) + +// NormalizePagination 规范化分页参数 +func NormalizePagination(page, pageSize int) (int, int) { + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + return page, pageSize +} + +// GetProfileWithPermissionCheck 获取档案并验证权限 +// 返回档案,如果不存在或无权限则返回相应错误 +func GetProfileWithPermissionCheck(uuid string, userID int64) (*model.Profile, error) { + profile, err := repository.FindProfileByUUID(uuid) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrProfileNotFound + } + return nil, fmt.Errorf("查询档案失败: %w", err) + } + + if profile.UserID != userID { + return nil, ErrProfileNoPermission + } + + return profile, nil +} + +// GetTextureWithPermissionCheck 获取材质并验证权限 +// 返回材质,如果不存在或无权限则返回相应错误 +func GetTextureWithPermissionCheck(textureID, userID int64) (*model.Texture, error) { + texture, err := repository.FindTextureByID(textureID) + if err != nil { + return nil, err + } + if texture == nil { + return nil, ErrTextureNotFound + } + + if texture.UploaderID != userID { + return nil, ErrTextureNoPermission + } + + return texture, nil +} + +// EnsureTextureExists 确保材质存在 +func EnsureTextureExists(textureID int64) (*model.Texture, error) { + texture, err := repository.FindTextureByID(textureID) + if err != nil { + return nil, err + } + if texture == nil { + return nil, ErrTextureNotFound + } + if texture.Status == -1 { + return nil, errors.New("材质已删除") + } + return texture, nil +} + +// EnsureUserExists 确保用户存在 +func EnsureUserExists(userID int64) (*model.User, error) { + user, err := repository.FindUserByID(userID) + if err != nil { + return nil, err + } + if user == nil { + return nil, ErrUserNotFound + } + return user, nil +} + +// WrapError 包装错误,添加上下文信息 +func WrapError(err error, message string) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", message, err) +} + diff --git a/internal/service/profile_service.go b/internal/service/profile_service.go index 1cd5d17..d3e2057 100644 --- a/internal/service/profile_service.go +++ b/internal/service/profile_service.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "errors" "fmt" + "github.com/google/uuid" "github.com/jackc/pgx/v5" "gorm.io/gorm" @@ -16,53 +17,47 @@ import ( // CreateProfile 创建档案 func CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error) { - // 1. 验证用户存在 - user, err := repository.FindUserByID(userID) + // 验证用户存在 + user, err := EnsureUserExists(userID) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("用户不存在") - } - return nil, fmt.Errorf("查询用户失败: %w", err) + return nil, err } - if user.Status != 1 { return nil, fmt.Errorf("用户状态异常") } - // 2. 检查角色名是否已存在 + // 检查角色名是否已存在 existingName, err := repository.FindProfileByName(name) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("查询角色名失败: %w", err) + return nil, WrapError(err, "查询角色名失败") } if existingName != nil { return nil, fmt.Errorf("角色名已被使用") } - // 3. 生成UUID + // 生成UUID和RSA密钥 profileUUID := uuid.New().String() - - // 4. 生成RSA密钥对 privateKey, err := generateRSAPrivateKey() if err != nil { - return nil, fmt.Errorf("生成RSA密钥失败: %w", err) + return nil, WrapError(err, "生成RSA密钥失败") } - // 5. 创建档案 + // 创建档案 profile := &model.Profile{ UUID: profileUUID, UserID: userID, Name: name, RSAPrivateKey: privateKey, - IsActive: true, // 新创建的档案默认为活跃状态 + IsActive: true, } if err := repository.CreateProfile(profile); err != nil { - return nil, fmt.Errorf("创建档案失败: %w", err) + return nil, WrapError(err, "创建档案失败") } - // 6. 将用户的其他档案设置为非活跃 + // 设置活跃状态 if err := repository.SetActiveProfile(profileUUID, userID); err != nil { - return nil, fmt.Errorf("设置活跃状态失败: %w", err) + return nil, WrapError(err, "设置活跃状态失败") } return profile, nil @@ -73,9 +68,9 @@ func GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error) { profile, err := repository.FindProfileByUUID(uuid) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("档案不存在") + return nil, ErrProfileNotFound } - return nil, fmt.Errorf("查询档案失败: %w", err) + return nil, WrapError(err, "查询档案失败") } return profile, nil } @@ -84,32 +79,24 @@ func GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error) { func GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error) { profiles, err := repository.FindProfilesByUserID(userID) if err != nil { - return nil, fmt.Errorf("查询档案列表失败: %w", err) + return nil, WrapError(err, "查询档案列表失败") } return profiles, nil } // UpdateProfile 更新档案 func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error) { - // 1. 查询档案 - profile, err := repository.FindProfileByUUID(uuid) + // 获取档案并验证权限 + profile, err := GetProfileWithPermissionCheck(uuid, userID) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("档案不存在") - } - return nil, fmt.Errorf("查询档案失败: %w", err) + return nil, err } - // 2. 验证权限 - if profile.UserID != userID { - return nil, fmt.Errorf("无权操作此档案") - } - - // 3. 检查角色名是否重复 + // 检查角色名是否重复 if name != nil && *name != profile.Name { existingName, err := repository.FindProfileByName(*name) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("查询角色名失败: %w", err) + return nil, WrapError(err, "查询角色名失败") } if existingName != nil { return nil, fmt.Errorf("角色名已被使用") @@ -117,7 +104,7 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, profile.Name = *name } - // 4. 更新皮肤和披风 + // 更新皮肤和披风 if skinID != nil { profile.SkinID = skinID } @@ -125,63 +112,37 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, profile.CapeID = capeID } - // 5. 保存更新 if err := repository.UpdateProfile(profile); err != nil { - return nil, fmt.Errorf("更新档案失败: %w", err) + return nil, WrapError(err, "更新档案失败") } - // 6. 重新加载关联数据 return repository.FindProfileByUUID(uuid) } // DeleteProfile 删除档案 func DeleteProfile(db *gorm.DB, uuid string, userID int64) error { - // 1. 查询档案 - profile, err := repository.FindProfileByUUID(uuid) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("档案不存在") - } - return fmt.Errorf("查询档案失败: %w", err) + if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil { + return err } - // 2. 验证权限 - if profile.UserID != userID { - return fmt.Errorf("无权操作此档案") - } - - // 3. 删除档案 if err := repository.DeleteProfile(uuid); err != nil { - return fmt.Errorf("删除档案失败: %w", err) + return WrapError(err, "删除档案失败") } - return nil } // SetActiveProfile 设置活跃档案 func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error { - // 1. 查询档案 - profile, err := repository.FindProfileByUUID(uuid) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("档案不存在") - } - return fmt.Errorf("查询档案失败: %w", err) + if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil { + return err } - // 2. 验证权限 - if profile.UserID != userID { - return fmt.Errorf("无权操作此档案") - } - - // 3. 设置活跃状态 if err := repository.SetActiveProfile(uuid, userID); err != nil { - return fmt.Errorf("设置活跃状态失败: %w", err) + return WrapError(err, "设置活跃状态失败") } - // 4. 更新最后使用时间 if err := repository.UpdateProfileLastUsedAt(uuid); err != nil { - return fmt.Errorf("更新使用时间失败: %w", err) + return WrapError(err, "更新使用时间失败") } return nil @@ -191,25 +152,22 @@ func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error { func CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error { count, err := repository.CountProfilesByUserID(userID) if err != nil { - return fmt.Errorf("查询档案数量失败: %w", err) + return WrapError(err, "查询档案数量失败") } if int(count) >= maxProfiles { return fmt.Errorf("已达到档案数量上限(%d个)", maxProfiles) } - return nil } // generateRSAPrivateKey 生成RSA-2048私钥(PEM格式) func generateRSAPrivateKey() (string, error) { - // 生成2048位RSA密钥对 privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", err } - // 将私钥编码为PEM格式 privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) privateKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", @@ -229,7 +187,7 @@ func ValidateProfileByUserID(db *gorm.DB, userId int64, UUID string) (bool, erro if errors.Is(err, pgx.ErrNoRows) { return false, errors.New("配置文件不存在") } - return false, fmt.Errorf("验证配置文件失败: %w", err) + return false, WrapError(err, "验证配置文件失败") } return profile.UserID == userId, nil } @@ -237,16 +195,15 @@ func ValidateProfileByUserID(db *gorm.DB, userId int64, UUID string) (bool, erro func GetProfilesDataByNames(db *gorm.DB, names []string) ([]*model.Profile, error) { profiles, err := repository.GetProfilesByNames(names) if err != nil { - return nil, fmt.Errorf("查找失败: %w", err) + return nil, WrapError(err, "查找失败") } return profiles, nil } -// GetProfileKeyPair 从 PostgreSQL 获取密钥对(GORM 实现,无手动 SQL) func GetProfileKeyPair(db *gorm.DB, profileId string) (*model.KeyPair, error) { keyPair, err := repository.GetProfileKeyPair(profileId) if err != nil { - return nil, fmt.Errorf("查找失败: %w", err) + return nil, WrapError(err, "查找失败") } return keyPair, nil } diff --git a/internal/service/security_service.go b/internal/service/security_service.go new file mode 100644 index 0000000..195403c --- /dev/null +++ b/internal/service/security_service.go @@ -0,0 +1,142 @@ +package service + +import ( + "context" + "fmt" + "time" + + "carrotskin/pkg/redis" +) + +const ( + // 登录失败限制配置 + MaxLoginAttempts = 5 // 最大登录失败次数 + LoginLockDuration = 15 * time.Minute // 账号锁定时间 + LoginAttemptWindow = 10 * time.Minute // 失败次数统计窗口 + + // 验证码错误限制配置 + MaxVerifyAttempts = 5 // 最大验证码错误次数 + VerifyLockDuration = 30 * time.Minute // 验证码锁定时间 + + // Redis Key 前缀 + LoginAttemptKeyPrefix = "security:login_attempt:" + LoginLockedKeyPrefix = "security:login_locked:" + VerifyAttemptKeyPrefix = "security:verify_attempt:" + VerifyLockedKeyPrefix = "security:verify_locked:" +) + +// CheckLoginLocked 检查账号是否被锁定 +func CheckLoginLocked(ctx context.Context, redisClient *redis.Client, identifier string) (bool, time.Duration, error) { + key := LoginLockedKeyPrefix + identifier + ttl, err := redisClient.TTL(ctx, key) + if err != nil { + return false, 0, err + } + if ttl > 0 { + return true, ttl, nil + } + return false, 0, nil +} + +// RecordLoginFailure 记录登录失败 +func RecordLoginFailure(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) { + attemptKey := LoginAttemptKeyPrefix + identifier + + // 增加失败次数 + count, err := redisClient.Incr(ctx, attemptKey) + if err != nil { + return 0, fmt.Errorf("记录登录失败次数失败: %w", err) + } + + // 设置过期时间(仅在第一次设置) + if count == 1 { + if err := redisClient.Expire(ctx, attemptKey, LoginAttemptWindow); err != nil { + return int(count), fmt.Errorf("设置过期时间失败: %w", err) + } + } + + // 如果超过最大次数,锁定账号 + if count >= MaxLoginAttempts { + lockedKey := LoginLockedKeyPrefix + identifier + if err := redisClient.Set(ctx, lockedKey, "1", LoginLockDuration); err != nil { + return int(count), fmt.Errorf("锁定账号失败: %w", err) + } + // 清除失败计数 + _ = redisClient.Del(ctx, attemptKey) + } + + return int(count), nil +} + +// ClearLoginAttempts 清除登录失败记录(登录成功后调用) +func ClearLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) error { + attemptKey := LoginAttemptKeyPrefix + identifier + return redisClient.Del(ctx, attemptKey) +} + +// GetRemainingLoginAttempts 获取剩余登录尝试次数 +func GetRemainingLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) { + attemptKey := LoginAttemptKeyPrefix + identifier + countStr, err := redisClient.Get(ctx, attemptKey) + if err != nil { + // key 不存在,返回最大次数 + return MaxLoginAttempts, nil + } + + var count int + fmt.Sscanf(countStr, "%d", &count) + remaining := MaxLoginAttempts - count + if remaining < 0 { + remaining = 0 + } + return remaining, nil +} + +// CheckVerifyLocked 检查验证码是否被锁定 +func CheckVerifyLocked(ctx context.Context, redisClient *redis.Client, email, codeType string) (bool, time.Duration, error) { + key := VerifyLockedKeyPrefix + codeType + ":" + email + ttl, err := redisClient.TTL(ctx, key) + if err != nil { + return false, 0, err + } + if ttl > 0 { + return true, ttl, nil + } + return false, 0, nil +} + +// RecordVerifyFailure 记录验证码验证失败 +func RecordVerifyFailure(ctx context.Context, redisClient *redis.Client, email, codeType string) (int, error) { + attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email + + // 增加失败次数 + count, err := redisClient.Incr(ctx, attemptKey) + if err != nil { + return 0, fmt.Errorf("记录验证码失败次数失败: %w", err) + } + + // 设置过期时间 + if count == 1 { + if err := redisClient.Expire(ctx, attemptKey, VerifyLockDuration); err != nil { + return int(count), err + } + } + + // 如果超过最大次数,锁定验证 + if count >= MaxVerifyAttempts { + lockedKey := VerifyLockedKeyPrefix + codeType + ":" + email + if err := redisClient.Set(ctx, lockedKey, "1", VerifyLockDuration); err != nil { + return int(count), err + } + _ = redisClient.Del(ctx, attemptKey) + } + + return int(count), nil +} + +// ClearVerifyAttempts 清除验证码失败记录(验证成功后调用) +func ClearVerifyAttempts(ctx context.Context, redisClient *redis.Client, email, codeType string) error { + attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email + return redisClient.Del(ctx, attemptKey) +} + diff --git a/internal/service/texture_service.go b/internal/service/texture_service.go index c3747bb..ea312f0 100644 --- a/internal/service/texture_service.go +++ b/internal/service/texture_service.go @@ -12,13 +12,9 @@ import ( // CreateTexture 创建材质 func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) { // 验证用户存在 - user, err := repository.FindUserByID(uploaderID) - if err != nil { + if _, err := EnsureUserExists(uploaderID); err != nil { return nil, err } - if user == nil { - return nil, errors.New("用户不存在") - } // 检查Hash是否已存在 existingTexture, err := repository.FindTextureByHash(hash) @@ -30,14 +26,9 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType } // 转换材质类型 - var textureTypeEnum model.TextureType - switch textureType { - case "SKIN": - textureTypeEnum = model.TextureTypeSkin - case "CAPE": - textureTypeEnum = model.TextureTypeCape - default: - return nil, errors.New("无效的材质类型") + textureTypeEnum, err := parseTextureType(textureType) + if err != nil { + return nil, err } // 创建材质 @@ -65,58 +56,27 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType // GetTextureByID 根据ID获取材质 func GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error) { - texture, err := repository.FindTextureByID(id) - if err != nil { - return nil, err - } - if texture == nil { - return nil, errors.New("材质不存在") - } - if texture.Status == -1 { - return nil, errors.New("材质已删除") - } - return texture, nil + return EnsureTextureExists(id) } // GetUserTextures 获取用户上传的材质列表 func GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) { - if page < 1 { - page = 1 - } - if pageSize < 1 || pageSize > 100 { - pageSize = 20 - } - + page, pageSize = NormalizePagination(page, pageSize) return repository.FindTexturesByUploaderID(uploaderID, page, pageSize) } // SearchTextures 搜索材质 func SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) { - if page < 1 { - page = 1 - } - if pageSize < 1 || pageSize > 100 { - pageSize = 20 - } - + page, pageSize = NormalizePagination(page, pageSize) return repository.SearchTextures(keyword, textureType, publicOnly, page, pageSize) } // UpdateTexture 更新材质 func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) { - // 获取材质 - texture, err := repository.FindTextureByID(textureID) - if err != nil { + // 获取材质并验证权限 + if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil { return nil, err } - if texture == nil { - return nil, errors.New("材质不存在") - } - - // 检查权限:只有上传者可以修改 - if texture.UploaderID != uploaderID { - return nil, errors.New("无权修改此材质") - } // 更新字段 updates := make(map[string]interface{}) @@ -136,46 +96,27 @@ func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description s } } - // 返回更新后的材质 return repository.FindTextureByID(textureID) } // DeleteTexture 删除材质 func DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error { - // 获取材质 - texture, err := repository.FindTextureByID(textureID) - if err != nil { + if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil { return err } - if texture == nil { - return errors.New("材质不存在") - } - - // 检查权限:只有上传者可以删除 - if texture.UploaderID != uploaderID { - return errors.New("无权删除此材质") - } - return repository.DeleteTexture(textureID) } // RecordTextureDownload 记录下载 func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error { - // 检查材质是否存在 - texture, err := repository.FindTextureByID(textureID) - if err != nil { + if _, err := EnsureTextureExists(textureID); err != nil { return err } - if texture == nil { - return errors.New("材质不存在") - } - // 增加下载次数 if err := repository.IncrementTextureDownloadCount(textureID); err != nil { return err } - // 创建下载日志 log := &model.TextureDownloadLog{ TextureID: textureID, UserID: userID, @@ -188,23 +129,17 @@ func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddres // ToggleTextureFavorite 切换收藏状态 func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) { - // 检查材质是否存在 - texture, err := repository.FindTextureByID(textureID) - if err != nil { + if _, err := EnsureTextureExists(textureID); err != nil { return false, err } - if texture == nil { - return false, errors.New("材质不存在") - } - // 检查是否已收藏 isFavorited, err := repository.IsTextureFavorited(userID, textureID) if err != nil { return false, err } if isFavorited { - // 取消收藏 + // 已收藏 -> 取消收藏 if err := repository.RemoveTextureFavorite(userID, textureID); err != nil { return false, err } @@ -213,7 +148,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) { } return false, nil } else { - // 添加收藏 + // 未收藏 -> 添加收藏 if err := repository.AddTextureFavorite(userID, textureID); err != nil { return false, err } @@ -226,13 +161,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) { // GetUserTextureFavorites 获取用户收藏的材质列表 func GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error) { - if page < 1 { - page = 1 - } - if pageSize < 1 || pageSize > 100 { - pageSize = 20 - } - + page, pageSize = NormalizePagination(page, pageSize) return repository.GetUserTextureFavorites(userID, page, pageSize) } @@ -249,3 +178,15 @@ func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) err return nil } + +// parseTextureType 解析材质类型 +func parseTextureType(textureType string) (model.TextureType, error) { + switch textureType { + case "SKIN": + return model.TextureTypeSkin, nil + case "CAPE": + return model.TextureTypeCape, nil + default: + return "", errors.New("无效的材质类型") + } +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index e1d6ff3..6c46643 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -4,7 +4,10 @@ import ( "carrotskin/internal/model" "carrotskin/internal/repository" "carrotskin/pkg/auth" + "carrotskin/pkg/redis" + "context" "errors" + "fmt" "strings" "time" ) @@ -37,7 +40,12 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar // 确定头像URL:优先使用用户提供的头像,否则使用默认头像 avatarURL := avatar - if avatarURL == "" { + if avatarURL != "" { + // 验证用户提供的头像 URL 是否来自允许的域名 + if err := ValidateAvatarURL(avatarURL); err != nil { + return nil, "", err + } + } else { avatarURL = getDefaultAvatar() } @@ -49,8 +57,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar Avatar: avatarURL, Role: "user", Status: 1, - Points: 0, // 初始积分可以从配置读取 - // Properties 字段使用 datatypes.JSON,默认为 nil,数据库会存储 NULL + Points: 0, } if err := repository.CreateUser(user); err != nil { @@ -63,22 +70,34 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar return nil, "", errors.New("生成Token失败") } - // TODO: 添加注册奖励积分 - return user, token, nil } // LoginUser 用户登录(支持用户名或邮箱登录) func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) { + return LoginUserWithRateLimit(nil, jwtService, usernameOrEmail, password, ipAddress, userAgent) +} + +// LoginUserWithRateLimit 用户登录(带频率限制) +func LoginUserWithRateLimit(redisClient *redis.Client, jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) { + ctx := context.Background() + + // 检查账号是否被锁定(基于用户名/邮箱和IP) + if redisClient != nil { + identifier := usernameOrEmail + ":" + ipAddress + locked, ttl, err := CheckLoginLocked(ctx, redisClient, identifier) + if err == nil && locked { + return nil, "", fmt.Errorf("登录尝试次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1) + } + } + // 查找用户:判断是用户名还是邮箱 var user *model.User var err error if strings.Contains(usernameOrEmail, "@") { - // 包含@符号,认为是邮箱 user, err = repository.FindUserByEmail(usernameOrEmail) } else { - // 否则认为是用户名 user, err = repository.FindUserByUsername(usernameOrEmail) } @@ -86,7 +105,16 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress return nil, "", err } if user == nil { - // 记录失败日志 + // 记录失败尝试 + if redisClient != nil { + identifier := usernameOrEmail + ":" + ipAddress + count, _ := RecordLoginFailure(ctx, redisClient, identifier) + remaining := MaxLoginAttempts - count + if remaining > 0 { + logFailedLogin(0, ipAddress, userAgent, "用户不存在") + return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining) + } + } logFailedLogin(0, ipAddress, userAgent, "用户不存在") return nil, "", errors.New("用户名/邮箱或密码错误") } @@ -99,10 +127,26 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress // 验证密码 if !auth.CheckPassword(user.Password, password) { + // 记录失败尝试 + if redisClient != nil { + identifier := usernameOrEmail + ":" + ipAddress + count, _ := RecordLoginFailure(ctx, redisClient, identifier) + remaining := MaxLoginAttempts - count + if remaining > 0 { + logFailedLogin(user.ID, ipAddress, userAgent, "密码错误") + return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining) + } + } logFailedLogin(user.ID, ipAddress, userAgent, "密码错误") return nil, "", errors.New("用户名/邮箱或密码错误") } + // 登录成功,清除失败计数 + if redisClient != nil { + identifier := usernameOrEmail + ":" + ipAddress + _ = ClearLoginAttempts(ctx, redisClient, identifier) + } + // 生成JWT Token token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role) if err != nil { @@ -141,24 +185,20 @@ func UpdateUserAvatar(userID int64, avatarURL string) error { // ChangeUserPassword 修改密码 func ChangeUserPassword(userID int64, oldPassword, newPassword string) error { - // 获取用户 user, err := repository.FindUserByID(userID) if err != nil { return errors.New("用户不存在") } - // 验证旧密码 if !auth.CheckPassword(user.Password, oldPassword) { return errors.New("原密码错误") } - // 加密新密码 hashedPassword, err := auth.HashPassword(newPassword) if err != nil { return errors.New("密码加密失败") } - // 更新密码 return repository.UpdateUserFields(userID, map[string]interface{}{ "password": hashedPassword, }) @@ -166,19 +206,16 @@ func ChangeUserPassword(userID int64, oldPassword, newPassword string) error { // ResetUserPassword 重置密码(通过邮箱) func ResetUserPassword(email, newPassword string) error { - // 查找用户 user, err := repository.FindUserByEmail(email) if err != nil { return errors.New("用户不存在") } - // 加密新密码 hashedPassword, err := auth.HashPassword(newPassword) if err != nil { return errors.New("密码加密失败") } - // 更新密码 return repository.UpdateUserFields(user.ID, map[string]interface{}{ "password": hashedPassword, }) @@ -186,7 +223,6 @@ func ResetUserPassword(email, newPassword string) error { // ChangeUserEmail 更换邮箱 func ChangeUserEmail(userID int64, newEmail string) error { - // 检查新邮箱是否已被使用 existingUser, err := repository.FindUserByEmail(newEmail) if err != nil { return err @@ -195,7 +231,6 @@ func ChangeUserEmail(userID int64, newEmail string) error { return errors.New("邮箱已被其他用户使用") } - // 更新邮箱 return repository.UpdateUserFields(userID, map[string]interface{}{ "email": newEmail, }) @@ -228,18 +263,40 @@ func logFailedLogin(userID int64, ipAddress, userAgent, reason string) { // getDefaultAvatar 获取默认头像URL func getDefaultAvatar() string { - // 如果数据库中不存在默认头像配置,返回错误信息 - const log = "数据库中不存在默认头像配置" - - // 尝试从数据库读取配置 config, err := repository.GetSystemConfigByKey("default_avatar") - if err != nil || config == nil { - return log + if err != nil || config == nil || config.Value == "" { + return "" } - return config.Value } +// ValidateAvatarURL 验证头像URL是否合法 +func ValidateAvatarURL(avatarURL string) error { + if avatarURL == "" { + return nil + } + + // 允许的域名列表 + allowedDomains := []string{ + "rustfs.example.com", + "localhost", + "127.0.0.1", + } + + for _, domain := range allowedDomains { + if strings.Contains(avatarURL, domain) { + return nil + } + } + + if strings.HasPrefix(avatarURL, "/") { + return nil + } + + return errors.New("头像URL不在允许的域名列表中") +} + +// GetUserByEmail 根据邮箱获取用户 func GetUserByEmail(email string) (*model.User, error) { user, err := repository.FindUserByEmail(email) if err != nil { @@ -247,3 +304,31 @@ func GetUserByEmail(email string) (*model.User, error) { } return user, nil } + +// GetMaxProfilesPerUser 获取每用户最大档案数量配置 +func GetMaxProfilesPerUser() int { + config, err := repository.GetSystemConfigByKey("max_profiles_per_user") + if err != nil || config == nil { + return 5 + } + var value int + fmt.Sscanf(config.Value, "%d", &value) + if value <= 0 { + return 5 + } + return value +} + +// GetMaxTexturesPerUser 获取每用户最大材质数量配置 +func GetMaxTexturesPerUser() int { + config, err := repository.GetSystemConfigByKey("max_textures_per_user") + if err != nil || config == nil { + return 50 + } + var value int + fmt.Sscanf(config.Value, "%d", &value) + if value <= 0 { + return 50 + } + return value +} diff --git a/internal/service/verification_service.go b/internal/service/verification_service.go index 49bb40d..2001be7 100644 --- a/internal/service/verification_service.go +++ b/internal/service/verification_service.go @@ -91,21 +91,36 @@ func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, cod return nil } + // 检查是否被锁定 + locked, ttl, err := CheckVerifyLocked(ctx, redisClient, email, codeType) + if err == nil && locked { + return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1) + } + codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email) // 从Redis获取验证码 storedCode, err := redisClient.Get(ctx, codeKey) if err != nil { + // 记录失败尝试 + RecordVerifyFailure(ctx, redisClient, email, codeType) return fmt.Errorf("验证码已过期或不存在") } // 验证验证码 if storedCode != code { + // 记录失败尝试 + count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType) + remaining := MaxVerifyAttempts - count + if remaining > 0 { + return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining) + } return fmt.Errorf("验证码错误") } - // 验证成功,删除验证码 + // 验证成功,删除验证码和失败计数 _ = redisClient.Del(ctx, codeKey) + _ = ClearVerifyAttempts(ctx, redisClient, email, codeType) return nil } diff --git a/internal/service/yggdrasil_service.go b/internal/service/yggdrasil_service.go index 0b5d3c1..cf093c8 100644 --- a/internal/service/yggdrasil_service.go +++ b/internal/service/yggdrasil_service.go @@ -3,6 +3,7 @@ package service import ( "carrotskin/internal/model" "carrotskin/internal/repository" + "carrotskin/pkg/auth" "carrotskin/pkg/redis" "carrotskin/pkg/utils" "context" @@ -54,7 +55,8 @@ func VerifyPassword(db *gorm.DB, password string, Id int64) error { if err != nil { return errors.New("未生成密码") } - if passwordStore != password { + // 使用 bcrypt 验证密码 + if !auth.CheckPassword(passwordStore, password) { return errors.New("密码错误") } return nil @@ -81,29 +83,36 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) { // ResetYggdrasilPassword 重置并返回新的Yggdrasil密码 func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) { - // 生成新的16位随机密码 - newPassword := model.GenerateRandomPassword(16) + // 生成新的16位随机密码(明文,返回给用户) + plainPassword := model.GenerateRandomPassword(16) + + // 使用 bcrypt 加密密码后存储 + hashedPassword, err := auth.HashPassword(plainPassword) + if err != nil { + return "", fmt.Errorf("密码加密失败: %w", err) + } // 检查Yggdrasil记录是否存在 - _, err := repository.GetYggdrasilPasswordById(userId) + _, err = repository.GetYggdrasilPasswordById(userId) if err != nil { // 如果不存在,创建新记录 yggdrasil := model.Yggdrasil{ ID: userId, - Password: newPassword, + Password: hashedPassword, } if err := db.Create(&yggdrasil).Error; err != nil { return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err) } - return newPassword, nil + return plainPassword, nil } - // 如果存在,更新密码 - if err := repository.ResetYggdrasilPassword(userId, newPassword); err != nil { + // 如果存在,更新密码(存储加密后的密码) + if err := repository.ResetYggdrasilPassword(userId, hashedPassword); err != nil { return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err) } - return newPassword, nil + // 返回明文密码给用户 + return plainPassword, nil } // JoinServer 记录玩家加入服务器的会话信息 diff --git a/pkg/database/seed.go b/pkg/database/seed.go new file mode 100644 index 0000000..953bf11 --- /dev/null +++ b/pkg/database/seed.go @@ -0,0 +1,156 @@ +package database + +import ( + "carrotskin/internal/model" + + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// 默认管理员配置 +const ( + defaultAdminUsername = "admin" + defaultAdminEmail = "admin@example.com" + defaultAdminPassword = "admin123456" // 首次登录后请立即修改 +) + +// defaultSystemConfigs 默认系统配置 +var defaultSystemConfigs = []model.SystemConfig{ + {Key: "site_name", Value: "CarrotSkin", Description: "网站名称", Type: model.ConfigTypeString, IsPublic: true}, + {Key: "site_description", Value: "一个优秀的Minecraft皮肤站", Description: "网站描述", Type: model.ConfigTypeString, IsPublic: true}, + {Key: "registration_enabled", Value: "true", Description: "是否允许用户注册", Type: model.ConfigTypeBoolean, IsPublic: true}, + {Key: "checkin_reward", Value: "10", Description: "签到奖励积分", Type: model.ConfigTypeInteger, IsPublic: true}, + {Key: "texture_download_reward", Value: "1", Description: "材质被下载奖励积分", Type: model.ConfigTypeInteger, IsPublic: false}, + {Key: "max_textures_per_user", Value: "50", Description: "每个用户最大材质数量", Type: model.ConfigTypeInteger, IsPublic: false}, + {Key: "max_profiles_per_user", Value: "5", Description: "每个用户最大角色数量", Type: model.ConfigTypeInteger, IsPublic: false}, + {Key: "default_avatar", Value: "", Description: "默认头像URL", Type: model.ConfigTypeString, IsPublic: true}, +} + +// defaultCasbinRules 默认Casbin权限规则 +var defaultCasbinRules = []model.CasbinRule{ + // 管理员拥有所有权限 + {PType: "p", V0: "admin", V1: "*", V2: "*"}, + // 普通用户权限 + {PType: "p", V0: "user", V1: "texture", V2: "create"}, + {PType: "p", V0: "user", V1: "texture", V2: "read"}, + {PType: "p", V0: "user", V1: "texture", V2: "update_own"}, + {PType: "p", V0: "user", V1: "texture", V2: "delete_own"}, + {PType: "p", V0: "user", V1: "profile", V2: "create"}, + {PType: "p", V0: "user", V1: "profile", V2: "read"}, + {PType: "p", V0: "user", V1: "profile", V2: "update_own"}, + {PType: "p", V0: "user", V1: "profile", V2: "delete_own"}, + {PType: "p", V0: "user", V1: "user", V2: "update_own"}, + // 角色继承:admin 继承 user 的所有权限 + {PType: "g", V0: "admin", V1: "user"}, +} + +// Seed 初始化种子数据 +func Seed(logger *zap.Logger) error { + db, err := GetDB() + if err != nil { + return err + } + + logger.Info("开始初始化种子数据...") + + // 初始化默认管理员用户 + if err := seedAdminUser(db, logger); err != nil { + return err + } + + // 初始化系统配置 + if err := seedSystemConfigs(db, logger); err != nil { + return err + } + + // 初始化Casbin权限规则 + if err := seedCasbinRules(db, logger); err != nil { + return err + } + + logger.Info("种子数据初始化完成") + return nil +} + +// seedAdminUser 初始化默认管理员用户 +func seedAdminUser(db *gorm.DB, logger *zap.Logger) error { + // 检查是否已存在管理员用户 + var count int64 + if err := db.Model(&model.User{}).Where("role = ?", "admin").Count(&count).Error; err != nil { + logger.Error("检查管理员用户失败", zap.Error(err)) + return err + } + + // 如果已存在管理员,跳过创建 + if count > 0 { + logger.Info("管理员用户已存在,跳过创建") + return nil + } + + // 加密密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost) + if err != nil { + logger.Error("密码加密失败", zap.Error(err)) + return err + } + + // 创建默认管理员 + admin := &model.User{ + Username: defaultAdminUsername, + Email: defaultAdminEmail, + Password: string(hashedPassword), + Role: "admin", + Status: 1, + Points: 0, + } + + if err := db.Create(admin).Error; err != nil { + logger.Error("创建管理员用户失败", zap.Error(err)) + return err + } + + logger.Info("默认管理员用户创建成功", + zap.String("username", defaultAdminUsername), + zap.String("email", defaultAdminEmail), + ) + logger.Warn("请立即登录并修改默认管理员密码!默认密码请查看源码中的 defaultAdminPassword 常量") + + return nil +} + +// seedSystemConfigs 初始化系统配置 +func seedSystemConfigs(db *gorm.DB, logger *zap.Logger) error { + for _, config := range defaultSystemConfigs { + // 使用 FirstOrCreate 避免重复插入 + var existing model.SystemConfig + result := db.Where("key = ?", config.Key).First(&existing) + if result.Error == gorm.ErrRecordNotFound { + if err := db.Create(&config).Error; err != nil { + logger.Error("创建系统配置失败", zap.String("key", config.Key), zap.Error(err)) + return err + } + logger.Info("创建系统配置", zap.String("key", config.Key)) + } + } + return nil +} + +// seedCasbinRules 初始化Casbin权限规则 +func seedCasbinRules(db *gorm.DB, logger *zap.Logger) error { + for _, rule := range defaultCasbinRules { + // 检查规则是否已存在 + var existing model.CasbinRule + query := db.Where("ptype = ? AND v0 = ? AND v1 = ? AND v2 = ?", rule.PType, rule.V0, rule.V1, rule.V2) + result := query.First(&existing) + if result.Error == gorm.ErrRecordNotFound { + if err := db.Create(&rule).Error; err != nil { + logger.Error("创建Casbin规则失败", zap.String("ptype", rule.PType), zap.Error(err)) + return err + } + logger.Info("创建Casbin规则", zap.String("ptype", rule.PType), zap.String("v0", rule.V0), zap.String("v1", rule.V1)) + } + } + return nil +} + diff --git a/pkg/redis/redis.go b/pkg/redis/redis.go index c904596..4e3d69d 100644 --- a/pkg/redis/redis.go +++ b/pkg/redis/redis.go @@ -82,6 +82,11 @@ func (c *Client) Expire(ctx context.Context, key string, expiration time.Duratio return c.Client.Expire(ctx, key, expiration).Err() } +// TTL 获取键的剩余过期时间 +func (c *Client) TTL(ctx context.Context, key string) (time.Duration, error) { + return c.Client.TTL(ctx, key).Result() +} + // Incr 自增 func (c *Client) Incr(ctx context.Context, key string) (int64, error) { return c.Client.Incr(ctx, key).Result() diff --git a/scripts/carrotskin_postgres.sql b/scripts/carrotskin_postgres.sql deleted file mode 100644 index 585ffdf..0000000 --- a/scripts/carrotskin_postgres.sql +++ /dev/null @@ -1,343 +0,0 @@ --- CarrotSkin PostgreSQL 数据库初始化脚本 --- 创建数据库(可选,如果已经创建可跳过) ---CREATE DATABASE carrotskin WITH ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8'; - --- 用户表,支持积分系统和权限管理 -CREATE TABLE "user" ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(255) NOT NULL DEFAULT '' UNIQUE, - password VARCHAR(255) NOT NULL DEFAULT '', - email VARCHAR(255) NOT NULL DEFAULT '' UNIQUE, - avatar VARCHAR(255) NOT NULL DEFAULT '', - points INTEGER NOT NULL DEFAULT 0, - role VARCHAR(50) NOT NULL DEFAULT 'user', - status SMALLINT NOT NULL DEFAULT 1, - last_login_at TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_user_role ON "user"(role); -CREATE INDEX idx_user_status ON "user"(status); -CREATE INDEX idx_user_points ON "user"(points DESC); - --- 用户表注释 -COMMENT ON TABLE "user" IS '用户表'; -COMMENT ON COLUMN "user".id IS '用户ID'; -COMMENT ON COLUMN "user".username IS '用户名'; -COMMENT ON COLUMN "user".password IS '密码哈希'; -COMMENT ON COLUMN "user".email IS '邮箱地址'; -COMMENT ON COLUMN "user".avatar IS '头像URL(存储在MinIO中)'; -COMMENT ON COLUMN "user".points IS '用户积分'; -COMMENT ON COLUMN "user".role IS '用户角色(user, admin等)'; -COMMENT ON COLUMN "user".status IS '用户状态(1:正常, 0:禁用, -1:删除)'; -COMMENT ON COLUMN "user".last_login_at IS '最后登录时间'; -COMMENT ON COLUMN "user".created_at IS '创建时间'; -COMMENT ON COLUMN "user".updated_at IS '更新时间'; - --- 创建材质类型枚举 -CREATE TYPE texture_type AS ENUM ('SKIN', 'CAPE'); - --- 材质表,存储皮肤和披风 -CREATE TABLE textures ( - id BIGSERIAL PRIMARY KEY, - uploader_id BIGINT NOT NULL, - name VARCHAR(100) NOT NULL DEFAULT '', - description TEXT, - type texture_type NOT NULL, - url VARCHAR(255) NOT NULL, - hash VARCHAR(64) NOT NULL UNIQUE, - size INTEGER NOT NULL DEFAULT 0, - is_public BOOLEAN NOT NULL DEFAULT FALSE, - download_count INTEGER NOT NULL DEFAULT 0, - favorite_count INTEGER NOT NULL DEFAULT 0, - is_slim BOOLEAN NOT NULL DEFAULT FALSE, - status SMALLINT NOT NULL DEFAULT 1, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_textures_uploader FOREIGN KEY (uploader_id) REFERENCES "user"(id) ON DELETE CASCADE -); - --- 创建索引 -CREATE INDEX idx_textures_uploader_id ON textures(uploader_id); -CREATE INDEX idx_textures_public_type_status ON textures(is_public, type, status); -CREATE INDEX idx_textures_download_count ON textures(download_count DESC); -CREATE INDEX idx_textures_favorite_count ON textures(favorite_count DESC); - --- 材质表注释 -COMMENT ON TABLE textures IS '皮肤与披风材质表'; -COMMENT ON COLUMN textures.id IS '材质的唯一ID'; -COMMENT ON COLUMN textures.uploader_id IS '上传者的用户ID'; -COMMENT ON COLUMN textures.name IS '材质名称'; -COMMENT ON COLUMN textures.description IS '材质描述'; -COMMENT ON COLUMN textures.type IS '材质类型(皮肤或披风)'; -COMMENT ON COLUMN textures.url IS '材质在MinIO中的永久访问URL'; -COMMENT ON COLUMN textures.hash IS '材质文件的SHA-256哈希值,用于快速去重和校验'; -COMMENT ON COLUMN textures.size IS '文件大小(字节)'; -COMMENT ON COLUMN textures.is_public IS '是否公开到皮肤广场'; -COMMENT ON COLUMN textures.download_count IS '下载次数'; -COMMENT ON COLUMN textures.favorite_count IS '收藏次数'; -COMMENT ON COLUMN textures.is_slim IS '是否为细手臂模型(Alex),默认为粗手臂模型(Steve)'; -COMMENT ON COLUMN textures.status IS '状态(1:正常, 0:审核中, -1:已删除)'; -COMMENT ON COLUMN textures.created_at IS '创建时间'; -COMMENT ON COLUMN textures.updated_at IS '更新时间'; - --- 用户材质收藏表 -CREATE TABLE user_texture_favorites ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - texture_id BIGINT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT uk_user_texture UNIQUE (user_id, texture_id), - CONSTRAINT fk_favorites_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE, - CONSTRAINT fk_favorites_texture FOREIGN KEY (texture_id) REFERENCES textures(id) ON DELETE CASCADE -); - --- 创建索引 -CREATE INDEX idx_favorites_user_id ON user_texture_favorites(user_id); -CREATE INDEX idx_favorites_texture_id ON user_texture_favorites(texture_id); -CREATE INDEX idx_favorites_created_at ON user_texture_favorites(created_at); - --- 收藏表注释 -COMMENT ON TABLE user_texture_favorites IS '用户材质收藏表'; -COMMENT ON COLUMN user_texture_favorites.id IS '收藏记录的唯一ID'; -COMMENT ON COLUMN user_texture_favorites.user_id IS '用户ID'; -COMMENT ON COLUMN user_texture_favorites.texture_id IS '收藏的材质ID'; -COMMENT ON COLUMN user_texture_favorites.created_at IS '收藏时间'; - --- 用户角色信息表(Minecraft档案) -CREATE TABLE profiles ( - uuid VARCHAR(36) PRIMARY KEY, - user_id BIGINT NOT NULL, - name VARCHAR(16) NOT NULL UNIQUE, - skin_id BIGINT, - cape_id BIGINT, - rsa_private_key TEXT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - last_used_at TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_profiles_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE, - CONSTRAINT fk_profiles_skin FOREIGN KEY (skin_id) REFERENCES textures(id) ON DELETE SET NULL, - CONSTRAINT fk_profiles_cape FOREIGN KEY (cape_id) REFERENCES textures(id) ON DELETE SET NULL -); - --- 创建索引 -CREATE INDEX idx_profiles_user_id ON profiles(user_id); -CREATE INDEX idx_profiles_active ON profiles(is_active); - --- 档案表注释 -COMMENT ON TABLE profiles IS '用户角色信息表(Minecraft档案)'; -COMMENT ON COLUMN profiles.uuid IS '角色的UUID,通常为Minecraft玩家的UUID'; -COMMENT ON COLUMN profiles.user_id IS '关联的用户ID'; -COMMENT ON COLUMN profiles.name IS '角色名(Minecraft游戏内名称)'; -COMMENT ON COLUMN profiles.skin_id IS '当前使用的皮肤ID'; -COMMENT ON COLUMN profiles.cape_id IS '当前使用的披风ID'; -COMMENT ON COLUMN profiles.rsa_private_key IS '用于签名的RSA-2048私钥(PEM格式)'; -COMMENT ON COLUMN profiles.is_active IS '是否为活跃档案'; -COMMENT ON COLUMN profiles.last_used_at IS '最后使用时间'; -COMMENT ON COLUMN profiles.created_at IS '创建时间'; -COMMENT ON COLUMN profiles.updated_at IS '更新时间'; - --- Casbin权限管理相关表 -CREATE TABLE casbin_rule ( - id BIGSERIAL PRIMARY KEY, - ptype VARCHAR(100) NOT NULL, - v0 VARCHAR(100) NOT NULL DEFAULT '', - v1 VARCHAR(100) NOT NULL DEFAULT '', - v2 VARCHAR(100) NOT NULL DEFAULT '', - v3 VARCHAR(100) NOT NULL DEFAULT '', - v4 VARCHAR(100) NOT NULL DEFAULT '', - v5 VARCHAR(100) NOT NULL DEFAULT '', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT uk_casbin_rule UNIQUE (ptype, v0, v1, v2, v3, v4, v5) -); - --- 创建索引 -CREATE INDEX idx_casbin_ptype ON casbin_rule(ptype); -CREATE INDEX idx_casbin_v0 ON casbin_rule(v0); -CREATE INDEX idx_casbin_v1 ON casbin_rule(v1); - --- Casbin表注释 -COMMENT ON TABLE casbin_rule IS 'Casbin权限规则表'; -COMMENT ON COLUMN casbin_rule.ptype IS '策略类型(p, g等)'; -COMMENT ON COLUMN casbin_rule.v0 IS '主体(用户或角色)'; -COMMENT ON COLUMN casbin_rule.v1 IS '资源对象'; -COMMENT ON COLUMN casbin_rule.v2 IS '操作动作'; - --- 创建变更类型枚举 -CREATE TYPE point_change_type AS ENUM ('EARN', 'SPEND', 'ADMIN_ADJUST'); - --- 用户积分变更记录表 -CREATE TABLE user_point_logs ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - change_type point_change_type NOT NULL, - amount INTEGER NOT NULL, - balance_before INTEGER NOT NULL, - balance_after INTEGER NOT NULL, - reason VARCHAR(255) NOT NULL, - reference_type VARCHAR(50), - reference_id BIGINT, - operator_id BIGINT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_point_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE, - CONSTRAINT fk_point_logs_operator FOREIGN KEY (operator_id) REFERENCES "user"(id) ON DELETE SET NULL -); - --- 创建索引 -CREATE INDEX idx_point_logs_user_id ON user_point_logs(user_id); -CREATE INDEX idx_point_logs_created_at ON user_point_logs(created_at DESC); -CREATE INDEX idx_point_logs_change_type ON user_point_logs(change_type); - --- 积分日志表注释 -COMMENT ON TABLE user_point_logs IS '用户积分变更记录表'; - --- 创建配置类型枚举 -CREATE TYPE config_type AS ENUM ('STRING', 'INTEGER', 'BOOLEAN', 'JSON'); - --- 系统配置表 -CREATE TABLE system_config ( - id BIGSERIAL PRIMARY KEY, - key VARCHAR(100) NOT NULL UNIQUE, - value TEXT NOT NULL, - description VARCHAR(255) NOT NULL DEFAULT '', - type config_type NOT NULL DEFAULT 'STRING', - is_public BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_system_config_public ON system_config(is_public); - --- 系统配置表注释 -COMMENT ON TABLE system_config IS '系统配置表'; - --- 用户登录日志表 -CREATE TABLE user_login_logs ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - ip_address INET NOT NULL, - user_agent TEXT, - login_method VARCHAR(50) NOT NULL DEFAULT 'PASSWORD', - is_success BOOLEAN NOT NULL, - failure_reason VARCHAR(255), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_login_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE -); - --- 创建索引 -CREATE INDEX idx_login_logs_user_id ON user_login_logs(user_id); -CREATE INDEX idx_login_logs_created_at ON user_login_logs(created_at DESC); -CREATE INDEX idx_login_logs_ip_address ON user_login_logs(ip_address); -CREATE INDEX idx_login_logs_success ON user_login_logs(is_success); - --- 登录日志表注释 -COMMENT ON TABLE user_login_logs IS '用户登录日志表'; - --- 审计日志表 -CREATE TABLE audit_logs ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT, - action VARCHAR(100) NOT NULL, - resource_type VARCHAR(50) NOT NULL, - resource_id VARCHAR(50), - old_values JSONB, - new_values JSONB, - ip_address INET NOT NULL, - user_agent TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_audit_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE SET NULL -); - --- 创建索引 -CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); -CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC); -CREATE INDEX idx_audit_logs_action ON audit_logs(action); -CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id); - --- 审计日志表注释 -COMMENT ON TABLE audit_logs IS '审计日志表'; - --- 材质下载记录表(用于统计和防刷) -CREATE TABLE texture_download_logs ( - id BIGSERIAL PRIMARY KEY, - texture_id BIGINT NOT NULL, - user_id BIGINT, - ip_address INET NOT NULL, - user_agent TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT fk_download_logs_texture FOREIGN KEY (texture_id) REFERENCES textures(id) ON DELETE CASCADE, - CONSTRAINT fk_download_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE SET NULL -); - --- 创建索引 -CREATE INDEX idx_download_logs_texture_id ON texture_download_logs(texture_id); -CREATE INDEX idx_download_logs_user_id ON texture_download_logs(user_id); -CREATE INDEX idx_download_logs_created_at ON texture_download_logs(created_at DESC); -CREATE INDEX idx_download_logs_ip_address ON texture_download_logs(ip_address); - --- 下载记录表注释 -COMMENT ON TABLE texture_download_logs IS '材质下载记录表'; - --- 创建更新时间触发器函数 -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ language 'plpgsql'; - --- 为需要的表添加更新时间触发器 -CREATE TRIGGER update_user_updated_at BEFORE UPDATE ON "user" - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_textures_updated_at BEFORE UPDATE ON textures - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON profiles - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- 插入默认的系统配置 -INSERT INTO system_config (key, value, description, type, is_public) VALUES -('site_name', 'CarrotSkin', '网站名称', 'STRING', TRUE), -('site_description', '一个优秀的Minecraft皮肤站', '网站描述', 'STRING', TRUE), -('registration_enabled', 'true', '是否允许用户注册', 'BOOLEAN', TRUE), -('checkin_reward', '10', '签到奖励积分', 'INTEGER', TRUE), -('texture_download_reward', '1', '材质被下载奖励积分', 'INTEGER', FALSE), -('max_textures_per_user', '50', '每个用户最大材质数量', 'INTEGER', FALSE), -('max_profiles_per_user', '5', '每个用户最大角色数量', 'INTEGER', FALSE), -('default_avatar', 'https://carrotskin.com/assets/images/default-avatar.png', '默认头像', 'STRING', TRUE); - --- 插入默认的Casbin权限规则 -INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES -('p', 'admin', '*', '*'), -('p', 'user', 'texture', 'create'), -('p', 'user', 'texture', 'read'), -('p', 'user', 'texture', 'update_own'), -('p', 'user', 'texture', 'delete_own'), -('p', 'user', 'profile', 'create'), -('p', 'user', 'profile', 'read'), -('p', 'user', 'profile', 'update_own'), -('p', 'user', 'profile', 'delete_own'), -('p', 'user', 'user', 'update_own'), -('g', 'admin', 'user', ''); - --- 插入默认的管理员 -INSERT INTO "user" (username, password, email, is_admin, created_at, updated_at) VALUES -('admin', '$2a$10$...', 'admin@example.com', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); -