Compare commits
2 Commits
bdd2be5dc5
...
13bab28926
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13bab28926 | ||
|
|
10fdcd916b |
@@ -49,6 +49,11 @@ func main() {
|
||||
loggerInstance.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化种子数据
|
||||
if err := database.Seed(loggerInstance); err != nil {
|
||||
loggerInstance.Fatal("种子数据初始化失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化JWT服务
|
||||
if err := auth.Init(cfg.JWT); err != nil {
|
||||
loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err))
|
||||
|
||||
@@ -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
160
internal/handler/helpers.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "设置成功"})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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 指定表名
|
||||
|
||||
@@ -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 指定表名
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 指定表名
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
82
internal/repository/helpers.go
Normal file
82
internal/repository/helpers.go
Normal 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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/database"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,15 +11,13 @@ import (
|
||||
|
||||
// CreateProfile 创建档案
|
||||
func CreateProfile(profile *model.Profile) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Create(profile).Error
|
||||
return getDB().Create(profile).Error
|
||||
}
|
||||
|
||||
// FindProfileByUUID 根据UUID查找档案
|
||||
func FindProfileByUUID(uuid string) (*model.Profile, error) {
|
||||
db := database.MustGetDB()
|
||||
var profile model.Profile
|
||||
err := db.Where("uuid = ?", uuid).
|
||||
err := getDB().Where("uuid = ?", uuid).
|
||||
Preload("Skin").
|
||||
Preload("Cape").
|
||||
First(&profile).Error
|
||||
@@ -32,9 +29,8 @@ func FindProfileByUUID(uuid string) (*model.Profile, error) {
|
||||
|
||||
// FindProfileByName 根据角色名查找档案
|
||||
func FindProfileByName(name string) (*model.Profile, error) {
|
||||
db := database.MustGetDB()
|
||||
var profile model.Profile
|
||||
err := db.Where("name = ?", name).First(&profile).Error
|
||||
err := getDB().Where("name = ?", name).First(&profile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,44 +39,36 @@ func FindProfileByName(name string) (*model.Profile, error) {
|
||||
|
||||
// FindProfilesByUserID 获取用户的所有档案
|
||||
func FindProfilesByUserID(userID int64) ([]*model.Profile, error) {
|
||||
db := database.MustGetDB()
|
||||
var profiles []*model.Profile
|
||||
err := db.Where("user_id = ?", userID).
|
||||
err := getDB().Where("user_id = ?", userID).
|
||||
Preload("Skin").
|
||||
Preload("Cape").
|
||||
Order("created_at DESC").
|
||||
Find(&profiles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profiles, nil
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
// UpdateProfile 更新档案
|
||||
func UpdateProfile(profile *model.Profile) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Save(profile).Error
|
||||
return getDB().Save(profile).Error
|
||||
}
|
||||
|
||||
// UpdateProfileFields 更新指定字段
|
||||
func UpdateProfileFields(uuid string, updates map[string]interface{}) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Model(&model.Profile{}).
|
||||
return getDB().Model(&model.Profile{}).
|
||||
Where("uuid = ?", uuid).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteProfile 删除档案
|
||||
func DeleteProfile(uuid string) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Where("uuid = ?", uuid).Delete(&model.Profile{}).Error
|
||||
return getDB().Where("uuid = ?", uuid).Delete(&model.Profile{}).Error
|
||||
}
|
||||
|
||||
// CountProfilesByUserID 统计用户的档案数量
|
||||
func CountProfilesByUserID(userID int64) (int64, error) {
|
||||
db := database.MustGetDB()
|
||||
var count int64
|
||||
err := db.Model(&model.Profile{}).
|
||||
err := getDB().Model(&model.Profile{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
@@ -88,30 +76,22 @@ func CountProfilesByUserID(userID int64) (int64, error) {
|
||||
|
||||
// SetActiveProfile 设置档案为活跃状态(同时将用户的其他档案设置为非活跃)
|
||||
func SetActiveProfile(uuid string, userID int64) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// 将用户的所有档案设置为非活跃
|
||||
return getDB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Profile{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将指定档案设置为活跃
|
||||
if err := tx.Model(&model.Profile{}).
|
||||
return tx.Model(&model.Profile{}).
|
||||
Where("uuid = ? AND user_id = ?", uuid, userID).
|
||||
Update("is_active", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
Update("is_active", true).Error
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfileLastUsedAt 更新最后使用时间
|
||||
func UpdateProfileLastUsedAt(uuid string) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Model(&model.Profile{}).
|
||||
return getDB().Model(&model.Profile{}).
|
||||
Where("uuid = ?", uuid).
|
||||
Update("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error
|
||||
}
|
||||
@@ -122,53 +102,40 @@ func FindOneProfileByUserID(userID int64) (*model.Profile, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := profiles[0]
|
||||
return profile, nil
|
||||
if len(profiles) == 0 {
|
||||
return nil, errors.New("未找到角色")
|
||||
}
|
||||
return profiles[0], nil
|
||||
}
|
||||
|
||||
func GetProfilesByNames(names []string) ([]*model.Profile, error) {
|
||||
db := database.MustGetDB()
|
||||
var profiles []*model.Profile
|
||||
err := db.Where("name in (?)", names).Find(&profiles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profiles, nil
|
||||
err := getDB().Where("name in (?)", names).Find(&profiles).Error
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
func GetProfileKeyPair(profileId string) (*model.KeyPair, error) {
|
||||
db := database.MustGetDB()
|
||||
// 1. 参数校验(保持原逻辑)
|
||||
if profileId == "" {
|
||||
return nil, errors.New("参数不能为空")
|
||||
}
|
||||
|
||||
// 2. GORM 查询:只查询 key_pair 字段(对应原 mongo 投影)
|
||||
var profile *model.Profile
|
||||
// 条件:id = profileId(PostgreSQL 主键),只选择 key_pair 字段
|
||||
result := db.WithContext(context.Background()).
|
||||
Select("key_pair"). // 只查询需要的字段(投影)
|
||||
Where("id = ?", profileId). // 查询条件(GORM 自动处理占位符,避免 SQL 注入)
|
||||
First(&profile) // 查单条记录
|
||||
var profile model.Profile
|
||||
result := getDB().WithContext(context.Background()).
|
||||
Select("key_pair").
|
||||
Where("id = ?", profileId).
|
||||
First(&profile)
|
||||
|
||||
// 3. 错误处理(适配 GORM 错误类型)
|
||||
if result.Error != nil {
|
||||
// 空结果判断(对应原 mongo.ErrNoDocuments / pgx.ErrNoRows)
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
if IsNotFound(result.Error) {
|
||||
return nil, errors.New("key pair未找到")
|
||||
}
|
||||
// 保持原错误封装格式
|
||||
return nil, fmt.Errorf("获取key pair失败: %w", result.Error)
|
||||
}
|
||||
|
||||
// 4. JSONB 反序列化为 model.KeyPair
|
||||
keyPair := &model.KeyPair{}
|
||||
return keyPair, nil
|
||||
return &model.KeyPair{}, nil
|
||||
}
|
||||
|
||||
func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error {
|
||||
db := database.MustGetDB()
|
||||
// 仅保留最必要的入参校验(避免无效数据库请求)
|
||||
if profileId == "" {
|
||||
return errors.New("profileId 不能为空")
|
||||
}
|
||||
@@ -176,24 +143,18 @@ func UpdateProfileKeyPair(profileId string, keyPair *model.KeyPair) error {
|
||||
return errors.New("keyPair 不能为 nil")
|
||||
}
|
||||
|
||||
// 事务内执行核心更新(保证原子性,出错自动回滚)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// 核心更新逻辑:按 profileId 匹配,直接更新 key_pair 相关字段
|
||||
return getDB().Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.WithContext(context.Background()).
|
||||
Table("profiles"). // 目标表名(与 PostgreSQL 表一致)
|
||||
Where("id = ?", profileId). // 更新条件:profileId 匹配
|
||||
// 直接映射字段(无需序列化,依赖 GORM 自动字段匹配)
|
||||
Table("profiles").
|
||||
Where("id = ?", profileId).
|
||||
UpdateColumns(map[string]interface{}{
|
||||
"private_key": keyPair.PrivateKey, // 数据库 private_key 字段
|
||||
"public_key": keyPair.PublicKey, // 数据库 public_key 字段
|
||||
// 若 key_pair 是单个字段(非拆分),替换为:"key_pair": keyPair
|
||||
"private_key": keyPair.PrivateKey,
|
||||
"public_key": keyPair.PublicKey,
|
||||
})
|
||||
|
||||
// 仅处理数据库层面的致命错误
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("更新 keyPair 失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
105
internal/service/helpers.go
Normal 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
142
internal/service/security_service.go
Normal file
142
internal/service/security_service.go
Normal 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)
|
||||
}
|
||||
|
||||
@@ -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("无效的材质类型")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 @@ 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)
|
||||
// 检查是否触发锁定
|
||||
if count >= MaxLoginAttempts {
|
||||
logFailedLogin(0, ipAddress, userAgent, "用户不存在-账号已锁定")
|
||||
return nil, "", fmt.Errorf("登录失败次数过多,账号已被锁定 %d 分钟", int(LoginLockDuration.Minutes()))
|
||||
}
|
||||
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 +132,31 @@ 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)
|
||||
// 检查是否触发锁定
|
||||
if count >= MaxLoginAttempts {
|
||||
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误-账号已锁定")
|
||||
return nil, "", fmt.Errorf("登录失败次数过多,账号已被锁定 %d 分钟", int(LoginLockDuration.Minutes()))
|
||||
}
|
||||
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 +195,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 +216,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 +233,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 +241,6 @@ func ChangeUserEmail(userID int64, newEmail string) error {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
}
|
||||
|
||||
// 更新邮箱
|
||||
return repository.UpdateUserFields(userID, map[string]interface{}{
|
||||
"email": newEmail,
|
||||
})
|
||||
@@ -228,18 +273,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 +314,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
|
||||
}
|
||||
|
||||
@@ -91,21 +91,46 @@ 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 {
|
||||
// 记录失败尝试并检查是否触发锁定
|
||||
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
|
||||
if count >= MaxVerifyAttempts {
|
||||
return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes()))
|
||||
}
|
||||
remaining := MaxVerifyAttempts - count
|
||||
if remaining > 0 {
|
||||
return fmt.Errorf("验证码已过期或不存在,还剩 %d 次尝试机会", remaining)
|
||||
}
|
||||
return fmt.Errorf("验证码已过期或不存在")
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if storedCode != code {
|
||||
// 记录失败尝试并检查是否触发锁定
|
||||
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
|
||||
if count >= MaxVerifyAttempts {
|
||||
return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes()))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 记录玩家加入服务器的会话信息
|
||||
|
||||
156
pkg/database/seed.go
Normal file
156
pkg/database/seed.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 默认管理员配置
|
||||
const (
|
||||
defaultAdminUsername = "admin"
|
||||
defaultAdminEmail = "admin@example.com"
|
||||
defaultAdminPassword = "admin123456" // 首次登录后请立即修改
|
||||
)
|
||||
|
||||
// defaultSystemConfigs 默认系统配置
|
||||
var defaultSystemConfigs = []model.SystemConfig{
|
||||
{Key: "site_name", Value: "CarrotSkin", Description: "网站名称", Type: model.ConfigTypeString, IsPublic: true},
|
||||
{Key: "site_description", Value: "一个优秀的Minecraft皮肤站", Description: "网站描述", Type: model.ConfigTypeString, IsPublic: true},
|
||||
{Key: "registration_enabled", Value: "true", Description: "是否允许用户注册", Type: model.ConfigTypeBoolean, IsPublic: true},
|
||||
{Key: "checkin_reward", Value: "10", Description: "签到奖励积分", Type: model.ConfigTypeInteger, IsPublic: true},
|
||||
{Key: "texture_download_reward", Value: "1", Description: "材质被下载奖励积分", Type: model.ConfigTypeInteger, IsPublic: false},
|
||||
{Key: "max_textures_per_user", Value: "50", Description: "每个用户最大材质数量", Type: model.ConfigTypeInteger, IsPublic: false},
|
||||
{Key: "max_profiles_per_user", Value: "5", Description: "每个用户最大角色数量", Type: model.ConfigTypeInteger, IsPublic: false},
|
||||
{Key: "default_avatar", Value: "", Description: "默认头像URL", Type: model.ConfigTypeString, IsPublic: true},
|
||||
}
|
||||
|
||||
// defaultCasbinRules 默认Casbin权限规则
|
||||
var defaultCasbinRules = []model.CasbinRule{
|
||||
// 管理员拥有所有权限
|
||||
{PType: "p", V0: "admin", V1: "*", V2: "*"},
|
||||
// 普通用户权限
|
||||
{PType: "p", V0: "user", V1: "texture", V2: "create"},
|
||||
{PType: "p", V0: "user", V1: "texture", V2: "read"},
|
||||
{PType: "p", V0: "user", V1: "texture", V2: "update_own"},
|
||||
{PType: "p", V0: "user", V1: "texture", V2: "delete_own"},
|
||||
{PType: "p", V0: "user", V1: "profile", V2: "create"},
|
||||
{PType: "p", V0: "user", V1: "profile", V2: "read"},
|
||||
{PType: "p", V0: "user", V1: "profile", V2: "update_own"},
|
||||
{PType: "p", V0: "user", V1: "profile", V2: "delete_own"},
|
||||
{PType: "p", V0: "user", V1: "user", V2: "update_own"},
|
||||
// 角色继承:admin 继承 user 的所有权限
|
||||
{PType: "g", V0: "admin", V1: "user"},
|
||||
}
|
||||
|
||||
// Seed 初始化种子数据
|
||||
func Seed(logger *zap.Logger) error {
|
||||
db, err := GetDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("开始初始化种子数据...")
|
||||
|
||||
// 初始化默认管理员用户
|
||||
if err := seedAdminUser(db, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化系统配置
|
||||
if err := seedSystemConfigs(db, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化Casbin权限规则
|
||||
if err := seedCasbinRules(db, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("种子数据初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedAdminUser 初始化默认管理员用户
|
||||
func seedAdminUser(db *gorm.DB, logger *zap.Logger) error {
|
||||
// 检查是否已存在管理员用户
|
||||
var count int64
|
||||
if err := db.Model(&model.User{}).Where("role = ?", "admin").Count(&count).Error; err != nil {
|
||||
logger.Error("检查管理员用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已存在管理员,跳过创建
|
||||
if count > 0 {
|
||||
logger.Info("管理员用户已存在,跳过创建")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Error("密码加密失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建默认管理员
|
||||
admin := &model.User{
|
||||
Username: defaultAdminUsername,
|
||||
Email: defaultAdminEmail,
|
||||
Password: string(hashedPassword),
|
||||
Role: "admin",
|
||||
Status: 1,
|
||||
Points: 0,
|
||||
}
|
||||
|
||||
if err := db.Create(admin).Error; err != nil {
|
||||
logger.Error("创建管理员用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("默认管理员用户创建成功",
|
||||
zap.String("username", defaultAdminUsername),
|
||||
zap.String("email", defaultAdminEmail),
|
||||
)
|
||||
logger.Warn("请立即登录并修改默认管理员密码!默认密码请查看源码中的 defaultAdminPassword 常量")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedSystemConfigs 初始化系统配置
|
||||
func seedSystemConfigs(db *gorm.DB, logger *zap.Logger) error {
|
||||
for _, config := range defaultSystemConfigs {
|
||||
// 使用 FirstOrCreate 避免重复插入
|
||||
var existing model.SystemConfig
|
||||
result := db.Where("key = ?", config.Key).First(&existing)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
if err := db.Create(&config).Error; err != nil {
|
||||
logger.Error("创建系统配置失败", zap.String("key", config.Key), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Info("创建系统配置", zap.String("key", config.Key))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedCasbinRules 初始化Casbin权限规则
|
||||
func seedCasbinRules(db *gorm.DB, logger *zap.Logger) error {
|
||||
for _, rule := range defaultCasbinRules {
|
||||
// 检查规则是否已存在
|
||||
var existing model.CasbinRule
|
||||
query := db.Where("ptype = ? AND v0 = ? AND v1 = ? AND v2 = ?", rule.PType, rule.V0, rule.V1, rule.V2)
|
||||
result := query.First(&existing)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
logger.Error("创建Casbin规则失败", zap.String("ptype", rule.PType), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Info("创建Casbin规则", zap.String("ptype", rule.PType), zap.String("v0", rule.V0), zap.String("v1", rule.V1))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ func (c *Client) Expire(ctx context.Context, key string, expiration time.Duratio
|
||||
return c.Client.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
// TTL 获取键的剩余过期时间
|
||||
func (c *Client) TTL(ctx context.Context, key string) (time.Duration, error) {
|
||||
return c.Client.TTL(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Incr 自增
|
||||
func (c *Client) Incr(ctx context.Context, key string) (int64, error) {
|
||||
return c.Client.Incr(ctx, key).Result()
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
-- CarrotSkin PostgreSQL 数据库初始化脚本
|
||||
-- 创建数据库(可选,如果已经创建可跳过)
|
||||
--CREATE DATABASE carrotskin WITH ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8';
|
||||
|
||||
-- 用户表,支持积分系统和权限管理
|
||||
CREATE TABLE "user" (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||||
password VARCHAR(255) NOT NULL DEFAULT '',
|
||||
email VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||||
avatar VARCHAR(255) NOT NULL DEFAULT '',
|
||||
points INTEGER NOT NULL DEFAULT 0,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
last_login_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_user_role ON "user"(role);
|
||||
CREATE INDEX idx_user_status ON "user"(status);
|
||||
CREATE INDEX idx_user_points ON "user"(points DESC);
|
||||
|
||||
-- 用户表注释
|
||||
COMMENT ON TABLE "user" IS '用户表';
|
||||
COMMENT ON COLUMN "user".id IS '用户ID';
|
||||
COMMENT ON COLUMN "user".username IS '用户名';
|
||||
COMMENT ON COLUMN "user".password IS '密码哈希';
|
||||
COMMENT ON COLUMN "user".email IS '邮箱地址';
|
||||
COMMENT ON COLUMN "user".avatar IS '头像URL(存储在MinIO中)';
|
||||
COMMENT ON COLUMN "user".points IS '用户积分';
|
||||
COMMENT ON COLUMN "user".role IS '用户角色(user, admin等)';
|
||||
COMMENT ON COLUMN "user".status IS '用户状态(1:正常, 0:禁用, -1:删除)';
|
||||
COMMENT ON COLUMN "user".last_login_at IS '最后登录时间';
|
||||
COMMENT ON COLUMN "user".created_at IS '创建时间';
|
||||
COMMENT ON COLUMN "user".updated_at IS '更新时间';
|
||||
|
||||
-- 创建材质类型枚举
|
||||
CREATE TYPE texture_type AS ENUM ('SKIN', 'CAPE');
|
||||
|
||||
-- 材质表,存储皮肤和披风
|
||||
CREATE TABLE textures (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
uploader_id BIGINT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
type texture_type NOT NULL,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
download_count INTEGER NOT NULL DEFAULT 0,
|
||||
favorite_count INTEGER NOT NULL DEFAULT 0,
|
||||
is_slim BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_textures_uploader FOREIGN KEY (uploader_id) REFERENCES "user"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_textures_uploader_id ON textures(uploader_id);
|
||||
CREATE INDEX idx_textures_public_type_status ON textures(is_public, type, status);
|
||||
CREATE INDEX idx_textures_download_count ON textures(download_count DESC);
|
||||
CREATE INDEX idx_textures_favorite_count ON textures(favorite_count DESC);
|
||||
|
||||
-- 材质表注释
|
||||
COMMENT ON TABLE textures IS '皮肤与披风材质表';
|
||||
COMMENT ON COLUMN textures.id IS '材质的唯一ID';
|
||||
COMMENT ON COLUMN textures.uploader_id IS '上传者的用户ID';
|
||||
COMMENT ON COLUMN textures.name IS '材质名称';
|
||||
COMMENT ON COLUMN textures.description IS '材质描述';
|
||||
COMMENT ON COLUMN textures.type IS '材质类型(皮肤或披风)';
|
||||
COMMENT ON COLUMN textures.url IS '材质在MinIO中的永久访问URL';
|
||||
COMMENT ON COLUMN textures.hash IS '材质文件的SHA-256哈希值,用于快速去重和校验';
|
||||
COMMENT ON COLUMN textures.size IS '文件大小(字节)';
|
||||
COMMENT ON COLUMN textures.is_public IS '是否公开到皮肤广场';
|
||||
COMMENT ON COLUMN textures.download_count IS '下载次数';
|
||||
COMMENT ON COLUMN textures.favorite_count IS '收藏次数';
|
||||
COMMENT ON COLUMN textures.is_slim IS '是否为细手臂模型(Alex),默认为粗手臂模型(Steve)';
|
||||
COMMENT ON COLUMN textures.status IS '状态(1:正常, 0:审核中, -1:已删除)';
|
||||
COMMENT ON COLUMN textures.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN textures.updated_at IS '更新时间';
|
||||
|
||||
-- 用户材质收藏表
|
||||
CREATE TABLE user_texture_favorites (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
texture_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT uk_user_texture UNIQUE (user_id, texture_id),
|
||||
CONSTRAINT fk_favorites_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_favorites_texture FOREIGN KEY (texture_id) REFERENCES textures(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_favorites_user_id ON user_texture_favorites(user_id);
|
||||
CREATE INDEX idx_favorites_texture_id ON user_texture_favorites(texture_id);
|
||||
CREATE INDEX idx_favorites_created_at ON user_texture_favorites(created_at);
|
||||
|
||||
-- 收藏表注释
|
||||
COMMENT ON TABLE user_texture_favorites IS '用户材质收藏表';
|
||||
COMMENT ON COLUMN user_texture_favorites.id IS '收藏记录的唯一ID';
|
||||
COMMENT ON COLUMN user_texture_favorites.user_id IS '用户ID';
|
||||
COMMENT ON COLUMN user_texture_favorites.texture_id IS '收藏的材质ID';
|
||||
COMMENT ON COLUMN user_texture_favorites.created_at IS '收藏时间';
|
||||
|
||||
-- 用户角色信息表(Minecraft档案)
|
||||
CREATE TABLE profiles (
|
||||
uuid VARCHAR(36) PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
name VARCHAR(16) NOT NULL UNIQUE,
|
||||
skin_id BIGINT,
|
||||
cape_id BIGINT,
|
||||
rsa_private_key TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
last_used_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_profiles_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_profiles_skin FOREIGN KEY (skin_id) REFERENCES textures(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_profiles_cape FOREIGN KEY (cape_id) REFERENCES textures(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_profiles_user_id ON profiles(user_id);
|
||||
CREATE INDEX idx_profiles_active ON profiles(is_active);
|
||||
|
||||
-- 档案表注释
|
||||
COMMENT ON TABLE profiles IS '用户角色信息表(Minecraft档案)';
|
||||
COMMENT ON COLUMN profiles.uuid IS '角色的UUID,通常为Minecraft玩家的UUID';
|
||||
COMMENT ON COLUMN profiles.user_id IS '关联的用户ID';
|
||||
COMMENT ON COLUMN profiles.name IS '角色名(Minecraft游戏内名称)';
|
||||
COMMENT ON COLUMN profiles.skin_id IS '当前使用的皮肤ID';
|
||||
COMMENT ON COLUMN profiles.cape_id IS '当前使用的披风ID';
|
||||
COMMENT ON COLUMN profiles.rsa_private_key IS '用于签名的RSA-2048私钥(PEM格式)';
|
||||
COMMENT ON COLUMN profiles.is_active IS '是否为活跃档案';
|
||||
COMMENT ON COLUMN profiles.last_used_at IS '最后使用时间';
|
||||
COMMENT ON COLUMN profiles.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN profiles.updated_at IS '更新时间';
|
||||
|
||||
-- Casbin权限管理相关表
|
||||
CREATE TABLE casbin_rule (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ptype VARCHAR(100) NOT NULL,
|
||||
v0 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
v1 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
v2 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
v3 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
v4 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
v5 VARCHAR(100) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT uk_casbin_rule UNIQUE (ptype, v0, v1, v2, v3, v4, v5)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_casbin_ptype ON casbin_rule(ptype);
|
||||
CREATE INDEX idx_casbin_v0 ON casbin_rule(v0);
|
||||
CREATE INDEX idx_casbin_v1 ON casbin_rule(v1);
|
||||
|
||||
-- Casbin表注释
|
||||
COMMENT ON TABLE casbin_rule IS 'Casbin权限规则表';
|
||||
COMMENT ON COLUMN casbin_rule.ptype IS '策略类型(p, g等)';
|
||||
COMMENT ON COLUMN casbin_rule.v0 IS '主体(用户或角色)';
|
||||
COMMENT ON COLUMN casbin_rule.v1 IS '资源对象';
|
||||
COMMENT ON COLUMN casbin_rule.v2 IS '操作动作';
|
||||
|
||||
-- 创建变更类型枚举
|
||||
CREATE TYPE point_change_type AS ENUM ('EARN', 'SPEND', 'ADMIN_ADJUST');
|
||||
|
||||
-- 用户积分变更记录表
|
||||
CREATE TABLE user_point_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
change_type point_change_type NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
balance_before INTEGER NOT NULL,
|
||||
balance_after INTEGER NOT NULL,
|
||||
reason VARCHAR(255) NOT NULL,
|
||||
reference_type VARCHAR(50),
|
||||
reference_id BIGINT,
|
||||
operator_id BIGINT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_point_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_point_logs_operator FOREIGN KEY (operator_id) REFERENCES "user"(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_point_logs_user_id ON user_point_logs(user_id);
|
||||
CREATE INDEX idx_point_logs_created_at ON user_point_logs(created_at DESC);
|
||||
CREATE INDEX idx_point_logs_change_type ON user_point_logs(change_type);
|
||||
|
||||
-- 积分日志表注释
|
||||
COMMENT ON TABLE user_point_logs IS '用户积分变更记录表';
|
||||
|
||||
-- 创建配置类型枚举
|
||||
CREATE TYPE config_type AS ENUM ('STRING', 'INTEGER', 'BOOLEAN', 'JSON');
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE system_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
key VARCHAR(100) NOT NULL UNIQUE,
|
||||
value TEXT NOT NULL,
|
||||
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||
type config_type NOT NULL DEFAULT 'STRING',
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_system_config_public ON system_config(is_public);
|
||||
|
||||
-- 系统配置表注释
|
||||
COMMENT ON TABLE system_config IS '系统配置表';
|
||||
|
||||
-- 用户登录日志表
|
||||
CREATE TABLE user_login_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
ip_address INET NOT NULL,
|
||||
user_agent TEXT,
|
||||
login_method VARCHAR(50) NOT NULL DEFAULT 'PASSWORD',
|
||||
is_success BOOLEAN NOT NULL,
|
||||
failure_reason VARCHAR(255),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_login_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_login_logs_user_id ON user_login_logs(user_id);
|
||||
CREATE INDEX idx_login_logs_created_at ON user_login_logs(created_at DESC);
|
||||
CREATE INDEX idx_login_logs_ip_address ON user_login_logs(ip_address);
|
||||
CREATE INDEX idx_login_logs_success ON user_login_logs(is_success);
|
||||
|
||||
-- 登录日志表注释
|
||||
COMMENT ON TABLE user_login_logs IS '用户登录日志表';
|
||||
|
||||
-- 审计日志表
|
||||
CREATE TABLE audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id VARCHAR(50),
|
||||
old_values JSONB,
|
||||
new_values JSONB,
|
||||
ip_address INET NOT NULL,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_audit_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC);
|
||||
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
|
||||
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
|
||||
|
||||
-- 审计日志表注释
|
||||
COMMENT ON TABLE audit_logs IS '审计日志表';
|
||||
|
||||
-- 材质下载记录表(用于统计和防刷)
|
||||
CREATE TABLE texture_download_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
texture_id BIGINT NOT NULL,
|
||||
user_id BIGINT,
|
||||
ip_address INET NOT NULL,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_download_logs_texture FOREIGN KEY (texture_id) REFERENCES textures(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_download_logs_user FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_download_logs_texture_id ON texture_download_logs(texture_id);
|
||||
CREATE INDEX idx_download_logs_user_id ON texture_download_logs(user_id);
|
||||
CREATE INDEX idx_download_logs_created_at ON texture_download_logs(created_at DESC);
|
||||
CREATE INDEX idx_download_logs_ip_address ON texture_download_logs(ip_address);
|
||||
|
||||
-- 下载记录表注释
|
||||
COMMENT ON TABLE texture_download_logs IS '材质下载记录表';
|
||||
|
||||
-- 创建更新时间触发器函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为需要的表添加更新时间触发器
|
||||
CREATE TRIGGER update_user_updated_at BEFORE UPDATE ON "user"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_textures_updated_at BEFORE UPDATE ON textures
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 插入默认的系统配置
|
||||
INSERT INTO system_config (key, value, description, type, is_public) VALUES
|
||||
('site_name', 'CarrotSkin', '网站名称', 'STRING', TRUE),
|
||||
('site_description', '一个优秀的Minecraft皮肤站', '网站描述', 'STRING', TRUE),
|
||||
('registration_enabled', 'true', '是否允许用户注册', 'BOOLEAN', TRUE),
|
||||
('checkin_reward', '10', '签到奖励积分', 'INTEGER', TRUE),
|
||||
('texture_download_reward', '1', '材质被下载奖励积分', 'INTEGER', FALSE),
|
||||
('max_textures_per_user', '50', '每个用户最大材质数量', 'INTEGER', FALSE),
|
||||
('max_profiles_per_user', '5', '每个用户最大角色数量', 'INTEGER', FALSE),
|
||||
('default_avatar', 'https://carrotskin.com/assets/images/default-avatar.png', '默认头像', 'STRING', TRUE);
|
||||
|
||||
-- 插入默认的Casbin权限规则
|
||||
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES
|
||||
('p', 'admin', '*', '*'),
|
||||
('p', 'user', 'texture', 'create'),
|
||||
('p', 'user', 'texture', 'read'),
|
||||
('p', 'user', 'texture', 'update_own'),
|
||||
('p', 'user', 'texture', 'delete_own'),
|
||||
('p', 'user', 'profile', 'create'),
|
||||
('p', 'user', 'profile', 'read'),
|
||||
('p', 'user', 'profile', 'update_own'),
|
||||
('p', 'user', 'profile', 'delete_own'),
|
||||
('p', 'user', 'user', 'update_own'),
|
||||
('g', 'admin', 'user', '');
|
||||
|
||||
-- 插入默认的管理员
|
||||
INSERT INTO "user" (username, password, email, is_admin, created_at, updated_at) VALUES
|
||||
('admin', '$2a$10$...', 'admin@example.com', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
Reference in New Issue
Block a user