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

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

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

View File

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

View File

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

View File

@@ -405,12 +405,8 @@ func SignOut(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
password, err := service.GetPasswordByUserId(db, user.ID)
if err != nil {
loggerInstance.Error("[ERROR] 邮箱查找失败", zap.Any("UserId:", user.ID), zap.Error(err))
}
// 验证密码
if password != request.Password {
if err := service.VerifyPassword(db, request.Password, user.ID); err != nil {
loggerInstance.Warn("[WARN] 登出失败: 密码错误", zap.Any("用户ID:", user.ID))
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
return

View File

@@ -7,18 +7,18 @@ import (
// AuditLog 审计日志模型
type AuditLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index" json:"user_id,omitempty"`
Action string `gorm:"column:action;type:varchar(100);not null;index" json:"action"`
ResourceType string `gorm:"column:resource_type;type:varchar(50);not null;index:idx_audit_logs_resource" json:"resource_type"`
ResourceID string `gorm:"column:resource_id;type:varchar(50);index:idx_audit_logs_resource" json:"resource_id,omitempty"`
UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"`
Action string `gorm:"column:action;type:varchar(100);not null;index:idx_audit_logs_action" json:"action"`
ResourceType string `gorm:"column:resource_type;type:varchar(50);not null;index:idx_audit_logs_resource,priority:1" json:"resource_type"`
ResourceID string `gorm:"column:resource_id;type:varchar(50);index:idx_audit_logs_resource,priority:2" json:"resource_id,omitempty"`
OldValues string `gorm:"column:old_values;type:jsonb" json:"old_values,omitempty"` // JSONB 格式
NewValues string `gorm:"column:new_values;type:jsonb" json:"new_values,omitempty"` // JSONB 格式
IPAddress string `gorm:"column:ip_address;type:inet;not null" json:"ip_address"`
IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_audit_logs_ip" json:"ip_address"`
UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_audit_logs_created_at,sort:desc" json:"created_at"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_audit_logs_user_created,priority:2,sort:desc;index:idx_audit_logs_created_at,sort:desc" json:"created_at"`
// 关联
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:SET NULL" json:"user,omitempty"`
}
// TableName 指定表名
@@ -29,13 +29,13 @@ func (AuditLog) TableName() string {
// CasbinRule Casbin 权限规则模型
type CasbinRule struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
PType string `gorm:"column:ptype;type:varchar(100);not null;index;uniqueIndex:uk_casbin_rule" json:"ptype"`
V0 string `gorm:"column:v0;type:varchar(100);not null;default:'';index;uniqueIndex:uk_casbin_rule" json:"v0"`
V1 string `gorm:"column:v1;type:varchar(100);not null;default:'';index;uniqueIndex:uk_casbin_rule" json:"v1"`
V2 string `gorm:"column:v2;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v2"`
V3 string `gorm:"column:v3;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v3"`
V4 string `gorm:"column:v4;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v4"`
V5 string `gorm:"column:v5;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule" json:"v5"`
PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"`
V0 string `gorm:"column:v0;type:varchar(100);not null;default:'';index:idx_casbin_v0;uniqueIndex:uk_casbin_rule,priority:2" json:"v0"`
V1 string `gorm:"column:v1;type:varchar(100);not null;default:'';index:idx_casbin_v1;uniqueIndex:uk_casbin_rule,priority:3" json:"v1"`
V2 string `gorm:"column:v2;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:4" json:"v2"`
V3 string `gorm:"column:v3;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:5" json:"v3"`
V4 string `gorm:"column:v4;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:6" json:"v4"`
V5 string `gorm:"column:v5;type:varchar(100);not null;default:'';uniqueIndex:uk_casbin_rule,priority:7" json:"v5"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
}

View File

@@ -7,20 +7,20 @@ import (
// Profile Minecraft 档案模型
type Profile struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"`
Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex" json:"name"` // Minecraft 角色名
SkinID *int64 `gorm:"column:skin_id;type:bigint" json:"skin_id,omitempty"`
CapeID *int64 `gorm:"column:cape_id;type:bigint" json:"cape_id,omitempty"`
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1;index:idx_profiles_user_active,priority:1" json:"user_id"`
Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex:idx_profiles_name" json:"name"` // Minecraft 角色名
SkinID *int64 `gorm:"column:skin_id;type:bigint;index:idx_profiles_skin_id" json:"skin_id,omitempty"`
CapeID *int64 `gorm:"column:cape_id;type:bigint;index:idx_profiles_cape_id" json:"cape_id,omitempty"`
RSAPrivateKey string `gorm:"column:rsa_private_key;type:text;not null" json:"-"` // RSA 私钥不返回给前端
IsActive bool `gorm:"column:is_active;not null;default:true;index" json:"is_active"`
LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp" json:"last_used_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
IsActive bool `gorm:"column:is_active;not null;default:true;index:idx_profiles_user_active,priority:2" json:"is_active"`
LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp;index:idx_profiles_last_used,sort:desc" json:"last_used_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_profiles_user_created,priority:2,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
// 关联
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Skin *Texture `gorm:"foreignKey:SkinID" json:"skin,omitempty"`
Cape *Texture `gorm:"foreignKey:CapeID" json:"cape,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Skin *Texture `gorm:"foreignKey:SkinID;constraint:OnDelete:SET NULL" json:"skin,omitempty"`
Cape *Texture `gorm:"foreignKey:CapeID;constraint:OnDelete:SET NULL" json:"cape,omitempty"`
}
// TableName 指定表名

View File

@@ -15,23 +15,23 @@ const (
// Texture 材质模型
type Texture struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UploaderID int64 `gorm:"column:uploader_id;not null;index" json:"uploader_id"`
UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"`
Name string `gorm:"column:name;type:varchar(100);not null;default:''" json:"name"`
Description string `gorm:"column:description;type:text" json:"description,omitempty"`
Type TextureType `gorm:"column:type;type:varchar(50);not null" json:"type"` // SKIN, CAPE
Type TextureType `gorm:"column:type;type:varchar(50);not null;index:idx_textures_public_type_status,priority:2" json:"type"` // SKIN, CAPE
URL string `gorm:"column:url;type:varchar(255);not null" json:"url"`
Hash string `gorm:"column:hash;type:varchar(64);not null;uniqueIndex" json:"hash"` // SHA-256
Hash string `gorm:"column:hash;type:varchar(64);not null;uniqueIndex:idx_textures_hash" json:"hash"` // SHA-256
Size int `gorm:"column:size;type:integer;not null;default:0" json:"size"`
IsPublic bool `gorm:"column:is_public;not null;default:false;index:idx_textures_public_type_status" json:"is_public"`
IsPublic bool `gorm:"column:is_public;not null;default:false;index:idx_textures_public_type_status,priority:1" json:"is_public"`
DownloadCount int `gorm:"column:download_count;type:integer;not null;default:0;index:idx_textures_download_count,sort:desc" json:"download_count"`
FavoriteCount int `gorm:"column:favorite_count;type:integer;not null;default:0;index:idx_textures_favorite_count,sort:desc" json:"favorite_count"`
IsSlim bool `gorm:"column:is_slim;not null;default:false" json:"is_slim"` // Alex(细) or Steve(粗)
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_textures_public_type_status" json:"status"` // 1:正常, 0:审核中, -1:已删除
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_textures_public_type_status,priority:3;index:idx_textures_uploader_status,priority:2" json:"status"` // 1:正常, 0:审核中, -1:已删除
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_textures_uploader_created,priority:2,sort:desc;index:idx_textures_created_at,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
// 关联
Uploader *User `gorm:"foreignKey:UploaderID" json:"uploader,omitempty"`
Uploader *User `gorm:"foreignKey:UploaderID;constraint:OnDelete:CASCADE" json:"uploader,omitempty"`
}
// TableName 指定表名
@@ -42,13 +42,13 @@ func (Texture) TableName() string {
// UserTextureFavorite 用户材质收藏
type UserTextureFavorite struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index;uniqueIndex:uk_user_texture" json:"user_id"`
TextureID int64 `gorm:"column:texture_id;not null;index;uniqueIndex:uk_user_texture" json:"texture_id"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index" json:"created_at"`
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"`
TextureID int64 `gorm:"column:texture_id;not null;uniqueIndex:uk_user_texture,priority:2;index:idx_favorites_texture_id" json:"texture_id"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_favorites_user_created,priority:2,sort:desc;index:idx_favorites_created_at,sort:desc" json:"created_at"`
// 关联
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Texture *Texture `gorm:"foreignKey:TextureID" json:"texture,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"`
}
// TableName 指定表名
@@ -59,15 +59,15 @@ func (UserTextureFavorite) TableName() string {
// TextureDownloadLog 材质下载记录
type TextureDownloadLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
TextureID int64 `gorm:"column:texture_id;not null;index" json:"texture_id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index" json:"user_id,omitempty"`
IPAddress string `gorm:"column:ip_address;type:inet;not null;index" json:"ip_address"`
TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_download_logs_user_id" json:"user_id,omitempty"`
IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_download_logs_ip" json:"ip_address"`
UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_download_logs_created_at,sort:desc" json:"created_at"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_download_logs_texture_created,priority:2,sort:desc;index:idx_download_logs_created_at,sort:desc" json:"created_at"`
// 关联
Texture *Texture `gorm:"foreignKey:TextureID" json:"texture,omitempty"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Texture *Texture `gorm:"foreignKey:TextureID;constraint:OnDelete:CASCADE" json:"texture,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:SET NULL" json:"user,omitempty"`
}
// TableName 指定表名

View File

@@ -2,13 +2,19 @@ package model
import "time"
// Token Yggdrasil 认证令牌模型
type Token struct {
AccessToken string `json:"_id"`
UserID int64 `json:"user_id"`
ClientToken string `json:"client_token"`
ProfileId string `json:"profile_id"`
Usable bool `json:"usable"`
IssueDate time.Time `json:"issue_date"`
AccessToken string `gorm:"column:access_token;type:varchar(64);primaryKey" json:"access_token"`
UserID int64 `gorm:"column:user_id;not null;index:idx_tokens_user_id" json:"user_id"`
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;index:idx_tokens_client_token" json:"client_token"`
ProfileId string `gorm:"column:profile_id;type:varchar(36);not null;index:idx_tokens_profile_id" json:"profile_id"`
Usable bool `gorm:"column:usable;not null;default:true;index:idx_tokens_usable" json:"usable"`
IssueDate time.Time `gorm:"column:issue_date;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_tokens_issue_date,sort:desc" json:"issue_date"`
// 关联
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Profile *Profile `gorm:"foreignKey:ProfileId;references:UUID;constraint:OnDelete:CASCADE" json:"profile,omitempty"`
}
func (Token) TableName() string { return "token" }
// TableName 指定表名
func (Token) TableName() string { return "tokens" }

View File

@@ -9,16 +9,16 @@ import (
// User 用户模型
type User struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex:idx_user_email_status,priority:1" json:"email"`
Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"`
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
}
@@ -30,20 +30,20 @@ func (User) TableName() string {
// UserPointLog 用户积分变更记录
type UserPointLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"`
ChangeType string `gorm:"column:change_type;type:varchar(50);not null" json:"change_type"` // EARN, SPEND, ADMIN_ADJUST
UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"`
ChangeType string `gorm:"column:change_type;type:varchar(50);not null;index:idx_point_logs_change_type" json:"change_type"` // EARN, SPEND, ADMIN_ADJUST
Amount int `gorm:"column:amount;type:integer;not null" json:"amount"`
BalanceBefore int `gorm:"column:balance_before;type:integer;not null" json:"balance_before"`
BalanceAfter int `gorm:"column:balance_after;type:integer;not null" json:"balance_after"`
Reason string `gorm:"column:reason;type:varchar(255);not null" json:"reason"`
ReferenceType string `gorm:"column:reference_type;type:varchar(50)" json:"reference_type,omitempty"`
ReferenceID *int64 `gorm:"column:reference_id;type:bigint" json:"reference_id,omitempty"`
OperatorID *int64 `gorm:"column:operator_id;type:bigint" json:"operator_id,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_point_logs_created_at,sort:desc" json:"created_at"`
OperatorID *int64 `gorm:"column:operator_id;type:bigint;index" json:"operator_id,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_point_logs_user_created,priority:2,sort:desc;index:idx_point_logs_created_at,sort:desc" json:"created_at"`
// 关联
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Operator *User `gorm:"foreignKey:OperatorID;constraint:OnDelete:SET NULL" json:"operator,omitempty"`
}
// TableName 指定表名
@@ -54,16 +54,16 @@ func (UserPointLog) TableName() string {
// UserLoginLog 用户登录日志
type UserLoginLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index" json:"user_id"`
IPAddress string `gorm:"column:ip_address;type:inet;not null;index" json:"ip_address"`
UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"`
IPAddress string `gorm:"column:ip_address;type:inet;not null;index:idx_login_logs_ip" json:"ip_address"`
UserAgent string `gorm:"column:user_agent;type:text" json:"user_agent,omitempty"`
LoginMethod string `gorm:"column:login_method;type:varchar(50);not null;default:'PASSWORD'" json:"login_method"`
IsSuccess bool `gorm:"column:is_success;not null;index" json:"is_success"`
IsSuccess bool `gorm:"column:is_success;not null;index:idx_login_logs_success" json:"is_success"`
FailureReason string `gorm:"column:failure_reason;type:varchar(255)" json:"failure_reason,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_login_logs_created_at,sort:desc" json:"created_at"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_login_logs_user_created,priority:2,sort:desc;index:idx_login_logs_created_at,sort:desc" json:"created_at"`
// 关联
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
}
// TableName 指定表名

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -4,7 +4,10 @@ import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"context"
"errors"
"fmt"
"strings"
"time"
)
@@ -37,7 +40,12 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
// 确定头像URL优先使用用户提供的头像否则使用默认头像
avatarURL := avatar
if avatarURL == "" {
if avatarURL != "" {
// 验证用户提供的头像 URL 是否来自允许的域名
if err := ValidateAvatarURL(avatarURL); err != nil {
return nil, "", err
}
} else {
avatarURL = getDefaultAvatar()
}
@@ -49,8 +57,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
Avatar: avatarURL,
Role: "user",
Status: 1,
Points: 0, // 初始积分可以从配置读取
// Properties 字段使用 datatypes.JSON默认为 nil数据库会存储 NULL
Points: 0,
}
if err := repository.CreateUser(user); err != nil {
@@ -63,22 +70,34 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
return nil, "", errors.New("生成Token失败")
}
// TODO: 添加注册奖励积分
return user, token, nil
}
// LoginUser 用户登录(支持用户名或邮箱登录)
func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
return LoginUserWithRateLimit(nil, jwtService, usernameOrEmail, password, ipAddress, userAgent)
}
// LoginUserWithRateLimit 用户登录(带频率限制)
func LoginUserWithRateLimit(redisClient *redis.Client, jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
ctx := context.Background()
// 检查账号是否被锁定(基于用户名/邮箱和IP
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
locked, ttl, err := CheckLoginLocked(ctx, redisClient, identifier)
if err == nil && locked {
return nil, "", fmt.Errorf("登录尝试次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
}
}
// 查找用户:判断是用户名还是邮箱
var user *model.User
var err error
if strings.Contains(usernameOrEmail, "@") {
// 包含@符号,认为是邮箱
user, err = repository.FindUserByEmail(usernameOrEmail)
} else {
// 否则认为是用户名
user, err = repository.FindUserByUsername(usernameOrEmail)
}
@@ -86,7 +105,16 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
return nil, "", err
}
if user == nil {
// 记录失败日志
// 记录失败尝试
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
remaining := MaxLoginAttempts - count
if remaining > 0 {
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
}
}
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
return nil, "", errors.New("用户名/邮箱或密码错误")
}
@@ -99,10 +127,26 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
// 验证密码
if !auth.CheckPassword(user.Password, password) {
// 记录失败尝试
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
remaining := MaxLoginAttempts - count
if remaining > 0 {
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
}
}
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
return nil, "", errors.New("用户名/邮箱或密码错误")
}
// 登录成功,清除失败计数
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
_ = ClearLoginAttempts(ctx, redisClient, identifier)
}
// 生成JWT Token
token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
@@ -141,24 +185,20 @@ func UpdateUserAvatar(userID int64, avatarURL string) error {
// ChangeUserPassword 修改密码
func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// 获取用户
user, err := repository.FindUserByID(userID)
if err != nil {
return errors.New("用户不存在")
}
// 验证旧密码
if !auth.CheckPassword(user.Password, oldPassword) {
return errors.New("原密码错误")
}
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
}
// 更新密码
return repository.UpdateUserFields(userID, map[string]interface{}{
"password": hashedPassword,
})
@@ -166,19 +206,16 @@ func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// ResetUserPassword 重置密码(通过邮箱)
func ResetUserPassword(email, newPassword string) error {
// 查找用户
user, err := repository.FindUserByEmail(email)
if err != nil {
return errors.New("用户不存在")
}
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
}
// 更新密码
return repository.UpdateUserFields(user.ID, map[string]interface{}{
"password": hashedPassword,
})
@@ -186,7 +223,6 @@ func ResetUserPassword(email, newPassword string) error {
// ChangeUserEmail 更换邮箱
func ChangeUserEmail(userID int64, newEmail string) error {
// 检查新邮箱是否已被使用
existingUser, err := repository.FindUserByEmail(newEmail)
if err != nil {
return err
@@ -195,7 +231,6 @@ func ChangeUserEmail(userID int64, newEmail string) error {
return errors.New("邮箱已被其他用户使用")
}
// 更新邮箱
return repository.UpdateUserFields(userID, map[string]interface{}{
"email": newEmail,
})
@@ -228,18 +263,40 @@ func logFailedLogin(userID int64, ipAddress, userAgent, reason string) {
// getDefaultAvatar 获取默认头像URL
func getDefaultAvatar() string {
// 如果数据库中不存在默认头像配置,返回错误信息
const log = "数据库中不存在默认头像配置"
// 尝试从数据库读取配置
config, err := repository.GetSystemConfigByKey("default_avatar")
if err != nil || config == nil {
return log
if err != nil || config == nil || config.Value == "" {
return ""
}
return config.Value
}
// ValidateAvatarURL 验证头像URL是否合法
func ValidateAvatarURL(avatarURL string) error {
if avatarURL == "" {
return nil
}
// 允许的域名列表
allowedDomains := []string{
"rustfs.example.com",
"localhost",
"127.0.0.1",
}
for _, domain := range allowedDomains {
if strings.Contains(avatarURL, domain) {
return nil
}
}
if strings.HasPrefix(avatarURL, "/") {
return nil
}
return errors.New("头像URL不在允许的域名列表中")
}
// GetUserByEmail 根据邮箱获取用户
func GetUserByEmail(email string) (*model.User, error) {
user, err := repository.FindUserByEmail(email)
if err != nil {
@@ -247,3 +304,31 @@ func GetUserByEmail(email string) (*model.User, error) {
}
return user, nil
}
// GetMaxProfilesPerUser 获取每用户最大档案数量配置
func GetMaxProfilesPerUser() int {
config, err := repository.GetSystemConfigByKey("max_profiles_per_user")
if err != nil || config == nil {
return 5
}
var value int
fmt.Sscanf(config.Value, "%d", &value)
if value <= 0 {
return 5
}
return value
}
// GetMaxTexturesPerUser 获取每用户最大材质数量配置
func GetMaxTexturesPerUser() int {
config, err := repository.GetSystemConfigByKey("max_textures_per_user")
if err != nil || config == nil {
return 50
}
var value int
fmt.Sscanf(config.Value, "%d", &value)
if value <= 0 {
return 50
}
return value
}

View File

@@ -91,21 +91,36 @@ func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, cod
return nil
}
// 检查是否被锁定
locked, ttl, err := CheckVerifyLocked(ctx, redisClient, email, codeType)
if err == nil && locked {
return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
}
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码
storedCode, err := redisClient.Get(ctx, codeKey)
if err != nil {
// 记录失败尝试
RecordVerifyFailure(ctx, redisClient, email, codeType)
return fmt.Errorf("验证码已过期或不存在")
}
// 验证验证码
if storedCode != code {
// 记录失败尝试
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
remaining := MaxVerifyAttempts - count
if remaining > 0 {
return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining)
}
return fmt.Errorf("验证码错误")
}
// 验证成功,删除验证码
// 验证成功,删除验证码和失败计数
_ = redisClient.Del(ctx, codeKey)
_ = ClearVerifyAttempts(ctx, redisClient, email, codeType)
return nil
}

View File

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