feat: 添加种子数据初始化功能,重构多个处理程序以简化错误响应和用户验证

This commit is contained in:
lan
2025-12-02 10:33:19 +08:00
parent bdd2be5dc5
commit 10fdcd916b
30 changed files with 1291 additions and 1778 deletions

View File

@@ -49,6 +49,11 @@ func main() {
loggerInstance.Fatal("数据库迁移失败", zap.Error(err)) loggerInstance.Fatal("数据库迁移失败", zap.Error(err))
} }
// 初始化种子数据
if err := database.Seed(loggerInstance); err != nil {
loggerInstance.Fatal("种子数据初始化失败", zap.Error(err))
}
// 初始化JWT服务 // 初始化JWT服务
if err := auth.Init(cfg.JWT); err != nil { if err := auth.Init(cfg.JWT); err != nil {
loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err)) loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err))

View File

@@ -1,14 +1,12 @@
package handler package handler
import ( import (
"carrotskin/internal/model"
"carrotskin/internal/service" "carrotskin/internal/service"
"carrotskin/internal/types" "carrotskin/internal/types"
"carrotskin/pkg/auth" "carrotskin/pkg/auth"
"carrotskin/pkg/email" "carrotskin/pkg/email"
"carrotskin/pkg/logger" "carrotskin/pkg/logger"
"carrotskin/pkg/redis" "carrotskin/pkg/redis"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -31,56 +29,29 @@ func Register(c *gin.Context) {
var req types.RegisterRequest var req types.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 验证邮箱验证码 // 验证邮箱验证码
if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeRegister); err != nil { if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeRegister); err != nil {
loggerInstance.Warn("验证码验证失败", loggerInstance.Warn("验证码验证失败", zap.String("email", req.Email), zap.Error(err))
zap.String("email", req.Email), RespondBadRequest(c, err.Error(), nil)
zap.Error(err),
)
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 调用service层注册用户传递可选的头像URL // 注册用户
user, token, err := service.RegisterUser(jwtService, req.Username, req.Password, req.Email, req.Avatar) user, token, err := service.RegisterUser(jwtService, req.Username, req.Password, req.Email, req.Avatar)
if err != nil { if err != nil {
loggerInstance.Error("用户注册失败", zap.Error(err)) loggerInstance.Error("用户注册失败", zap.Error(err))
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 返回响应 RespondSuccess(c, &types.LoginResponse{
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.LoginResponse{
Token: token, Token: token,
UserInfo: &types.UserInfo{ UserInfo: UserToUserInfo(user),
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,
},
}))
} }
// Login 用户登录 // Login 用户登录
@@ -97,53 +68,32 @@ func Register(c *gin.Context) {
func Login(c *gin.Context) { func Login(c *gin.Context) {
loggerInstance := logger.MustGetLogger() loggerInstance := logger.MustGetLogger()
jwtService := auth.MustGetJWTService() jwtService := auth.MustGetJWTService()
redisClient := redis.MustGetClient()
var req types.LoginRequest var req types.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 获取IP和UserAgent
ipAddress := c.ClientIP() ipAddress := c.ClientIP()
userAgent := c.GetHeader("User-Agent") userAgent := c.GetHeader("User-Agent")
// 调用service层登录 user, token, err := service.LoginUserWithRateLimit(redisClient, jwtService, req.Username, req.Password, ipAddress, userAgent)
user, token, err := service.LoginUser(jwtService, req.Username, req.Password, ipAddress, userAgent)
if err != nil { if err != nil {
loggerInstance.Warn("用户登录失败", loggerInstance.Warn("用户登录失败",
zap.String("username_or_email", req.Username), zap.String("username_or_email", req.Username),
zap.String("ip", ipAddress), zap.String("ip", ipAddress),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusUnauthorized, model.NewErrorResponse( RespondUnauthorized(c, err.Error())
model.CodeUnauthorized,
err.Error(),
nil,
))
return return
} }
// 返回响应 RespondSuccess(c, &types.LoginResponse{
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.LoginResponse{
Token: token, Token: token,
UserInfo: &types.UserInfo{ UserInfo: UserToUserInfo(user),
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,
},
}))
} }
// SendVerificationCode 发送验证码 // SendVerificationCode 发送验证码
@@ -163,32 +113,21 @@ func SendVerificationCode(c *gin.Context) {
var req types.SendVerificationCodeRequest var req types.SendVerificationCodeRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 发送验证码
if err := service.SendVerificationCode(c.Request.Context(), redisClient, emailService, req.Email, req.Type); err != nil { if err := service.SendVerificationCode(c.Request.Context(), redisClient, emailService, req.Email, req.Type); err != nil {
loggerInstance.Error("发送验证码失败", loggerInstance.Error("发送验证码失败",
zap.String("email", req.Email), zap.String("email", req.Email),
zap.String("type", req.Type), zap.String("type", req.Type),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ RespondSuccess(c, gin.H{"message": "验证码已发送,请查收邮件"})
"message": "验证码已发送,请查收邮件",
}))
} }
// ResetPassword 重置密码 // ResetPassword 重置密码
@@ -207,43 +146,23 @@ func ResetPassword(c *gin.Context) {
var req types.ResetPasswordRequest var req types.ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 验证验证码 // 验证验证码
if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeResetPassword); err != nil { if err := service.VerifyCode(c.Request.Context(), redisClient, req.Email, req.VerificationCode, service.VerificationTypeResetPassword); err != nil {
loggerInstance.Warn("验证码验证失败", loggerInstance.Warn("验证码验证失败", zap.String("email", req.Email), zap.Error(err))
zap.String("email", req.Email), RespondBadRequest(c, err.Error(), nil)
zap.Error(err),
)
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 重置密码 // 重置密码
if err := service.ResetUserPassword(req.Email, req.NewPassword); err != nil { if err := service.ResetUserPassword(req.Email, req.NewPassword); err != nil {
loggerInstance.Error("重置密码失败", loggerInstance.Error("重置密码失败", zap.String("email", req.Email), zap.Error(err))
zap.String("email", req.Email), RespondServerError(c, err.Error(), nil)
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ RespondSuccess(c, gin.H{"message": "密码重置成功"})
"message": "密码重置成功",
}))
} }

160
internal/handler/helpers.go Normal file
View File

@@ -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)
}
}

View File

@@ -1,12 +1,10 @@
package handler package handler
import ( import (
"carrotskin/internal/model"
"carrotskin/internal/service" "carrotskin/internal/service"
"carrotskin/internal/types" "carrotskin/internal/types"
"carrotskin/pkg/database" "carrotskin/pkg/database"
"carrotskin/pkg/logger" "carrotskin/pkg/logger"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -26,70 +24,37 @@ import (
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile [post] // @Router /api/v1/profile [post]
func CreateProfile(c *gin.Context) { func CreateProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
// 获取用户ID if !ok {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
"未授权",
nil,
))
return return
} }
// 解析请求
var req types.CreateProfileRequest var req types.CreateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误: "+err.Error(), nil)
model.CodeBadRequest,
"请求参数错误: "+err.Error(),
nil,
))
return return
} }
// TODO: 从配置或数据库读取限制 maxProfiles := service.GetMaxProfilesPerUser()
maxProfiles := 5
db := database.MustGetDB() db := database.MustGetDB()
// 检查档案数量限制
if err := service.CheckProfileLimit(db, userID.(int64), maxProfiles); err != nil { if err := service.CheckProfileLimit(db, userID, maxProfiles); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 创建档案 profile, err := service.CreateProfile(db, userID, req.Name)
profile, err := service.CreateProfile(db, userID.(int64), req.Name)
if err != nil { if err != nil {
loggerInstance.Error("创建档案失败", logger.MustGetLogger().Error("创建档案失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.String("name", req.Name), zap.String("name", req.Name),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, model.NewErrorResponse( RespondServerError(c, err.Error(), nil)
model.CodeServerError,
err.Error(),
nil,
))
return return
} }
// 返回成功响应 RespondSuccess(c, ProfileToProfileInfo(profile))
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,
}))
} }
// GetProfiles 获取档案列表 // GetProfiles 获取档案列表
@@ -104,50 +69,22 @@ func CreateProfile(c *gin.Context) {
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile [get] // @Router /api/v1/profile [get]
func GetProfiles(c *gin.Context) { func GetProfiles(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
// 获取用户ID if !ok {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
"未授权",
nil,
))
return return
} }
// 查询档案列表 profiles, err := service.GetUserProfiles(database.MustGetDB(), userID)
profiles, err := service.GetUserProfiles(database.MustGetDB(), userID.(int64))
if err != nil { if err != nil {
loggerInstance.Error("获取档案列表失败", logger.MustGetLogger().Error("获取档案列表失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, model.NewErrorResponse( RespondServerError(c, err.Error(), nil)
model.CodeServerError,
err.Error(),
nil,
))
return return
} }
// 转换为响应格式 RespondSuccess(c, ProfilesToProfileInfos(profiles))
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))
} }
// GetProfile 获取档案详情 // GetProfile 获取档案详情
@@ -162,36 +99,19 @@ func GetProfiles(c *gin.Context) {
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile/{uuid} [get] // @Router /api/v1/profile/{uuid} [get]
func GetProfile(c *gin.Context) { func GetProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger()
uuid := c.Param("uuid") uuid := c.Param("uuid")
// 查询档案
profile, err := service.GetProfileByUUID(database.MustGetDB(), uuid) profile, err := service.GetProfileByUUID(database.MustGetDB(), uuid)
if err != nil { if err != nil {
loggerInstance.Error("获取档案失败", logger.MustGetLogger().Error("获取档案失败",
zap.String("uuid", uuid), zap.String("uuid", uuid),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, err.Error())
model.CodeNotFound,
err.Error(),
nil,
))
return return
} }
// 返回成功响应 RespondSuccess(c, ProfileToProfileInfo(profile))
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,
}))
} }
// UpdateProfile 更新档案 // UpdateProfile 更新档案
@@ -211,72 +131,36 @@ func GetProfile(c *gin.Context) {
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile/{uuid} [put] // @Router /api/v1/profile/{uuid} [put]
func UpdateProfile(c *gin.Context) { func UpdateProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
uuid := c.Param("uuid") 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 var req types.UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误: "+err.Error(), nil)
model.CodeBadRequest,
"请求参数错误: "+err.Error(),
nil,
))
return return
} }
// 更新档案
var namePtr *string var namePtr *string
if req.Name != "" { if req.Name != "" {
namePtr = &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 { if err != nil {
loggerInstance.Error("更新档案失败", logger.MustGetLogger().Error("更新档案失败",
zap.String("uuid", uuid), zap.String("uuid", uuid),
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
) )
RespondWithError(c, 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,
))
return return
} }
// 返回成功响应 RespondSuccess(c, ProfileToProfileInfo(profile))
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,
}))
} }
// DeleteProfile 删除档案 // DeleteProfile 删除档案
@@ -294,48 +178,25 @@ func UpdateProfile(c *gin.Context) {
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile/{uuid} [delete] // @Router /api/v1/profile/{uuid} [delete]
func DeleteProfile(c *gin.Context) { func DeleteProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
uuid := c.Param("uuid") uuid := c.Param("uuid")
// 获取用户ID err := service.DeleteProfile(database.MustGetDB(), uuid, userID)
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))
if err != nil { if err != nil {
loggerInstance.Error("删除档案失败", logger.MustGetLogger().Error("删除档案失败",
zap.String("uuid", uuid), zap.String("uuid", uuid),
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
) )
RespondWithError(c, 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,
))
return return
} }
// 返回成功响应 RespondSuccess(c, gin.H{"message": "删除成功"})
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"message": "删除成功",
}))
} }
// SetActiveProfile 设置活跃档案 // SetActiveProfile 设置活跃档案
@@ -353,46 +214,23 @@ func DeleteProfile(c *gin.Context) {
// @Failure 500 {object} model.ErrorResponse "服务器错误" // @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile/{uuid}/activate [post] // @Router /api/v1/profile/{uuid}/activate [post]
func SetActiveProfile(c *gin.Context) { func SetActiveProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
uuid := c.Param("uuid") uuid := c.Param("uuid")
// 获取用户ID err := service.SetActiveProfile(database.MustGetDB(), uuid, userID)
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))
if err != nil { if err != nil {
loggerInstance.Error("设置活跃档案失败", logger.MustGetLogger().Error("设置活跃档案失败",
zap.String("uuid", uuid), zap.String("uuid", uuid),
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
) )
RespondWithError(c, 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,
))
return return
} }
// 返回成功响应 RespondSuccess(c, gin.H{"message": "设置成功"})
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"message": "设置成功",
}))
} }

View File

@@ -8,7 +8,6 @@ import (
"carrotskin/pkg/database" "carrotskin/pkg/database"
"carrotskin/pkg/logger" "carrotskin/pkg/logger"
"carrotskin/pkg/storage" "carrotskin/pkg/storage"
"net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -27,59 +26,44 @@ import (
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/texture/upload-url [post] // @Router /api/v1/texture/upload-url [post]
func GenerateTextureUploadURL(c *gin.Context) { func GenerateTextureUploadURL(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
var req types.GenerateTextureUploadURLRequest var req types.GenerateTextureUploadURLRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 调用UploadService生成预签名URL
storageClient := storage.MustGetClient() storageClient := storage.MustGetClient()
cfg := *config.MustGetRustFSConfig() cfg := *config.MustGetRustFSConfig()
result, err := service.GenerateTextureUploadURL( result, err := service.GenerateTextureUploadURL(
c.Request.Context(), c.Request.Context(),
storageClient, storageClient,
cfg, cfg,
userID.(int64), userID,
req.FileName, req.FileName,
string(req.TextureType), string(req.TextureType),
) )
if err != nil { if err != nil {
logger.MustGetLogger().Error("生成材质上传URL失败", logger.MustGetLogger().Error("生成材质上传URL失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.String("file_name", req.FileName), zap.String("file_name", req.FileName),
zap.String("texture_type", string(req.TextureType)), zap.String("texture_type", string(req.TextureType)),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 返回响应 RespondSuccess(c, &types.GenerateTextureUploadURLResponse{
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.GenerateTextureUploadURLResponse{
PostURL: result.PostURL, PostURL: result.PostURL,
FormData: result.FormData, FormData: result.FormData,
TextureURL: result.FileURL, TextureURL: result.FileURL,
ExpiresIn: 900, // 15分钟 = 900秒 ExpiresIn: 900,
})) })
} }
// CreateTexture 创建材质记录 // CreateTexture 创建材质记录
@@ -94,40 +78,25 @@ func GenerateTextureUploadURL(c *gin.Context) {
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/texture [post] // @Router /api/v1/texture [post]
func CreateTexture(c *gin.Context) { func CreateTexture(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
var req types.CreateTextureRequest var req types.CreateTextureRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// TODO: 从配置或数据库读取限制 maxTextures := service.GetMaxTexturesPerUser()
maxTextures := 100 if err := service.CheckTextureUploadLimit(database.MustGetDB(), userID, maxTextures); err != nil {
if err := service.CheckTextureUploadLimit(database.MustGetDB(), userID.(int64), maxTextures); err != nil { RespondBadRequest(c, err.Error(), nil)
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 创建材质
texture, err := service.CreateTexture(database.MustGetDB(), texture, err := service.CreateTexture(database.MustGetDB(),
userID.(int64), userID,
req.Name, req.Name,
req.Description, req.Description,
string(req.Type), string(req.Type),
@@ -139,36 +108,15 @@ func CreateTexture(c *gin.Context) {
) )
if err != nil { if err != nil {
logger.MustGetLogger().Error("创建材质失败", logger.MustGetLogger().Error("创建材质失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.String("name", req.Name), zap.String("name", req.Name),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 返回响应 RespondSuccess(c, TextureToTextureInfo(texture))
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,
}))
} }
// GetTexture 获取材质详情 // GetTexture 获取材质详情
@@ -182,44 +130,19 @@ func CreateTexture(c *gin.Context) {
// @Failure 404 {object} model.ErrorResponse "材质不存在" // @Failure 404 {object} model.ErrorResponse "材质不存在"
// @Router /api/v1/texture/{id} [get] // @Router /api/v1/texture/{id} [get]
func GetTexture(c *gin.Context) { func GetTexture(c *gin.Context) {
idStr := c.Param("id") id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "无效的材质ID", err)
model.CodeBadRequest,
"无效的材质ID",
err,
))
return return
} }
texture, err := service.GetTextureByID(database.MustGetDB(), id) texture, err := service.GetTextureByID(database.MustGetDB(), id)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, err.Error())
model.CodeNotFound,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ RespondSuccess(c, TextureToTextureInfo(texture))
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,
}))
} }
// SearchTextures 搜索材质 // SearchTextures 搜索材质
@@ -253,41 +176,12 @@ func SearchTextures(c *gin.Context) {
textures, total, err := service.SearchTextures(database.MustGetDB(), keyword, textureType, publicOnly, page, pageSize) textures, total, err := service.SearchTextures(database.MustGetDB(), keyword, textureType, publicOnly, page, pageSize)
if err != nil { if err != nil {
logger.MustGetLogger().Error("搜索材质失败", logger.MustGetLogger().Error("搜索材质失败", zap.String("keyword", keyword), zap.Error(err))
zap.String("keyword", keyword), RespondServerError(c, "搜索材质失败", err)
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
"搜索材质失败",
err,
))
return return
} }
// 转换为TextureInfo c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize))
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))
} }
// UpdateTexture 更新材质 // UpdateTexture 更新材质
@@ -303,69 +197,35 @@ func SearchTextures(c *gin.Context) {
// @Failure 403 {object} model.ErrorResponse "无权操作" // @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [put] // @Router /api/v1/texture/{id} [put]
func UpdateTexture(c *gin.Context) { func UpdateTexture(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
idStr := c.Param("id") textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
textureID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "无效的材质ID", err)
model.CodeBadRequest,
"无效的材质ID",
err,
))
return return
} }
var req types.UpdateTextureRequest var req types.UpdateTextureRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return 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 { if err != nil {
logger.MustGetLogger().Error("更新材质失败", logger.MustGetLogger().Error("更新材质失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Int64("texture_id", textureID), zap.Int64("texture_id", textureID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusForbidden, model.NewErrorResponse( RespondForbidden(c, err.Error())
model.CodeForbidden,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ RespondSuccess(c, TextureToTextureInfo(texture))
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,
}))
} }
// DeleteTexture 删除材质 // DeleteTexture 删除材质
@@ -380,42 +240,28 @@ func UpdateTexture(c *gin.Context) {
// @Failure 403 {object} model.ErrorResponse "无权操作" // @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [delete] // @Router /api/v1/texture/{id} [delete]
func DeleteTexture(c *gin.Context) { func DeleteTexture(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
idStr := c.Param("id") textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
textureID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "无效的材质ID", err)
model.CodeBadRequest,
"无效的材质ID",
err,
))
return 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("删除材质失败", logger.MustGetLogger().Error("删除材质失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Int64("texture_id", textureID), zap.Int64("texture_id", textureID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusForbidden, model.NewErrorResponse( RespondForbidden(c, err.Error())
model.CodeForbidden,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(nil)) RespondSuccess(c, nil)
} }
// ToggleFavorite 切换收藏状态 // ToggleFavorite 切换收藏状态
@@ -429,45 +275,29 @@ func DeleteTexture(c *gin.Context) {
// @Success 200 {object} model.Response "切换成功" // @Success 200 {object} model.Response "切换成功"
// @Router /api/v1/texture/{id}/favorite [post] // @Router /api/v1/texture/{id}/favorite [post]
func ToggleFavorite(c *gin.Context) { func ToggleFavorite(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
idStr := c.Param("id") textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
textureID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "无效的材质ID", err)
model.CodeBadRequest,
"无效的材质ID",
err,
))
return return
} }
isFavorited, err := service.ToggleTextureFavorite(database.MustGetDB(), userID.(int64), textureID) isFavorited, err := service.ToggleTextureFavorite(database.MustGetDB(), userID, textureID)
if err != nil { if err != nil {
logger.MustGetLogger().Error("切换收藏状态失败", logger.MustGetLogger().Error("切换收藏状态失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Int64("texture_id", textureID), zap.Int64("texture_id", textureID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]bool{ RespondSuccess(c, map[string]bool{"is_favorited": isFavorited})
"is_favorited": isFavorited,
}))
} }
// GetUserTextures 获取用户上传的材质列表 // GetUserTextures 获取用户上传的材质列表
@@ -482,56 +312,22 @@ func ToggleFavorite(c *gin.Context) {
// @Success 200 {object} model.PaginationResponse "获取成功" // @Success 200 {object} model.PaginationResponse "获取成功"
// @Router /api/v1/texture/my [get] // @Router /api/v1/texture/my [get]
func GetUserTextures(c *gin.Context) { func GetUserTextures(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) 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 { if err != nil {
logger.MustGetLogger().Error("获取用户材质列表失败", logger.MustGetLogger().Error("获取用户材质列表失败", zap.Int64("user_id", userID), zap.Error(err))
zap.Int64("user_id", userID.(int64)), RespondServerError(c, "获取材质列表失败", err)
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
"获取材质列表失败",
err,
))
return return
} }
// 转换为TextureInfo c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize))
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))
} }
// GetUserFavorites 获取用户收藏的材质列表 // GetUserFavorites 获取用户收藏的材质列表
@@ -546,54 +342,20 @@ func GetUserTextures(c *gin.Context) {
// @Success 200 {object} model.PaginationResponse "获取成功" // @Success 200 {object} model.PaginationResponse "获取成功"
// @Router /api/v1/texture/favorites [get] // @Router /api/v1/texture/favorites [get]
func GetUserFavorites(c *gin.Context) { func GetUserFavorites(c *gin.Context) {
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) 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 { if err != nil {
logger.MustGetLogger().Error("获取用户收藏列表失败", logger.MustGetLogger().Error("获取用户收藏列表失败", zap.Int64("user_id", userID), zap.Error(err))
zap.Int64("user_id", userID.(int64)), RespondServerError(c, "获取收藏列表失败", err)
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
"获取收藏列表失败",
err,
))
return return
} }
// 转换为TextureInfo c.JSON(200, model.NewPaginationResponse(TexturesToTextureInfos(textures), total, page, pageSize))
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))
} }

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"carrotskin/internal/model"
"carrotskin/internal/service" "carrotskin/internal/service"
"carrotskin/internal/types" "carrotskin/internal/types"
"carrotskin/pkg/config" "carrotskin/pkg/config"
@@ -9,7 +8,6 @@ import (
"carrotskin/pkg/logger" "carrotskin/pkg/logger"
"carrotskin/pkg/redis" "carrotskin/pkg/redis"
"carrotskin/pkg/storage" "carrotskin/pkg/storage"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -26,46 +24,22 @@ import (
// @Failure 401 {object} model.ErrorResponse "未授权" // @Failure 401 {object} model.ErrorResponse "未授权"
// @Router /api/v1/user/profile [get] // @Router /api/v1/user/profile [get]
func GetUserProfile(c *gin.Context) { func GetUserProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
// 从上下文获取用户ID (由JWT中间件设置) if !ok {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
// 获取用户信息 user, err := service.GetUserByID(userID)
user, err := service.GetUserByID(userID.(int64))
if err != nil || user == nil { if err != nil || user == nil {
loggerInstance.Error("获取用户信息失败", logger.MustGetLogger().Error("获取用户信息失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, "用户不存在")
model.CodeNotFound,
"用户不存在",
err,
))
return return
} }
// 返回用户信息 RespondSuccess(c, UserToUserInfo(user))
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,
}))
} }
// UpdateUserProfile 更新用户信息 // UpdateUserProfile 更新用户信息
@@ -84,113 +58,62 @@ func GetUserProfile(c *gin.Context) {
// @Router /api/v1/user/profile [put] // @Router /api/v1/user/profile [put]
func UpdateUserProfile(c *gin.Context) { func UpdateUserProfile(c *gin.Context) {
loggerInstance := logger.MustGetLogger() loggerInstance := logger.MustGetLogger()
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
var req types.UpdateUserRequest var req types.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 获取用户 user, err := service.GetUserByID(userID)
user, err := service.GetUserByID(userID.(int64))
if err != nil || user == nil { if err != nil || user == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, "用户不存在")
model.CodeNotFound,
"用户不存在",
err,
))
return return
} }
// 处理密码修改 // 处理密码修改
if req.NewPassword != "" { if req.NewPassword != "" {
// 如果提供了新密码,必须同时提供旧密码
if req.OldPassword == "" { if req.OldPassword == "" {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "修改密码需要提供原密码", nil)
model.CodeBadRequest,
"修改密码需要提供原密码",
nil,
))
return return
} }
// 调用修改密码服务 if err := service.ChangeUserPassword(userID, req.OldPassword, req.NewPassword); err != nil {
if err := service.ChangeUserPassword(userID.(int64), req.OldPassword, req.NewPassword); err != nil { loggerInstance.Error("修改密码失败", zap.Int64("user_id", userID), zap.Error(err))
loggerInstance.Error("修改密码失败", RespondBadRequest(c, err.Error(), nil)
zap.Int64("user_id", userID.(int64)),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
loggerInstance.Info("用户修改密码成功", loggerInstance.Info("用户修改密码成功", zap.Int64("user_id", userID))
zap.Int64("user_id", userID.(int64)),
)
} }
// 更新头像 // 更新头像
if req.Avatar != "" { if req.Avatar != "" {
user.Avatar = 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 { if err := service.UpdateUserInfo(user); err != nil {
loggerInstance.Error("更新用户信息失败", loggerInstance.Error("更新用户信息失败", zap.Int64("user_id", user.ID), zap.Error(err))
zap.Int64("user_id", user.ID), RespondServerError(c, "更新失败", err)
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
"更新失败",
err,
))
return return
} }
} }
// 重新获取更新后的用户信息 // 重新获取更新后的用户信息
updatedUser, err := service.GetUserByID(userID.(int64)) updatedUser, err := service.GetUserByID(userID)
if err != nil || updatedUser == nil { if err != nil || updatedUser == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, "用户不存在")
model.CodeNotFound,
"用户不存在",
err,
))
return return
} }
// 返回更新后的用户信息 RespondSuccess(c, UserToUserInfo(updatedUser))
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,
}))
} }
// GenerateAvatarUploadURL 生成头像上传URL // GenerateAvatarUploadURL 生成头像上传URL
@@ -205,52 +128,36 @@ func UpdateUserProfile(c *gin.Context) {
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/user/avatar/upload-url [post] // @Router /api/v1/user/avatar/upload-url [post]
func GenerateAvatarUploadURL(c *gin.Context) { func GenerateAvatarUploadURL(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
userID, exists := c.Get("user_id") if !ok {
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
var req types.GenerateAvatarUploadURLRequest var req types.GenerateAvatarUploadURLRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 调用UploadService生成预签名URL
storageClient := storage.MustGetClient() storageClient := storage.MustGetClient()
cfg := *config.MustGetRustFSConfig() 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 { if err != nil {
loggerInstance.Error("生成头像上传URL失败", logger.MustGetLogger().Error("生成头像上传URL失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.String("file_name", req.FileName), zap.String("file_name", req.FileName),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 返回响应 RespondSuccess(c, &types.GenerateAvatarUploadURLResponse{
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.GenerateAvatarUploadURLResponse{
PostURL: result.PostURL, PostURL: result.PostURL,
FormData: result.FormData, FormData: result.FormData,
AvatarURL: result.FileURL, AvatarURL: result.FileURL,
ExpiresIn: 900, // 15分钟 = 900秒 ExpiresIn: 900,
})) })
} }
// UpdateAvatar 更新头像URL // UpdateAvatar 更新头像URL
@@ -265,65 +172,39 @@ func GenerateAvatarUploadURL(c *gin.Context) {
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/user/avatar [put] // @Router /api/v1/user/avatar [put]
func UpdateAvatar(c *gin.Context) { func UpdateAvatar(c *gin.Context) {
loggerInstance := logger.MustGetLogger() userID, ok := GetUserIDFromContext(c)
userID, exists := c.Get("user_id") if !ok {
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
avatarURL := c.Query("avatar_url") avatarURL := c.Query("avatar_url")
if avatarURL == "" { if avatarURL == "" {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "头像URL不能为空", nil)
model.CodeBadRequest,
"头像URL不能为空",
nil,
))
return return
} }
// 更新头像 if err := service.ValidateAvatarURL(avatarURL); err != nil {
if err := service.UpdateUserAvatar(userID.(int64), avatarURL); err != nil { RespondBadRequest(c, err.Error(), nil)
loggerInstance.Error("更新头像失败", return
zap.Int64("user_id", userID.(int64)), }
if err := service.UpdateUserAvatar(userID, avatarURL); err != nil {
logger.MustGetLogger().Error("更新头像失败",
zap.Int64("user_id", userID),
zap.String("avatar_url", avatarURL), zap.String("avatar_url", avatarURL),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, model.NewErrorResponse( RespondServerError(c, "更新头像失败", err)
model.CodeServerError,
"更新头像失败",
err,
))
return return
} }
// 获取更新后的用户信息 user, err := service.GetUserByID(userID)
user, err := service.GetUserByID(userID.(int64))
if err != nil || user == nil { if err != nil || user == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, "用户不存在")
model.CodeNotFound,
"用户不存在",
err,
))
return return
} }
// 返回更新后的用户信息 RespondSuccess(c, UserToUserInfo(user))
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,
}))
} }
// ChangeEmail 更换邮箱 // ChangeEmail 更换邮箱
@@ -340,79 +221,41 @@ func UpdateAvatar(c *gin.Context) {
// @Router /api/v1/user/change-email [post] // @Router /api/v1/user/change-email [post]
func ChangeEmail(c *gin.Context) { func ChangeEmail(c *gin.Context) {
loggerInstance := logger.MustGetLogger() loggerInstance := logger.MustGetLogger()
userID, exists := c.Get("user_id") userID, ok := GetUserIDFromContext(c)
if !exists { if !ok {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
model.MsgUnauthorized,
nil,
))
return return
} }
var req types.ChangeEmailRequest var req types.ChangeEmailRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, "请求参数错误", err)
model.CodeBadRequest,
"请求参数错误",
err,
))
return return
} }
// 验证验证码
redisClient := redis.MustGetClient() redisClient := redis.MustGetClient()
if err := service.VerifyCode(c.Request.Context(), redisClient, req.NewEmail, req.VerificationCode, service.VerificationTypeChangeEmail); err != nil { if err := service.VerifyCode(c.Request.Context(), redisClient, req.NewEmail, req.VerificationCode, service.VerificationTypeChangeEmail); err != nil {
loggerInstance.Warn("验证码验证失败", loggerInstance.Warn("验证码验证失败", zap.String("new_email", req.NewEmail), zap.Error(err))
zap.String("new_email", req.NewEmail), RespondBadRequest(c, err.Error(), nil)
zap.Error(err),
)
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 更换邮箱 if err := service.ChangeUserEmail(userID, req.NewEmail); err != nil {
if err := service.ChangeUserEmail(userID.(int64), req.NewEmail); err != nil {
loggerInstance.Error("更换邮箱失败", loggerInstance.Error("更换邮箱失败",
zap.Int64("user_id", userID.(int64)), zap.Int64("user_id", userID),
zap.String("new_email", req.NewEmail), zap.String("new_email", req.NewEmail),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusBadRequest, model.NewErrorResponse( RespondBadRequest(c, err.Error(), nil)
model.CodeBadRequest,
err.Error(),
nil,
))
return return
} }
// 获取更新后的用户信息 user, err := service.GetUserByID(userID)
user, err := service.GetUserByID(userID.(int64))
if err != nil || user == nil { if err != nil || user == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse( RespondNotFound(c, "用户不存在")
model.CodeNotFound,
"用户不存在",
err,
))
return return
} }
c.JSON(http.StatusOK, model.NewSuccessResponse(&types.UserInfo{ RespondSuccess(c, UserToUserInfo(user))
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,
}))
} }
// ResetYggdrasilPassword 重置Yggdrasil密码 // ResetYggdrasilPassword 重置Yggdrasil密码
@@ -428,35 +271,19 @@ func ChangeEmail(c *gin.Context) {
// @Router /api/v1/user/yggdrasil-password/reset [post] // @Router /api/v1/user/yggdrasil-password/reset [post]
func ResetYggdrasilPassword(c *gin.Context) { func ResetYggdrasilPassword(c *gin.Context) {
loggerInstance := logger.MustGetLogger() loggerInstance := logger.MustGetLogger()
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
db := database.MustGetDB() db := database.MustGetDB()
newPassword, err := service.ResetYggdrasilPassword(db, userID)
// 从上下文获取用户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)
if err != nil { if err != nil {
loggerInstance.Error("[ERROR] 重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userId)) loggerInstance.Error("重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userID))
c.JSON(http.StatusInternalServerError, model.NewErrorResponse( RespondServerError(c, "重置Yggdrasil密码失败", nil)
model.CodeServerError,
"重置Yggdrasil密码失败",
nil,
))
return return
} }
loggerInstance.Info("[INFO] Yggdrasil密码重置成功", zap.Int64("userId", userId)) loggerInstance.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID))
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ RespondSuccess(c, gin.H{"password": newPassword})
"password": newPassword,
}))
} }

View File

@@ -405,12 +405,8 @@ func SignOut(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return 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)) loggerInstance.Warn("[WARN] 登出失败: 密码错误", zap.Any("用户ID:", user.ID))
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword}) c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
return return

View File

@@ -7,18 +7,18 @@ import (
// AuditLog 审计日志模型 // AuditLog 审计日志模型
type AuditLog struct { type AuditLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index" json:"user_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" json:"action"` 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" json:"resource_type"` 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" json:"resource_id,omitempty"` 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 格式 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 格式 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"` 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 指定表名 // TableName 指定表名
@@ -29,13 +29,13 @@ func (AuditLog) TableName() string {
// CasbinRule Casbin 权限规则模型 // CasbinRule Casbin 权限规则模型
type CasbinRule struct { type CasbinRule struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` 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"` 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;uniqueIndex:uk_casbin_rule" json:"v0"` 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;uniqueIndex:uk_casbin_rule" json:"v1"` 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" json:"v2"` 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" json:"v3"` 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" json:"v4"` 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" json:"v5"` 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"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
} }

View File

@@ -7,20 +7,20 @@ import (
// Profile Minecraft 档案模型 // Profile Minecraft 档案模型
type Profile struct { type Profile struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` 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" json:"name"` // Minecraft 角色名 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" json:"skin_id,omitempty"` 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" json:"cape_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 私钥不返回给前端 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"` 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" json:"last_used_at,omitempty"` 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" json:"created_at"` 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"` 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"` User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Skin *Texture `gorm:"foreignKey:SkinID" json:"skin,omitempty"` Skin *Texture `gorm:"foreignKey:SkinID;constraint:OnDelete:SET NULL" json:"skin,omitempty"`
Cape *Texture `gorm:"foreignKey:CapeID" json:"cape,omitempty"` Cape *Texture `gorm:"foreignKey:CapeID;constraint:OnDelete:SET NULL" json:"cape,omitempty"`
} }
// TableName 指定表名 // TableName 指定表名

View File

@@ -15,23 +15,23 @@ const (
// Texture 材质模型 // Texture 材质模型
type Texture struct { type Texture struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` 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"` Name string `gorm:"column:name;type:varchar(100);not null;default:''" json:"name"`
Description string `gorm:"column:description;type:text" json:"description,omitempty"` 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"` 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"` 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"` 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"` 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(粗) 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:已删除 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" json:"created_at"` 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"` 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 指定表名 // TableName 指定表名
@@ -42,13 +42,13 @@ func (Texture) TableName() string {
// UserTextureFavorite 用户材质收藏 // UserTextureFavorite 用户材质收藏
type UserTextureFavorite struct { type UserTextureFavorite struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` 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"` 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;index;uniqueIndex:uk_user_texture" json:"texture_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" json:"created_at"` 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"` User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Texture *Texture `gorm:"foreignKey:TextureID" json:"texture,omitempty"` Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"`
} }
// TableName 指定表名 // TableName 指定表名
@@ -59,15 +59,15 @@ func (UserTextureFavorite) TableName() string {
// TextureDownloadLog 材质下载记录 // TextureDownloadLog 材质下载记录
type TextureDownloadLog struct { type TextureDownloadLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
TextureID int64 `gorm:"column:texture_id;not null;index" json:"texture_id"` 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" json:"user_id,omitempty"` 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" json:"ip_address"` 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"` 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"` Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` User *User `gorm:"foreignKey:UserID;constraint:OnDelete:SET NULL" json:"user,omitempty"`
} }
// TableName 指定表名 // TableName 指定表名

View File

@@ -2,13 +2,19 @@ package model
import "time" import "time"
// Token Yggdrasil 认证令牌模型
type Token struct { type Token struct {
AccessToken string `json:"_id"` AccessToken string `gorm:"column:access_token;type:varchar(64);primaryKey" json:"access_token"`
UserID int64 `json:"user_id"` UserID int64 `gorm:"column:user_id;not null;index:idx_tokens_user_id" json:"user_id"`
ClientToken string `json:"client_token"` ClientToken string `gorm:"column:client_token;type:varchar(64);not null;index:idx_tokens_client_token" json:"client_token"`
ProfileId string `json:"profile_id"` ProfileId string `gorm:"column:profile_id;type:varchar(36);not null;index:idx_tokens_profile_id" json:"profile_id"`
Usable bool `json:"usable"` Usable bool `gorm:"column:usable;not null;default:true;index:idx_tokens_usable" json:"usable"`
IssueDate time.Time `json:"issue_date"` 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" }

View File

@@ -9,16 +9,16 @@ import (
// User 用户模型 // User 用户模型
type User struct { type User struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` 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:"-"` // 密码不返回给前端 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"` 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"` 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'" json:"role"` 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" json:"status"` // 1:正常, 0:禁用, -1:删除 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类型 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"` 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" json:"created_at"` 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"` 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 用户积分变更记录 // UserPointLog 用户积分变更记录
type UserPointLog struct { type UserPointLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` 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" json:"change_type"` // EARN, SPEND, ADMIN_ADJUST 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"` Amount int `gorm:"column:amount;type:integer;not null" json:"amount"`
BalanceBefore int `gorm:"column:balance_before;type:integer;not null" json:"balance_before"` 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"` 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"` 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"` 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"` ReferenceID *int64 `gorm:"column:reference_id;type:bigint" json:"reference_id,omitempty"`
OperatorID *int64 `gorm:"column:operator_id;type:bigint" json:"operator_id,omitempty"` 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_created_at,sort:desc" json:"created_at"` 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"` User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"` Operator *User `gorm:"foreignKey:OperatorID;constraint:OnDelete:SET NULL" json:"operator,omitempty"`
} }
// TableName 指定表名 // TableName 指定表名
@@ -54,16 +54,16 @@ func (UserPointLog) TableName() string {
// UserLoginLog 用户登录日志 // UserLoginLog 用户登录日志
type UserLoginLog struct { type UserLoginLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"` 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" json:"ip_address"` 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"` 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"` 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"` 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 指定表名 // TableName 指定表名

View File

@@ -1,10 +1,12 @@
package model package model
import ( import (
"crypto/rand"
"fmt" "fmt"
"math/big"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"math/rand"
"time"
) )
// 定义随机字符集 // 定义随机字符集
@@ -13,36 +15,47 @@ const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234
// Yggdrasil ygg密码与用户id绑定 // Yggdrasil ygg密码与用户id绑定
type Yggdrasil struct { type Yggdrasil struct {
ID int64 `gorm:"column:id;primaryKey;not null" json:"id"` 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但不自动创建外键约束避免循环依赖 // 关联 - Yggdrasil的ID引用User的ID但不自动创建外键约束避免循环依赖
User *User `gorm:"foreignKey:ID;references:ID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE" json:"user,omitempty"` 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 { func (u *User) AfterCreate(tx *gorm.DB) error {
randomPwd := GenerateRandomPassword(16) // 生成随机明文密码
plainPassword := GenerateRandomPassword(16)
// 创建GeneratePassword记录 // 使用 bcrypt 加密密码
gp := Yggdrasil{ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
ID: u.ID, // 关联User的ID if err != nil {
Password: randomPwd, // 16位随机密码 return fmt.Errorf("密码加密失败: %w", err)
} }
if err := tx.Create(&gp).Error; err != nil { // 创建Yggdrasil记录存储加密后的密码
// 若同步失败,可记录日志或回滚事务(根据业务需求处理) ygg := Yggdrasil{
return fmt.Errorf("同步生成密码失败: %w", err) ID: u.ID,
Password: string(hashedPassword),
}
if err := tx.Create(&ygg).Error; err != nil {
return fmt.Errorf("同步生成Yggdrasil密码失败: %w", err)
} }
return nil return nil
} }
// GenerateRandomPassword 生成指定长度的随机字符串 // GenerateRandomPassword 生成指定长度的安全随机字符串
func GenerateRandomPassword(length int) string { func GenerateRandomPassword(length int) string {
rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
b := make([]byte, length) b := make([]byte, length)
for i := range b { 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) return string(b)
} }

View File

@@ -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
}

View File

@@ -2,7 +2,6 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@@ -12,15 +11,13 @@ import (
// CreateProfile 创建档案 // CreateProfile 创建档案
func CreateProfile(profile *model.Profile) error { func CreateProfile(profile *model.Profile) error {
db := database.MustGetDB() return getDB().Create(profile).Error
return db.Create(profile).Error
} }
// FindProfileByUUID 根据UUID查找档案 // FindProfileByUUID 根据UUID查找档案
func FindProfileByUUID(uuid string) (*model.Profile, error) { func FindProfileByUUID(uuid string) (*model.Profile, error) {
db := database.MustGetDB()
var profile model.Profile var profile model.Profile
err := db.Where("uuid = ?", uuid). err := getDB().Where("uuid = ?", uuid).
Preload("Skin"). Preload("Skin").
Preload("Cape"). Preload("Cape").
First(&profile).Error First(&profile).Error
@@ -32,9 +29,8 @@ func FindProfileByUUID(uuid string) (*model.Profile, error) {
// FindProfileByName 根据角色名查找档案 // FindProfileByName 根据角色名查找档案
func FindProfileByName(name string) (*model.Profile, error) { func FindProfileByName(name string) (*model.Profile, error) {
db := database.MustGetDB()
var profile model.Profile var profile model.Profile
err := db.Where("name = ?", name).First(&profile).Error err := getDB().Where("name = ?", name).First(&profile).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -43,44 +39,36 @@ func FindProfileByName(name string) (*model.Profile, error) {
// FindProfilesByUserID 获取用户的所有档案 // FindProfilesByUserID 获取用户的所有档案
func FindProfilesByUserID(userID int64) ([]*model.Profile, error) { func FindProfilesByUserID(userID int64) ([]*model.Profile, error) {
db := database.MustGetDB()
var profiles []*model.Profile var profiles []*model.Profile
err := db.Where("user_id = ?", userID). err := getDB().Where("user_id = ?", userID).
Preload("Skin"). Preload("Skin").
Preload("Cape"). Preload("Cape").
Order("created_at DESC"). Order("created_at DESC").
Find(&profiles).Error Find(&profiles).Error
if err != nil { return profiles, err
return nil, err
}
return profiles, nil
} }
// UpdateProfile 更新档案 // UpdateProfile 更新档案
func UpdateProfile(profile *model.Profile) error { func UpdateProfile(profile *model.Profile) error {
db := database.MustGetDB() return getDB().Save(profile).Error
return db.Save(profile).Error
} }
// UpdateProfileFields 更新指定字段 // UpdateProfileFields 更新指定字段
func UpdateProfileFields(uuid string, updates map[string]interface{}) error { func UpdateProfileFields(uuid string, updates map[string]interface{}) error {
db := database.MustGetDB() return getDB().Model(&model.Profile{}).
return db.Model(&model.Profile{}).
Where("uuid = ?", uuid). Where("uuid = ?", uuid).
Updates(updates).Error Updates(updates).Error
} }
// DeleteProfile 删除档案 // DeleteProfile 删除档案
func DeleteProfile(uuid string) error { func DeleteProfile(uuid string) error {
db := database.MustGetDB() return getDB().Where("uuid = ?", uuid).Delete(&model.Profile{}).Error
return db.Where("uuid = ?", uuid).Delete(&model.Profile{}).Error
} }
// CountProfilesByUserID 统计用户的档案数量 // CountProfilesByUserID 统计用户的档案数量
func CountProfilesByUserID(userID int64) (int64, error) { func CountProfilesByUserID(userID int64) (int64, error) {
db := database.MustGetDB()
var count int64 var count int64
err := db.Model(&model.Profile{}). err := getDB().Model(&model.Profile{}).
Where("user_id = ?", userID). Where("user_id = ?", userID).
Count(&count).Error Count(&count).Error
return count, err return count, err
@@ -88,30 +76,22 @@ func CountProfilesByUserID(userID int64) (int64, error) {
// SetActiveProfile 设置档案为活跃状态(同时将用户的其他档案设置为非活跃) // SetActiveProfile 设置档案为活跃状态(同时将用户的其他档案设置为非活跃)
func SetActiveProfile(uuid string, userID int64) error { func SetActiveProfile(uuid string, userID int64) error {
db := database.MustGetDB() return getDB().Transaction(func(tx *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// 将用户的所有档案设置为非活跃
if err := tx.Model(&model.Profile{}). if err := tx.Model(&model.Profile{}).
Where("user_id = ?", userID). Where("user_id = ?", userID).
Update("is_active", false).Error; err != nil { Update("is_active", false).Error; err != nil {
return err return err
} }
// 将指定档案设置为活跃 return tx.Model(&model.Profile{}).
if err := tx.Model(&model.Profile{}).
Where("uuid = ? AND user_id = ?", uuid, userID). Where("uuid = ? AND user_id = ?", uuid, userID).
Update("is_active", true).Error; err != nil { Update("is_active", true).Error
return err
}
return nil
}) })
} }
// UpdateProfileLastUsedAt 更新最后使用时间 // UpdateProfileLastUsedAt 更新最后使用时间
func UpdateProfileLastUsedAt(uuid string) error { func UpdateProfileLastUsedAt(uuid string) error {
db := database.MustGetDB() return getDB().Model(&model.Profile{}).
return db.Model(&model.Profile{}).
Where("uuid = ?", uuid). Where("uuid = ?", uuid).
Update("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error Update("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error
} }
@@ -122,53 +102,40 @@ func FindOneProfileByUserID(userID int64) (*model.Profile, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
profile := profiles[0] if len(profiles) == 0 {
return profile, nil return nil, errors.New("未找到角色")
}
return profiles[0], nil
} }
func GetProfilesByNames(names []string) ([]*model.Profile, error) { func GetProfilesByNames(names []string) ([]*model.Profile, error) {
db := database.MustGetDB()
var profiles []*model.Profile var profiles []*model.Profile
err := db.Where("name in (?)", names).Find(&profiles).Error err := getDB().Where("name in (?)", names).Find(&profiles).Error
if err != nil { return profiles, err
return nil, err
}
return profiles, nil
} }
func GetProfileKeyPair(profileId string) (*model.KeyPair, error) { func GetProfileKeyPair(profileId string) (*model.KeyPair, error) {
db := database.MustGetDB()
// 1. 参数校验(保持原逻辑)
if profileId == "" { if profileId == "" {
return nil, errors.New("参数不能为空") return nil, errors.New("参数不能为空")
} }
// 2. GORM 查询:只查询 key_pair 字段(对应原 mongo 投影) var profile model.Profile
var profile *model.Profile result := getDB().WithContext(context.Background()).
// 条件id = profileIdPostgreSQL 主键),只选择 key_pair 字段 Select("key_pair").
result := db.WithContext(context.Background()). Where("id = ?", profileId).
Select("key_pair"). // 只查询需要的字段(投影) First(&profile)
Where("id = ?", profileId). // 查询条件GORM 自动处理占位符,避免 SQL 注入)
First(&profile) // 查单条记录
// 3. 错误处理(适配 GORM 错误类型)
if result.Error != nil { if result.Error != nil {
// 空结果判断(对应原 mongo.ErrNoDocuments / pgx.ErrNoRows if IsNotFound(result.Error) {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("key pair未找到") return nil, errors.New("key pair未找到")
} }
// 保持原错误封装格式
return nil, fmt.Errorf("获取key pair失败: %w", result.Error) return nil, fmt.Errorf("获取key pair失败: %w", result.Error)
} }
// 4. JSONB 反序列化为 model.KeyPair return &model.KeyPair{}, nil
keyPair := &model.KeyPair{}
return keyPair, nil
} }
func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error { func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error {
db := database.MustGetDB()
// 仅保留最必要的入参校验(避免无效数据库请求)
if profileId == "" { if profileId == "" {
return errors.New("profileId 不能为空") return errors.New("profileId 不能为空")
} }
@@ -176,24 +143,18 @@ func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error {
return errors.New("keyPair 不能为 nil") return errors.New("keyPair 不能为 nil")
} }
// 事务内执行核心更新(保证原子性,出错自动回滚) return getDB().Transaction(func(tx *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// 核心更新逻辑:按 profileId 匹配,直接更新 key_pair 相关字段
result := tx.WithContext(context.Background()). result := tx.WithContext(context.Background()).
Table("profiles"). // 目标表名(与 PostgreSQL 表一致) Table("profiles").
Where("id = ?", profileId). // 更新条件profileId 匹配 Where("id = ?", profileId).
// 直接映射字段(无需序列化,依赖 GORM 自动字段匹配)
UpdateColumns(map[string]interface{}{ UpdateColumns(map[string]interface{}{
"private_key": keyPair.PrivateKey, // 数据库 private_key 字段 "private_key": keyPair.PrivateKey,
"public_key": keyPair.PublicKey, // 数据库 public_key 字段 "public_key": keyPair.PublicKey,
// 若 key_pair 是单个字段(非拆分),替换为:"key_pair": keyPair
}) })
// 仅处理数据库层面的致命错误
if result.Error != nil { if result.Error != nil {
return fmt.Errorf("更新 keyPair 失败: %w", result.Error) return fmt.Errorf("更新 keyPair 失败: %w", result.Error)
} }
return nil return nil
}) })
} }

View File

@@ -2,56 +2,35 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
"errors"
"gorm.io/gorm"
) )
// GetSystemConfigByKey 根据键获取配置 // GetSystemConfigByKey 根据键获取配置
func GetSystemConfigByKey(key string) (*model.SystemConfig, error) { func GetSystemConfigByKey(key string) (*model.SystemConfig, error) {
db := database.MustGetDB()
var config model.SystemConfig var config model.SystemConfig
err := db.Where("key = ?", key).First(&config).Error err := getDB().Where("key = ?", key).First(&config).Error
if err != nil { return HandleNotFound(&config, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &config, nil
} }
// GetPublicSystemConfigs 获取所有公开配置 // GetPublicSystemConfigs 获取所有公开配置
func GetPublicSystemConfigs() ([]model.SystemConfig, error) { func GetPublicSystemConfigs() ([]model.SystemConfig, error) {
db := database.MustGetDB()
var configs []model.SystemConfig var configs []model.SystemConfig
err := db.Where("is_public = ?", true).Find(&configs).Error err := getDB().Where("is_public = ?", true).Find(&configs).Error
if err != nil { return configs, err
return nil, err
}
return configs, nil
} }
// GetAllSystemConfigs 获取所有配置(管理员用) // GetAllSystemConfigs 获取所有配置(管理员用)
func GetAllSystemConfigs() ([]model.SystemConfig, error) { func GetAllSystemConfigs() ([]model.SystemConfig, error) {
db := database.MustGetDB()
var configs []model.SystemConfig var configs []model.SystemConfig
err := db.Find(&configs).Error err := getDB().Find(&configs).Error
if err != nil { return configs, err
return nil, err
}
return configs, nil
} }
// UpdateSystemConfig 更新配置 // UpdateSystemConfig 更新配置
func UpdateSystemConfig(config *model.SystemConfig) error { func UpdateSystemConfig(config *model.SystemConfig) error {
db := database.MustGetDB() return getDB().Save(config).Error
return db.Save(config).Error
} }
// UpdateSystemConfigValue 更新配置值 // UpdateSystemConfigValue 更新配置值
func UpdateSystemConfigValue(key, value string) error { func UpdateSystemConfigValue(key, value string) error {
db := database.MustGetDB() return getDB().Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
return db.Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
} }

View File

@@ -2,63 +2,44 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
"gorm.io/gorm" "gorm.io/gorm"
) )
// CreateTexture 创建材质 // CreateTexture 创建材质
func CreateTexture(texture *model.Texture) error { func CreateTexture(texture *model.Texture) error {
db := database.MustGetDB() return getDB().Create(texture).Error
return db.Create(texture).Error
} }
// FindTextureByID 根据ID查找材质 // FindTextureByID 根据ID查找材质
func FindTextureByID(id int64) (*model.Texture, error) { func FindTextureByID(id int64) (*model.Texture, error) {
db := database.MustGetDB()
var texture model.Texture var texture model.Texture
err := db.Preload("Uploader").First(&texture, id).Error err := getDB().Preload("Uploader").First(&texture, id).Error
if err != nil { return HandleNotFound(&texture, err)
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &texture, nil
} }
// FindTextureByHash 根据Hash查找材质 // FindTextureByHash 根据Hash查找材质
func FindTextureByHash(hash string) (*model.Texture, error) { func FindTextureByHash(hash string) (*model.Texture, error) {
db := database.MustGetDB()
var texture model.Texture var texture model.Texture
err := db.Where("hash = ?", hash).First(&texture).Error err := getDB().Where("hash = ?", hash).First(&texture).Error
if err != nil { return HandleNotFound(&texture, err)
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &texture, nil
} }
// FindTexturesByUploaderID 根据上传者ID查找材质列表 // FindTexturesByUploaderID 根据上传者ID查找材质列表
func FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) { func FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
db := database.MustGetDB() db := getDB()
var textures []*model.Texture var textures []*model.Texture
var total int64 var total int64
query := db.Model(&model.Texture{}).Where("uploader_id = ? AND status != -1", uploaderID) query := db.Model(&model.Texture{}).Where("uploader_id = ? AND status != -1", uploaderID)
// 获取总数
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
// 分页查询 err := query.Scopes(Paginate(page, pageSize)).
offset := (page - 1) * pageSize Preload("Uploader").
err := query.Preload("Uploader").
Order("created_at DESC"). Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&textures).Error Find(&textures).Error
if err != nil { if err != nil {
@@ -70,38 +51,29 @@ func FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Te
// SearchTextures 搜索材质 // SearchTextures 搜索材质
func SearchTextures(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) { 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 textures []*model.Texture
var total int64 var total int64
query := db.Model(&model.Texture{}).Where("status = 1") query := db.Model(&model.Texture{}).Where("status = 1")
// 公开筛选
if publicOnly { if publicOnly {
query = query.Where("is_public = ?", true) query = query.Where("is_public = ?", true)
} }
// 类型筛选
if textureType != "" { if textureType != "" {
query = query.Where("type = ?", textureType) query = query.Where("type = ?", textureType)
} }
// 关键词搜索
if keyword != "" { if keyword != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%") query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
} }
// 获取总数
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
// 分页查询 err := query.Scopes(Paginate(page, pageSize)).
offset := (page - 1) * pageSize Preload("Uploader").
err := query.Preload("Uploader").
Order("created_at DESC"). Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&textures).Error Find(&textures).Error
if err != nil { if err != nil {
@@ -113,86 +85,72 @@ func SearchTextures(keyword string, textureType model.TextureType, publicOnly bo
// UpdateTexture 更新材质 // UpdateTexture 更新材质
func UpdateTexture(texture *model.Texture) error { func UpdateTexture(texture *model.Texture) error {
db := database.MustGetDB() return getDB().Save(texture).Error
return db.Save(texture).Error
} }
// UpdateTextureFields 更新材质指定字段 // UpdateTextureFields 更新材质指定字段
func UpdateTextureFields(id int64, fields map[string]interface{}) error { func UpdateTextureFields(id int64, fields map[string]interface{}) error {
db := database.MustGetDB() return getDB().Model(&model.Texture{}).Where("id = ?", id).Updates(fields).Error
return db.Model(&model.Texture{}).Where("id = ?", id).Updates(fields).Error
} }
// DeleteTexture 删除材质(软删除) // DeleteTexture 删除材质(软删除)
func DeleteTexture(id int64) error { func DeleteTexture(id int64) error {
db := database.MustGetDB() return getDB().Model(&model.Texture{}).Where("id = ?", id).Update("status", -1).Error
return db.Model(&model.Texture{}).Where("id = ?", id).Update("status", -1).Error
} }
// IncrementTextureDownloadCount 增加下载次数 // IncrementTextureDownloadCount 增加下载次数
func IncrementTextureDownloadCount(id int64) error { func IncrementTextureDownloadCount(id int64) error {
db := database.MustGetDB() return getDB().Model(&model.Texture{}).Where("id = ?", id).
return db.Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
} }
// IncrementTextureFavoriteCount 增加收藏次数 // IncrementTextureFavoriteCount 增加收藏次数
func IncrementTextureFavoriteCount(id int64) error { func IncrementTextureFavoriteCount(id int64) error {
db := database.MustGetDB() return getDB().Model(&model.Texture{}).Where("id = ?", id).
return db.Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error
} }
// DecrementTextureFavoriteCount 减少收藏次数 // DecrementTextureFavoriteCount 减少收藏次数
func DecrementTextureFavoriteCount(id int64) error { func DecrementTextureFavoriteCount(id int64) error {
db := database.MustGetDB() return getDB().Model(&model.Texture{}).Where("id = ?", id).
return db.Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error
} }
// CreateTextureDownloadLog 创建下载日志 // CreateTextureDownloadLog 创建下载日志
func CreateTextureDownloadLog(log *model.TextureDownloadLog) error { func CreateTextureDownloadLog(log *model.TextureDownloadLog) error {
db := database.MustGetDB() return getDB().Create(log).Error
return db.Create(log).Error
} }
// IsTextureFavorited 检查是否已收藏 // IsTextureFavorited 检查是否已收藏
func IsTextureFavorited(userID, textureID int64) (bool, error) { func IsTextureFavorited(userID, textureID int64) (bool, error) {
db := database.MustGetDB()
var count int64 var count int64
err := db.Model(&model.UserTextureFavorite{}). err := getDB().Model(&model.UserTextureFavorite{}).
Where("user_id = ? AND texture_id = ?", userID, textureID). Where("user_id = ? AND texture_id = ?", userID, textureID).
Count(&count).Error Count(&count).Error
if err != nil { return count > 0, err
return false, err
}
return count > 0, nil
} }
// AddTextureFavorite 添加收藏 // AddTextureFavorite 添加收藏
func AddTextureFavorite(userID, textureID int64) error { func AddTextureFavorite(userID, textureID int64) error {
db := database.MustGetDB()
favorite := &model.UserTextureFavorite{ favorite := &model.UserTextureFavorite{
UserID: userID, UserID: userID,
TextureID: textureID, TextureID: textureID,
} }
return db.Create(favorite).Error return getDB().Create(favorite).Error
} }
// RemoveTextureFavorite 取消收藏 // RemoveTextureFavorite 取消收藏
func RemoveTextureFavorite(userID, textureID int64) error { func RemoveTextureFavorite(userID, textureID int64) error {
db := database.MustGetDB() return getDB().Where("user_id = ? AND texture_id = ?", userID, textureID).
return db.Where("user_id = ? AND texture_id = ?", userID, textureID).
Delete(&model.UserTextureFavorite{}).Error Delete(&model.UserTextureFavorite{}).Error
} }
// GetUserTextureFavorites 获取用户收藏的材质列表 // GetUserTextureFavorites 获取用户收藏的材质列表
func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture, int64, error) { func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
db := database.MustGetDB() db := getDB()
var textures []*model.Texture var textures []*model.Texture
var total int64 var total int64
// 子查询获取收藏的材质ID
subQuery := db.Model(&model.UserTextureFavorite{}). subQuery := db.Model(&model.UserTextureFavorite{}).
Select("texture_id"). Select("texture_id").
Where("user_id = ?", userID) Where("user_id = ?", userID)
@@ -200,17 +158,13 @@ func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture
query := db.Model(&model.Texture{}). query := db.Model(&model.Texture{}).
Where("id IN (?) AND status = 1", subQuery) Where("id IN (?) AND status = 1", subQuery)
// 获取总数
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
// 分页查询 err := query.Scopes(Paginate(page, pageSize)).
offset := (page - 1) * pageSize Preload("Uploader").
err := query.Preload("Uploader").
Order("created_at DESC"). Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&textures).Error Find(&textures).Error
if err != nil { if err != nil {
@@ -222,9 +176,8 @@ func GetUserTextureFavorites(userID int64, page, pageSize int) ([]*model.Texture
// CountTexturesByUploaderID 统计用户上传的材质数量 // CountTexturesByUploaderID 统计用户上传的材质数量
func CountTexturesByUploaderID(uploaderID int64) (int64, error) { func CountTexturesByUploaderID(uploaderID int64) (int64, error) {
db := database.MustGetDB()
var count int64 var count int64
err := db.Model(&model.Texture{}). err := getDB().Model(&model.Texture{}).
Where("uploader_id = ? AND status != -1", uploaderID). Where("uploader_id = ? AND status != -1", uploaderID).
Count(&count).Error Count(&count).Error
return count, err return count, err

View File

@@ -2,48 +2,38 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
) )
func CreateToken(token *model.Token) error { func CreateToken(token *model.Token) error {
db := database.MustGetDB() return getDB().Create(token).Error
return db.Create(token).Error
} }
func GetTokensByUserId(userId int64) ([]*model.Token, error) { func GetTokensByUserId(userId int64) ([]*model.Token, error) {
db := database.MustGetDB() var tokens []*model.Token
tokens := make([]*model.Token, 0) err := getDB().Where("user_id = ?", userId).Find(&tokens).Error
err := db.Where("user_id = ?", userId).Find(&tokens).Error return tokens, err
if err != nil {
return nil, err
}
return tokens, nil
} }
func BatchDeleteTokens(tokensToDelete []string) (int64, error) { func BatchDeleteTokens(tokensToDelete []string) (int64, error) {
db := database.MustGetDB()
if len(tokensToDelete) == 0 { 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 return result.RowsAffected, result.Error
} }
func FindTokenByID(accessToken string) (*model.Token, error) { func FindTokenByID(accessToken string) (*model.Token, error) {
db := database.MustGetDB() var token model.Token
var tokens []*model.Token err := getDB().Where("access_token = ?", accessToken).First(&token).Error
err := db.Where("_id = ?", accessToken).Find(&tokens).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return tokens[0], nil return &token, nil
} }
func GetUUIDByAccessToken(accessToken string) (string, error) { func GetUUIDByAccessToken(accessToken string) (string, error) {
db := database.MustGetDB()
var token model.Token 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 { if err != nil {
return "", err return "", err
} }
@@ -51,9 +41,8 @@ func GetUUIDByAccessToken(accessToken string) (string, error) {
} }
func GetUserIDByAccessToken(accessToken string) (int64, error) { func GetUserIDByAccessToken(accessToken string) (int64, error) {
db := database.MustGetDB()
var token model.Token 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 { if err != nil {
return 0, err return 0, err
} }
@@ -61,9 +50,8 @@ func GetUserIDByAccessToken(accessToken string) (int64, error) {
} }
func GetTokenByAccessToken(accessToken string) (*model.Token, error) { func GetTokenByAccessToken(accessToken string) (*model.Token, error) {
db := database.MustGetDB()
var token model.Token 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 { if err != nil {
return nil, err return nil, err
} }
@@ -71,19 +59,9 @@ func GetTokenByAccessToken(accessToken string) (*model.Token, error) {
} }
func DeleteTokenByAccessToken(accessToken string) error { func DeleteTokenByAccessToken(accessToken string) error {
db := database.MustGetDB() return getDB().Where("access_token = ?", accessToken).Delete(&model.Token{}).Error
err := db.Where("access_token = ?", accessToken).Delete(&model.Token{}).Error
if err != nil {
return err
}
return nil
} }
func DeleteTokenByUserId(userId int64) error { func DeleteTokenByUserId(userId int64) error {
db := database.MustGetDB() return getDB().Where("user_id = ?", userId).Delete(&model.Token{}).Error
err := db.Where("user_id = ?", userId).Delete(&model.Token{}).Error
if err != nil {
return err
}
return nil
} }

View File

@@ -2,7 +2,6 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
"errors" "errors"
"gorm.io/gorm" "gorm.io/gorm"
@@ -10,87 +9,58 @@ import (
// CreateUser 创建用户 // CreateUser 创建用户
func CreateUser(user *model.User) error { func CreateUser(user *model.User) error {
db := database.MustGetDB() return getDB().Create(user).Error
return db.Create(user).Error
} }
// FindUserByID 根据ID查找用户 // FindUserByID 根据ID查找用户
func FindUserByID(id int64) (*model.User, error) { func FindUserByID(id int64) (*model.User, error) {
db := database.MustGetDB()
var user model.User var user model.User
err := db.Where("id = ? AND status != -1", id).First(&user).Error err := getDB().Where("id = ? AND status != -1", id).First(&user).Error
if err != nil { return HandleNotFound(&user, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
} }
// FindUserByUsername 根据用户名查找用户 // FindUserByUsername 根据用户名查找用户
func FindUserByUsername(username string) (*model.User, error) { func FindUserByUsername(username string) (*model.User, error) {
db := database.MustGetDB()
var user model.User var user model.User
err := db.Where("username = ? AND status != -1", username).First(&user).Error err := getDB().Where("username = ? AND status != -1", username).First(&user).Error
if err != nil { return HandleNotFound(&user, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
} }
// FindUserByEmail 根据邮箱查找用户 // FindUserByEmail 根据邮箱查找用户
func FindUserByEmail(email string) (*model.User, error) { func FindUserByEmail(email string) (*model.User, error) {
db := database.MustGetDB()
var user model.User var user model.User
err := db.Where("email = ? AND status != -1", email).First(&user).Error err := getDB().Where("email = ? AND status != -1", email).First(&user).Error
if err != nil { return HandleNotFound(&user, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
} }
// UpdateUser 更新用户 // UpdateUser 更新用户
func UpdateUser(user *model.User) error { func UpdateUser(user *model.User) error {
db := database.MustGetDB() return getDB().Save(user).Error
return db.Save(user).Error
} }
// UpdateUserFields 更新指定字段 // UpdateUserFields 更新指定字段
func UpdateUserFields(id int64, fields map[string]interface{}) error { func UpdateUserFields(id int64, fields map[string]interface{}) error {
db := database.MustGetDB() return getDB().Model(&model.User{}).Where("id = ?", id).Updates(fields).Error
return db.Model(&model.User{}).Where("id = ?", id).Updates(fields).Error
} }
// DeleteUser 软删除用户 // DeleteUser 软删除用户
func DeleteUser(id int64) error { func DeleteUser(id int64) error {
db := database.MustGetDB() return getDB().Model(&model.User{}).Where("id = ?", id).Update("status", -1).Error
return db.Model(&model.User{}).Where("id = ?", id).Update("status", -1).Error
} }
// CreateLoginLog 创建登录日志 // CreateLoginLog 创建登录日志
func CreateLoginLog(log *model.UserLoginLog) error { func CreateLoginLog(log *model.UserLoginLog) error {
db := database.MustGetDB() return getDB().Create(log).Error
return db.Create(log).Error
} }
// CreatePointLog 创建积分日志 // CreatePointLog 创建积分日志
func CreatePointLog(log *model.UserPointLog) error { func CreatePointLog(log *model.UserPointLog) error {
db := database.MustGetDB() return getDB().Create(log).Error
return db.Create(log).Error
} }
// UpdateUserPoints 更新用户积分(事务) // UpdateUserPoints 更新用户积分(事务)
func UpdateUserPoints(userID int64, amount int, changeType, reason string) error { func UpdateUserPoints(userID int64, amount int, changeType, reason string) error {
db := database.MustGetDB() return getDB().Transaction(func(tx *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// 获取当前用户积分
var user model.User var user model.User
if err := tx.Where("id = ?", userID).First(&user).Error; err != nil { if err := tx.Where("id = ?", userID).First(&user).Error; err != nil {
return err return err
@@ -99,17 +69,14 @@ func UpdateUserPoints(userID int64, amount int, changeType, reason string) error
balanceBefore := user.Points balanceBefore := user.Points
balanceAfter := balanceBefore + amount balanceAfter := balanceBefore + amount
// 检查积分是否足够
if balanceAfter < 0 { if balanceAfter < 0 {
return errors.New("积分不足") return errors.New("积分不足")
} }
// 更新用户积分
if err := tx.Model(&user).Update("points", balanceAfter).Error; err != nil { if err := tx.Model(&user).Update("points", balanceAfter).Error; err != nil {
return err return err
} }
// 创建积分日志
log := &model.UserPointLog{ log := &model.UserPointLog{
UserID: userID, UserID: userID,
ChangeType: changeType, ChangeType: changeType,
@@ -125,12 +92,10 @@ func UpdateUserPoints(userID int64, amount int, changeType, reason string) error
// UpdateUserAvatar 更新用户头像 // UpdateUserAvatar 更新用户头像
func UpdateUserAvatar(userID int64, avatarURL string) error { func UpdateUserAvatar(userID int64, avatarURL string) error {
db := database.MustGetDB() return getDB().Model(&model.User{}).Where("id = ?", userID).Update("avatar", avatarURL).Error
return db.Model(&model.User{}).Where("id = ?", userID).Update("avatar", avatarURL).Error
} }
// UpdateUserEmail 更新用户邮箱 // UpdateUserEmail 更新用户邮箱
func UpdateUserEmail(userID int64, email string) error { func UpdateUserEmail(userID int64, email string) error {
db := database.MustGetDB() return getDB().Model(&model.User{}).Where("id = ?", userID).Update("email", email).Error
return db.Model(&model.User{}).Where("id = ?", userID).Update("email", email).Error
} }

View File

@@ -2,13 +2,11 @@ package repository
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/pkg/database"
) )
func GetYggdrasilPasswordById(Id int64) (string, error) { func GetYggdrasilPasswordById(id int64) (string, error) {
db := database.MustGetDB()
var yggdrasil model.Yggdrasil var yggdrasil model.Yggdrasil
err := db.Where("id = ?", Id).First(&yggdrasil).Error err := getDB().Where("id = ?", id).First(&yggdrasil).Error
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -17,6 +15,5 @@ func GetYggdrasilPasswordById(Id int64) (string, error) {
// ResetYggdrasilPassword 重置Yggdrasil密码 // ResetYggdrasilPassword 重置Yggdrasil密码
func ResetYggdrasilPassword(userId int64, newPassword string) error { func ResetYggdrasilPassword(userId int64, newPassword string) error {
db := database.MustGetDB() return getDB().Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error
return db.Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error
} }

105
internal/service/helpers.go Normal file
View File

@@ -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)
}

View File

@@ -9,6 +9,7 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"gorm.io/gorm" "gorm.io/gorm"
@@ -16,53 +17,47 @@ import (
// CreateProfile 创建档案 // CreateProfile 创建档案
func CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error) { 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { return nil, err
return nil, fmt.Errorf("用户不存在")
} }
return nil, fmt.Errorf("查询用户失败: %w", err)
}
if user.Status != 1 { if user.Status != 1 {
return nil, fmt.Errorf("用户状态异常") return nil, fmt.Errorf("用户状态异常")
} }
// 2. 检查角色名是否已存在 // 检查角色名是否已存在
existingName, err := repository.FindProfileByName(name) existingName, err := repository.FindProfileByName(name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询角色名失败: %w", err) return nil, WrapError(err, "查询角色名失败")
} }
if existingName != nil { if existingName != nil {
return nil, fmt.Errorf("角色名已被使用") return nil, fmt.Errorf("角色名已被使用")
} }
// 3. 生成UUID // 生成UUID和RSA密钥
profileUUID := uuid.New().String() profileUUID := uuid.New().String()
// 4. 生成RSA密钥对
privateKey, err := generateRSAPrivateKey() privateKey, err := generateRSAPrivateKey()
if err != nil { if err != nil {
return nil, fmt.Errorf("生成RSA密钥失败: %w", err) return nil, WrapError(err, "生成RSA密钥失败")
} }
// 5. 创建档案 // 创建档案
profile := &model.Profile{ profile := &model.Profile{
UUID: profileUUID, UUID: profileUUID,
UserID: userID, UserID: userID,
Name: name, Name: name,
RSAPrivateKey: privateKey, RSAPrivateKey: privateKey,
IsActive: true, // 新创建的档案默认为活跃状态 IsActive: true,
} }
if err := repository.CreateProfile(profile); err != nil { 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 { if err := repository.SetActiveProfile(profileUUID, userID); err != nil {
return nil, fmt.Errorf("设置活跃状态失败: %w", err) return nil, WrapError(err, "设置活跃状态失败")
} }
return profile, nil return profile, nil
@@ -73,9 +68,9 @@ func GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error) {
profile, err := repository.FindProfileByUUID(uuid) profile, err := repository.FindProfileByUUID(uuid)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { 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 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) { func GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error) {
profiles, err := repository.FindProfilesByUserID(userID) profiles, err := repository.FindProfilesByUserID(userID)
if err != nil { if err != nil {
return nil, fmt.Errorf("查询档案列表失败: %w", err) return nil, WrapError(err, "查询档案列表失败")
} }
return profiles, nil return profiles, nil
} }
// UpdateProfile 更新档案 // UpdateProfile 更新档案
func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error) { 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { return nil, err
return nil, fmt.Errorf("档案不存在")
}
return nil, fmt.Errorf("查询档案失败: %w", err)
} }
// 2. 验证权限 // 检查角色名是否重复
if profile.UserID != userID {
return nil, fmt.Errorf("无权操作此档案")
}
// 3. 检查角色名是否重复
if name != nil && *name != profile.Name { if name != nil && *name != profile.Name {
existingName, err := repository.FindProfileByName(*name) existingName, err := repository.FindProfileByName(*name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询角色名失败: %w", err) return nil, WrapError(err, "查询角色名失败")
} }
if existingName != nil { if existingName != nil {
return nil, fmt.Errorf("角色名已被使用") return nil, fmt.Errorf("角色名已被使用")
@@ -117,7 +104,7 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID,
profile.Name = *name profile.Name = *name
} }
// 4. 更新皮肤和披风 // 更新皮肤和披风
if skinID != nil { if skinID != nil {
profile.SkinID = skinID profile.SkinID = skinID
} }
@@ -125,63 +112,37 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID,
profile.CapeID = capeID profile.CapeID = capeID
} }
// 5. 保存更新
if err := repository.UpdateProfile(profile); err != nil { if err := repository.UpdateProfile(profile); err != nil {
return nil, fmt.Errorf("更新档案失败: %w", err) return nil, WrapError(err, "更新档案失败")
} }
// 6. 重新加载关联数据
return repository.FindProfileByUUID(uuid) return repository.FindProfileByUUID(uuid)
} }
// DeleteProfile 删除档案 // DeleteProfile 删除档案
func DeleteProfile(db *gorm.DB, uuid string, userID int64) error { func DeleteProfile(db *gorm.DB, uuid string, userID int64) error {
// 1. 查询档案 if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil {
profile, err := repository.FindProfileByUUID(uuid) return err
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("档案不存在")
}
return fmt.Errorf("查询档案失败: %w", err)
} }
// 2. 验证权限
if profile.UserID != userID {
return fmt.Errorf("无权操作此档案")
}
// 3. 删除档案
if err := repository.DeleteProfile(uuid); err != nil { if err := repository.DeleteProfile(uuid); err != nil {
return fmt.Errorf("删除档案失败: %w", err) return WrapError(err, "删除档案失败")
} }
return nil return nil
} }
// SetActiveProfile 设置活跃档案 // SetActiveProfile 设置活跃档案
func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error { func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error {
// 1. 查询档案 if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil {
profile, err := repository.FindProfileByUUID(uuid) return err
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("档案不存在")
}
return fmt.Errorf("查询档案失败: %w", err)
} }
// 2. 验证权限
if profile.UserID != userID {
return fmt.Errorf("无权操作此档案")
}
// 3. 设置活跃状态
if err := repository.SetActiveProfile(uuid, userID); err != nil { if err := repository.SetActiveProfile(uuid, userID); err != nil {
return fmt.Errorf("设置活跃状态失败: %w", err) return WrapError(err, "设置活跃状态失败")
} }
// 4. 更新最后使用时间
if err := repository.UpdateProfileLastUsedAt(uuid); err != nil { if err := repository.UpdateProfileLastUsedAt(uuid); err != nil {
return fmt.Errorf("更新使用时间失败: %w", err) return WrapError(err, "更新使用时间失败")
} }
return nil 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 { func CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error {
count, err := repository.CountProfilesByUserID(userID) count, err := repository.CountProfilesByUserID(userID)
if err != nil { if err != nil {
return fmt.Errorf("查询档案数量失败: %w", err) return WrapError(err, "查询档案数量失败")
} }
if int(count) >= maxProfiles { if int(count) >= maxProfiles {
return fmt.Errorf("已达到档案数量上限(%d个", maxProfiles) return fmt.Errorf("已达到档案数量上限(%d个", maxProfiles)
} }
return nil return nil
} }
// generateRSAPrivateKey 生成RSA-2048私钥PEM格式 // generateRSAPrivateKey 生成RSA-2048私钥PEM格式
func generateRSAPrivateKey() (string, error) { func generateRSAPrivateKey() (string, error) {
// 生成2048位RSA密钥对
privateKey, err := rsa.GenerateKey(rand.Reader, 2048) privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
return "", err return "", err
} }
// 将私钥编码为PEM格式
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyPEM := pem.EncodeToMemory(&pem.Block{ privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", 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) { if errors.Is(err, pgx.ErrNoRows) {
return false, errors.New("配置文件不存在") return false, errors.New("配置文件不存在")
} }
return false, fmt.Errorf("验证配置文件失败: %w", err) return false, WrapError(err, "验证配置文件失败")
} }
return profile.UserID == userId, nil 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) { func GetProfilesDataByNames(db *gorm.DB, names []string) ([]*model.Profile, error) {
profiles, err := repository.GetProfilesByNames(names) profiles, err := repository.GetProfilesByNames(names)
if err != nil { if err != nil {
return nil, fmt.Errorf("查找失败: %w", err) return nil, WrapError(err, "查找失败")
} }
return profiles, nil return profiles, nil
} }
// GetProfileKeyPair 从 PostgreSQL 获取密钥对GORM 实现,无手动 SQL
func GetProfileKeyPair(db *gorm.DB, profileId string) (*model.KeyPair, error) { func GetProfileKeyPair(db *gorm.DB, profileId string) (*model.KeyPair, error) {
keyPair, err := repository.GetProfileKeyPair(profileId) keyPair, err := repository.GetProfileKeyPair(profileId)
if err != nil { if err != nil {
return nil, fmt.Errorf("查找失败: %w", err) return nil, WrapError(err, "查找失败")
} }
return keyPair, nil return keyPair, nil
} }

View File

@@ -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)
}

View File

@@ -12,13 +12,9 @@ import (
// CreateTexture 创建材质 // CreateTexture 创建材质
func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) { 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 := EnsureUserExists(uploaderID); err != nil {
if err != nil {
return nil, err return nil, err
} }
if user == nil {
return nil, errors.New("用户不存在")
}
// 检查Hash是否已存在 // 检查Hash是否已存在
existingTexture, err := repository.FindTextureByHash(hash) existingTexture, err := repository.FindTextureByHash(hash)
@@ -30,14 +26,9 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType
} }
// 转换材质类型 // 转换材质类型
var textureTypeEnum model.TextureType textureTypeEnum, err := parseTextureType(textureType)
switch textureType { if err != nil {
case "SKIN": return nil, err
textureTypeEnum = model.TextureTypeSkin
case "CAPE":
textureTypeEnum = model.TextureTypeCape
default:
return nil, errors.New("无效的材质类型")
} }
// 创建材质 // 创建材质
@@ -65,58 +56,27 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType
// GetTextureByID 根据ID获取材质 // GetTextureByID 根据ID获取材质
func GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error) { func GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error) {
texture, err := repository.FindTextureByID(id) return EnsureTextureExists(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
} }
// GetUserTextures 获取用户上传的材质列表 // GetUserTextures 获取用户上传的材质列表
func GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) { func GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 { page, pageSize = NormalizePagination(page, pageSize)
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return repository.FindTexturesByUploaderID(uploaderID, page, pageSize) return repository.FindTexturesByUploaderID(uploaderID, page, pageSize)
} }
// SearchTextures 搜索材质 // SearchTextures 搜索材质
func SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) { func SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 { page, pageSize = NormalizePagination(page, pageSize)
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return repository.SearchTextures(keyword, textureType, publicOnly, page, pageSize) return repository.SearchTextures(keyword, textureType, publicOnly, page, pageSize)
} }
// UpdateTexture 更新材质 // UpdateTexture 更新材质
func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) { func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) {
// 获取材质 // 获取材质并验证权限
texture, err := repository.FindTextureByID(textureID) if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil {
if err != nil {
return nil, err return nil, err
} }
if texture == nil {
return nil, errors.New("材质不存在")
}
// 检查权限:只有上传者可以修改
if texture.UploaderID != uploaderID {
return nil, errors.New("无权修改此材质")
}
// 更新字段 // 更新字段
updates := make(map[string]interface{}) updates := make(map[string]interface{})
@@ -136,46 +96,27 @@ func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description s
} }
} }
// 返回更新后的材质
return repository.FindTextureByID(textureID) return repository.FindTextureByID(textureID)
} }
// DeleteTexture 删除材质 // DeleteTexture 删除材质
func DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error { func DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error {
// 获取材质 if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil {
texture, err := repository.FindTextureByID(textureID)
if err != nil {
return err return err
} }
if texture == nil {
return errors.New("材质不存在")
}
// 检查权限:只有上传者可以删除
if texture.UploaderID != uploaderID {
return errors.New("无权删除此材质")
}
return repository.DeleteTexture(textureID) return repository.DeleteTexture(textureID)
} }
// RecordTextureDownload 记录下载 // RecordTextureDownload 记录下载
func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error { func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error {
// 检查材质是否存在 if _, err := EnsureTextureExists(textureID); err != nil {
texture, err := repository.FindTextureByID(textureID)
if err != nil {
return err return err
} }
if texture == nil {
return errors.New("材质不存在")
}
// 增加下载次数
if err := repository.IncrementTextureDownloadCount(textureID); err != nil { if err := repository.IncrementTextureDownloadCount(textureID); err != nil {
return err return err
} }
// 创建下载日志
log := &model.TextureDownloadLog{ log := &model.TextureDownloadLog{
TextureID: textureID, TextureID: textureID,
UserID: userID, UserID: userID,
@@ -188,23 +129,17 @@ func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddres
// ToggleTextureFavorite 切换收藏状态 // ToggleTextureFavorite 切换收藏状态
func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) { func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
// 检查材质是否存在 if _, err := EnsureTextureExists(textureID); err != nil {
texture, err := repository.FindTextureByID(textureID)
if err != nil {
return false, err return false, err
} }
if texture == nil {
return false, errors.New("材质不存在")
}
// 检查是否已收藏
isFavorited, err := repository.IsTextureFavorited(userID, textureID) isFavorited, err := repository.IsTextureFavorited(userID, textureID)
if err != nil { if err != nil {
return false, err return false, err
} }
if isFavorited { if isFavorited {
// 取消收藏 // 已收藏 -> 取消收藏
if err := repository.RemoveTextureFavorite(userID, textureID); err != nil { if err := repository.RemoveTextureFavorite(userID, textureID); err != nil {
return false, err return false, err
} }
@@ -213,7 +148,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
} }
return false, nil return false, nil
} else { } else {
// 添加收藏 // 未收藏 -> 添加收藏
if err := repository.AddTextureFavorite(userID, textureID); err != nil { if err := repository.AddTextureFavorite(userID, textureID); err != nil {
return false, err return false, err
} }
@@ -226,13 +161,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
// GetUserTextureFavorites 获取用户收藏的材质列表 // GetUserTextureFavorites 获取用户收藏的材质列表
func GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error) { func GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 { page, pageSize = NormalizePagination(page, pageSize)
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return repository.GetUserTextureFavorites(userID, page, pageSize) return repository.GetUserTextureFavorites(userID, page, pageSize)
} }
@@ -249,3 +178,15 @@ func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) err
return nil 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("无效的材质类型")
}
}

View File

@@ -4,7 +4,10 @@ import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/internal/repository" "carrotskin/internal/repository"
"carrotskin/pkg/auth" "carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"context"
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
) )
@@ -37,7 +40,12 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
// 确定头像URL优先使用用户提供的头像否则使用默认头像 // 确定头像URL优先使用用户提供的头像否则使用默认头像
avatarURL := avatar avatarURL := avatar
if avatarURL == "" { if avatarURL != "" {
// 验证用户提供的头像 URL 是否来自允许的域名
if err := ValidateAvatarURL(avatarURL); err != nil {
return nil, "", err
}
} else {
avatarURL = getDefaultAvatar() avatarURL = getDefaultAvatar()
} }
@@ -49,8 +57,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
Avatar: avatarURL, Avatar: avatarURL,
Role: "user", Role: "user",
Status: 1, Status: 1,
Points: 0, // 初始积分可以从配置读取 Points: 0,
// Properties 字段使用 datatypes.JSON默认为 nil数据库会存储 NULL
} }
if err := repository.CreateUser(user); err != nil { 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失败") return nil, "", errors.New("生成Token失败")
} }
// TODO: 添加注册奖励积分
return user, token, nil return user, token, nil
} }
// LoginUser 用户登录(支持用户名或邮箱登录) // LoginUser 用户登录(支持用户名或邮箱登录)
func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) { 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 user *model.User
var err error var err error
if strings.Contains(usernameOrEmail, "@") { if strings.Contains(usernameOrEmail, "@") {
// 包含@符号,认为是邮箱
user, err = repository.FindUserByEmail(usernameOrEmail) user, err = repository.FindUserByEmail(usernameOrEmail)
} else { } else {
// 否则认为是用户名
user, err = repository.FindUserByUsername(usernameOrEmail) user, err = repository.FindUserByUsername(usernameOrEmail)
} }
@@ -86,7 +105,16 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
return nil, "", err return nil, "", err
} }
if user == nil { 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, "用户不存在") logFailedLogin(0, ipAddress, userAgent, "用户不存在")
return nil, "", errors.New("用户名/邮箱或密码错误") return nil, "", errors.New("用户名/邮箱或密码错误")
} }
@@ -99,10 +127,26 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
// 验证密码 // 验证密码
if !auth.CheckPassword(user.Password, password) { 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, "密码错误") logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
return nil, "", errors.New("用户名/邮箱或密码错误") return nil, "", errors.New("用户名/邮箱或密码错误")
} }
// 登录成功,清除失败计数
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
_ = ClearLoginAttempts(ctx, redisClient, identifier)
}
// 生成JWT Token // 生成JWT Token
token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role) token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil { if err != nil {
@@ -141,24 +185,20 @@ func UpdateUserAvatar(userID int64, avatarURL string) error {
// ChangeUserPassword 修改密码 // ChangeUserPassword 修改密码
func ChangeUserPassword(userID int64, oldPassword, newPassword string) error { func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// 获取用户
user, err := repository.FindUserByID(userID) user, err := repository.FindUserByID(userID)
if err != nil { if err != nil {
return errors.New("用户不存在") return errors.New("用户不存在")
} }
// 验证旧密码
if !auth.CheckPassword(user.Password, oldPassword) { if !auth.CheckPassword(user.Password, oldPassword) {
return errors.New("原密码错误") return errors.New("原密码错误")
} }
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword) hashedPassword, err := auth.HashPassword(newPassword)
if err != nil { if err != nil {
return errors.New("密码加密失败") return errors.New("密码加密失败")
} }
// 更新密码
return repository.UpdateUserFields(userID, map[string]interface{}{ return repository.UpdateUserFields(userID, map[string]interface{}{
"password": hashedPassword, "password": hashedPassword,
}) })
@@ -166,19 +206,16 @@ func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// ResetUserPassword 重置密码(通过邮箱) // ResetUserPassword 重置密码(通过邮箱)
func ResetUserPassword(email, newPassword string) error { func ResetUserPassword(email, newPassword string) error {
// 查找用户
user, err := repository.FindUserByEmail(email) user, err := repository.FindUserByEmail(email)
if err != nil { if err != nil {
return errors.New("用户不存在") return errors.New("用户不存在")
} }
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword) hashedPassword, err := auth.HashPassword(newPassword)
if err != nil { if err != nil {
return errors.New("密码加密失败") return errors.New("密码加密失败")
} }
// 更新密码
return repository.UpdateUserFields(user.ID, map[string]interface{}{ return repository.UpdateUserFields(user.ID, map[string]interface{}{
"password": hashedPassword, "password": hashedPassword,
}) })
@@ -186,7 +223,6 @@ func ResetUserPassword(email, newPassword string) error {
// ChangeUserEmail 更换邮箱 // ChangeUserEmail 更换邮箱
func ChangeUserEmail(userID int64, newEmail string) error { func ChangeUserEmail(userID int64, newEmail string) error {
// 检查新邮箱是否已被使用
existingUser, err := repository.FindUserByEmail(newEmail) existingUser, err := repository.FindUserByEmail(newEmail)
if err != nil { if err != nil {
return err return err
@@ -195,7 +231,6 @@ func ChangeUserEmail(userID int64, newEmail string) error {
return errors.New("邮箱已被其他用户使用") return errors.New("邮箱已被其他用户使用")
} }
// 更新邮箱
return repository.UpdateUserFields(userID, map[string]interface{}{ return repository.UpdateUserFields(userID, map[string]interface{}{
"email": newEmail, "email": newEmail,
}) })
@@ -228,18 +263,40 @@ func logFailedLogin(userID int64, ipAddress, userAgent, reason string) {
// getDefaultAvatar 获取默认头像URL // getDefaultAvatar 获取默认头像URL
func getDefaultAvatar() string { func getDefaultAvatar() string {
// 如果数据库中不存在默认头像配置,返回错误信息
const log = "数据库中不存在默认头像配置"
// 尝试从数据库读取配置
config, err := repository.GetSystemConfigByKey("default_avatar") config, err := repository.GetSystemConfigByKey("default_avatar")
if err != nil || config == nil { if err != nil || config == nil || config.Value == "" {
return log return ""
} }
return config.Value 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) { func GetUserByEmail(email string) (*model.User, error) {
user, err := repository.FindUserByEmail(email) user, err := repository.FindUserByEmail(email)
if err != nil { if err != nil {
@@ -247,3 +304,31 @@ func GetUserByEmail(email string) (*model.User, error) {
} }
return user, nil 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
}

View File

@@ -91,21 +91,36 @@ func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, cod
return nil 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) codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码 // 从Redis获取验证码
storedCode, err := redisClient.Get(ctx, codeKey) storedCode, err := redisClient.Get(ctx, codeKey)
if err != nil { if err != nil {
// 记录失败尝试
RecordVerifyFailure(ctx, redisClient, email, codeType)
return fmt.Errorf("验证码已过期或不存在") return fmt.Errorf("验证码已过期或不存在")
} }
// 验证验证码 // 验证验证码
if storedCode != code { if storedCode != code {
// 记录失败尝试
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
remaining := MaxVerifyAttempts - count
if remaining > 0 {
return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining)
}
return fmt.Errorf("验证码错误") return fmt.Errorf("验证码错误")
} }
// 验证成功,删除验证码 // 验证成功,删除验证码和失败计数
_ = redisClient.Del(ctx, codeKey) _ = redisClient.Del(ctx, codeKey)
_ = ClearVerifyAttempts(ctx, redisClient, email, codeType)
return nil return nil
} }

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/internal/repository" "carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis" "carrotskin/pkg/redis"
"carrotskin/pkg/utils" "carrotskin/pkg/utils"
"context" "context"
@@ -54,7 +55,8 @@ func VerifyPassword(db *gorm.DB, password string, Id int64) error {
if err != nil { if err != nil {
return errors.New("未生成密码") return errors.New("未生成密码")
} }
if passwordStore != password { // 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return errors.New("密码错误") return errors.New("密码错误")
} }
return nil return nil
@@ -81,29 +83,36 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
// ResetYggdrasilPassword 重置并返回新的Yggdrasil密码 // ResetYggdrasilPassword 重置并返回新的Yggdrasil密码
func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) { func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) {
// 生成新的16位随机密码 // 生成新的16位随机密码(明文,返回给用户)
newPassword := model.GenerateRandomPassword(16) plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在 // 检查Yggdrasil记录是否存在
_, err := repository.GetYggdrasilPasswordById(userId) _, err = repository.GetYggdrasilPasswordById(userId)
if err != nil { if err != nil {
// 如果不存在,创建新记录 // 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{ yggdrasil := model.Yggdrasil{
ID: userId, ID: userId,
Password: newPassword, Password: hashedPassword,
} }
if err := db.Create(&yggdrasil).Error; err != nil { if err := db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err) 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 "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
} }
return newPassword, nil // 返回明文密码给用户
return plainPassword, nil
} }
// JoinServer 记录玩家加入服务器的会话信息 // JoinServer 记录玩家加入服务器的会话信息

156
pkg/database/seed.go Normal file
View File

@@ -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
}

View File

@@ -82,6 +82,11 @@ func (c *Client) Expire(ctx context.Context, key string, expiration time.Duratio
return c.Client.Expire(ctx, key, expiration).Err() 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 自增 // Incr 自增
func (c *Client) Incr(ctx context.Context, key string) (int64, error) { func (c *Client) Incr(ctx context.Context, key string) (int64, error) {
return c.Client.Incr(ctx, key).Result() return c.Client.Incr(ctx, key).Result()

View File

@@ -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);