feat: 添加种子数据初始化功能,重构多个处理程序以简化错误响应和用户验证
This commit is contained in:
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,16 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
|
||||
return nil, "", err
|
||||
}
|
||||
if user == nil {
|
||||
// 记录失败日志
|
||||
// 记录失败尝试
|
||||
if redisClient != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
|
||||
remaining := MaxLoginAttempts - count
|
||||
if remaining > 0 {
|
||||
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
|
||||
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
|
||||
}
|
||||
}
|
||||
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
|
||||
return nil, "", errors.New("用户名/邮箱或密码错误")
|
||||
}
|
||||
@@ -99,10 +127,26 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
|
||||
|
||||
// 验证密码
|
||||
if !auth.CheckPassword(user.Password, password) {
|
||||
// 记录失败尝试
|
||||
if redisClient != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
|
||||
remaining := MaxLoginAttempts - count
|
||||
if remaining > 0 {
|
||||
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
|
||||
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
|
||||
}
|
||||
}
|
||||
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
|
||||
return nil, "", errors.New("用户名/邮箱或密码错误")
|
||||
}
|
||||
|
||||
// 登录成功,清除失败计数
|
||||
if redisClient != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
_ = ClearLoginAttempts(ctx, redisClient, identifier)
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
@@ -141,24 +185,20 @@ func UpdateUserAvatar(userID int64, avatarURL string) error {
|
||||
|
||||
// ChangeUserPassword 修改密码
|
||||
func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
|
||||
// 获取用户
|
||||
user, err := repository.FindUserByID(userID)
|
||||
if err != nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if !auth.CheckPassword(user.Password, oldPassword) {
|
||||
return errors.New("原密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("密码加密失败")
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
return repository.UpdateUserFields(userID, map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
})
|
||||
@@ -166,19 +206,16 @@ func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
|
||||
|
||||
// ResetUserPassword 重置密码(通过邮箱)
|
||||
func ResetUserPassword(email, newPassword string) error {
|
||||
// 查找用户
|
||||
user, err := repository.FindUserByEmail(email)
|
||||
if err != nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("密码加密失败")
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
return repository.UpdateUserFields(user.ID, map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
})
|
||||
@@ -186,7 +223,6 @@ func ResetUserPassword(email, newPassword string) error {
|
||||
|
||||
// ChangeUserEmail 更换邮箱
|
||||
func ChangeUserEmail(userID int64, newEmail string) error {
|
||||
// 检查新邮箱是否已被使用
|
||||
existingUser, err := repository.FindUserByEmail(newEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -195,7 +231,6 @@ func ChangeUserEmail(userID int64, newEmail string) error {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
}
|
||||
|
||||
// 更新邮箱
|
||||
return repository.UpdateUserFields(userID, map[string]interface{}{
|
||||
"email": newEmail,
|
||||
})
|
||||
@@ -228,18 +263,40 @@ func logFailedLogin(userID int64, ipAddress, userAgent, reason string) {
|
||||
|
||||
// getDefaultAvatar 获取默认头像URL
|
||||
func getDefaultAvatar() string {
|
||||
// 如果数据库中不存在默认头像配置,返回错误信息
|
||||
const log = "数据库中不存在默认头像配置"
|
||||
|
||||
// 尝试从数据库读取配置
|
||||
config, err := repository.GetSystemConfigByKey("default_avatar")
|
||||
if err != nil || config == nil {
|
||||
return log
|
||||
if err != nil || config == nil || config.Value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return config.Value
|
||||
}
|
||||
|
||||
// ValidateAvatarURL 验证头像URL是否合法
|
||||
func ValidateAvatarURL(avatarURL string) error {
|
||||
if avatarURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 允许的域名列表
|
||||
allowedDomains := []string{
|
||||
"rustfs.example.com",
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
}
|
||||
|
||||
for _, domain := range allowedDomains {
|
||||
if strings.Contains(avatarURL, domain) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(avatarURL, "/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("头像URL不在允许的域名列表中")
|
||||
}
|
||||
|
||||
// GetUserByEmail 根据邮箱获取用户
|
||||
func GetUserByEmail(email string) (*model.User, error) {
|
||||
user, err := repository.FindUserByEmail(email)
|
||||
if err != nil {
|
||||
@@ -247,3 +304,31 @@ func GetUserByEmail(email string) (*model.User, error) {
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetMaxProfilesPerUser 获取每用户最大档案数量配置
|
||||
func GetMaxProfilesPerUser() int {
|
||||
config, err := repository.GetSystemConfigByKey("max_profiles_per_user")
|
||||
if err != nil || config == nil {
|
||||
return 5
|
||||
}
|
||||
var value int
|
||||
fmt.Sscanf(config.Value, "%d", &value)
|
||||
if value <= 0 {
|
||||
return 5
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// GetMaxTexturesPerUser 获取每用户最大材质数量配置
|
||||
func GetMaxTexturesPerUser() int {
|
||||
config, err := repository.GetSystemConfigByKey("max_textures_per_user")
|
||||
if err != nil || config == nil {
|
||||
return 50
|
||||
}
|
||||
var value int
|
||||
fmt.Sscanf(config.Value, "%d", &value)
|
||||
if value <= 0 {
|
||||
return 50
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -91,21 +91,36 @@ func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, cod
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否被锁定
|
||||
locked, ttl, err := CheckVerifyLocked(ctx, redisClient, email, codeType)
|
||||
if err == nil && locked {
|
||||
return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
|
||||
}
|
||||
|
||||
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
|
||||
|
||||
// 从Redis获取验证码
|
||||
storedCode, err := redisClient.Get(ctx, codeKey)
|
||||
if err != nil {
|
||||
// 记录失败尝试
|
||||
RecordVerifyFailure(ctx, redisClient, email, codeType)
|
||||
return fmt.Errorf("验证码已过期或不存在")
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if storedCode != code {
|
||||
// 记录失败尝试
|
||||
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
|
||||
remaining := MaxVerifyAttempts - count
|
||||
if remaining > 0 {
|
||||
return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining)
|
||||
}
|
||||
return fmt.Errorf("验证码错误")
|
||||
}
|
||||
|
||||
// 验证成功,删除验证码
|
||||
// 验证成功,删除验证码和失败计数
|
||||
_ = redisClient.Del(ctx, codeKey)
|
||||
_ = ClearVerifyAttempts(ctx, redisClient, email, codeType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 记录玩家加入服务器的会话信息
|
||||
|
||||
Reference in New Issue
Block a user