feat: Service层接口化
新增Service接口定义(internal/service/interfaces.go): - UserService: 用户认证、查询、更新等接口 - ProfileService: 档案CRUD、状态管理接口 - TextureService: 材质管理、收藏功能接口 - TokenService: 令牌生命周期管理接口 - VerificationService: 验证码服务接口 - CaptchaService: 滑动验证码接口 - UploadService: 上传服务接口 - YggdrasilService: Yggdrasil API接口 新增Service实现: - user_service_impl.go: 用户服务实现 - profile_service_impl.go: 档案服务实现 - texture_service_impl.go: 材质服务实现 - token_service_impl.go: 令牌服务实现 更新Container: - 添加Service层字段 - 初始化Service实例 - 添加With*Service选项函数 遵循Go最佳实践: - 接口定义与实现分离 - 依赖通过构造函数注入 - 便于单元测试mock
This commit is contained in:
@@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/storage"
|
||||
@@ -26,6 +27,12 @@ type Container struct {
|
||||
TextureRepo repository.TextureRepository
|
||||
TokenRepo repository.TokenRepository
|
||||
ConfigRepo repository.SystemConfigRepository
|
||||
|
||||
// Service层
|
||||
UserService service.UserService
|
||||
ProfileService service.ProfileService
|
||||
TextureService service.TextureService
|
||||
TokenService service.TokenService
|
||||
}
|
||||
|
||||
// NewContainer 创建依赖容器
|
||||
@@ -51,6 +58,12 @@ func NewContainer(
|
||||
c.TokenRepo = repository.NewTokenRepository(db)
|
||||
c.ConfigRepo = repository.NewSystemConfigRepository(db)
|
||||
|
||||
// 初始化Service
|
||||
c.UserService = service.NewUserService(c.UserRepo, c.ConfigRepo, jwtService, redisClient, logger)
|
||||
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, logger)
|
||||
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, logger)
|
||||
c.TokenService = service.NewTokenService(c.TokenRepo, c.ProfileRepo, logger)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -135,3 +148,31 @@ func WithConfigRepo(repo repository.SystemConfigRepository) Option {
|
||||
c.ConfigRepo = repo
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserService 设置用户服务
|
||||
func WithUserService(svc service.UserService) Option {
|
||||
return func(c *Container) {
|
||||
c.UserService = svc
|
||||
}
|
||||
}
|
||||
|
||||
// WithProfileService 设置档案服务
|
||||
func WithProfileService(svc service.ProfileService) Option {
|
||||
return func(c *Container) {
|
||||
c.ProfileService = svc
|
||||
}
|
||||
}
|
||||
|
||||
// WithTextureService 设置材质服务
|
||||
func WithTextureService(svc service.TextureService) Option {
|
||||
return func(c *Container) {
|
||||
c.TextureService = svc
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenService 设置令牌服务
|
||||
func WithTokenService(svc service.TokenService) Option {
|
||||
return func(c *Container) {
|
||||
c.TokenService = svc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,4 +244,3 @@ func (h *ProfileHandler) SetActive(c *gin.Context) {
|
||||
|
||||
RespondSuccess(c, gin.H{"message": "设置成功"})
|
||||
}
|
||||
|
||||
|
||||
144
internal/service/interfaces.go
Normal file
144
internal/service/interfaces.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Package service 定义业务逻辑层接口
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UserService 用户服务接口
|
||||
type UserService interface {
|
||||
// 用户认证
|
||||
Register(username, password, email, avatar string) (*model.User, string, error)
|
||||
Login(usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error)
|
||||
|
||||
// 用户查询
|
||||
GetByID(id int64) (*model.User, error)
|
||||
GetByEmail(email string) (*model.User, error)
|
||||
|
||||
// 用户更新
|
||||
UpdateInfo(user *model.User) error
|
||||
UpdateAvatar(userID int64, avatarURL string) error
|
||||
ChangePassword(userID int64, oldPassword, newPassword string) error
|
||||
ResetPassword(email, newPassword string) error
|
||||
ChangeEmail(userID int64, newEmail string) error
|
||||
|
||||
// URL验证
|
||||
ValidateAvatarURL(avatarURL string) error
|
||||
|
||||
// 配置获取
|
||||
GetMaxProfilesPerUser() int
|
||||
GetMaxTexturesPerUser() int
|
||||
}
|
||||
|
||||
// ProfileService 档案服务接口
|
||||
type ProfileService interface {
|
||||
// 档案CRUD
|
||||
Create(userID int64, name string) (*model.Profile, error)
|
||||
GetByUUID(uuid string) (*model.Profile, error)
|
||||
GetByUserID(userID int64) ([]*model.Profile, error)
|
||||
Update(uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error)
|
||||
Delete(uuid string, userID int64) error
|
||||
|
||||
// 档案状态
|
||||
SetActive(uuid string, userID int64) error
|
||||
CheckLimit(userID int64, maxProfiles int) error
|
||||
|
||||
// 批量查询
|
||||
GetByNames(names []string) ([]*model.Profile, error)
|
||||
GetByProfileName(name string) (*model.Profile, error)
|
||||
}
|
||||
|
||||
// TextureService 材质服务接口
|
||||
type TextureService interface {
|
||||
// 材质CRUD
|
||||
Create(uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
|
||||
GetByID(id int64) (*model.Texture, error)
|
||||
GetByUserID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
Search(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
Update(textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)
|
||||
Delete(textureID, uploaderID int64) error
|
||||
|
||||
// 收藏
|
||||
ToggleFavorite(userID, textureID int64) (bool, error)
|
||||
GetUserFavorites(userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
|
||||
// 限制检查
|
||||
CheckUploadLimit(uploaderID int64, maxTextures int) error
|
||||
}
|
||||
|
||||
// TokenService 令牌服务接口
|
||||
type TokenService interface {
|
||||
// 令牌管理
|
||||
Create(userID int64, uuid, clientToken string) (*model.Profile, []*model.Profile, string, string, error)
|
||||
Validate(accessToken, clientToken string) bool
|
||||
Refresh(accessToken, clientToken, selectedProfileID string) (string, string, error)
|
||||
Invalidate(accessToken string)
|
||||
InvalidateUserTokens(userID int64)
|
||||
|
||||
// 令牌查询
|
||||
GetUUIDByAccessToken(accessToken string) (string, error)
|
||||
GetUserIDByAccessToken(accessToken string) (int64, error)
|
||||
}
|
||||
|
||||
// VerificationService 验证码服务接口
|
||||
type VerificationService interface {
|
||||
SendCode(ctx context.Context, email, codeType string) error
|
||||
VerifyCode(ctx context.Context, email, code, codeType string) error
|
||||
}
|
||||
|
||||
// CaptchaService 滑动验证码服务接口
|
||||
type CaptchaService interface {
|
||||
Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error)
|
||||
Verify(ctx context.Context, dx int, captchaID string) (bool, error)
|
||||
}
|
||||
|
||||
// UploadService 上传服务接口
|
||||
type UploadService interface {
|
||||
GenerateAvatarUploadURL(ctx context.Context, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error)
|
||||
GenerateTextureUploadURL(ctx context.Context, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error)
|
||||
}
|
||||
|
||||
// YggdrasilService Yggdrasil服务接口
|
||||
type YggdrasilService interface {
|
||||
// 用户认证
|
||||
GetUserIDByEmail(email string) (int64, error)
|
||||
VerifyPassword(password string, userID int64) error
|
||||
|
||||
// 会话管理
|
||||
JoinServer(serverID, accessToken, selectedProfile, ip string) error
|
||||
HasJoinedServer(serverID, username, ip string) error
|
||||
|
||||
// 密码管理
|
||||
ResetYggdrasilPassword(userID int64) (string, error)
|
||||
|
||||
// 序列化
|
||||
SerializeProfile(profile model.Profile) map[string]interface{}
|
||||
SerializeUser(user *model.User, uuid string) map[string]interface{}
|
||||
|
||||
// 证书
|
||||
GeneratePlayerCertificate(uuid string) (map[string]interface{}, error)
|
||||
GetPublicKey() (string, error)
|
||||
}
|
||||
|
||||
// Services 服务集合
|
||||
type Services struct {
|
||||
User UserService
|
||||
Profile ProfileService
|
||||
Texture TextureService
|
||||
Token TokenService
|
||||
Verification VerificationService
|
||||
Captcha CaptchaService
|
||||
Upload UploadService
|
||||
Yggdrasil YggdrasilService
|
||||
}
|
||||
|
||||
// ServiceDeps 服务依赖
|
||||
type ServiceDeps struct {
|
||||
Logger *zap.Logger
|
||||
Storage *storage.StorageClient
|
||||
}
|
||||
|
||||
233
internal/service/profile_service_impl.go
Normal file
233
internal/service/profile_service_impl.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// profileServiceImpl ProfileService的实现
|
||||
type profileServiceImpl struct {
|
||||
profileRepo repository.ProfileRepository
|
||||
userRepo repository.UserRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProfileService 创建ProfileService实例
|
||||
func NewProfileService(
|
||||
profileRepo repository.ProfileRepository,
|
||||
userRepo repository.UserRepository,
|
||||
logger *zap.Logger,
|
||||
) ProfileService {
|
||||
return &profileServiceImpl{
|
||||
profileRepo: profileRepo,
|
||||
userRepo: userRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) Create(userID int64, name string) (*model.Profile, error) {
|
||||
// 验证用户存在
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
if user.Status != 1 {
|
||||
return nil, errors.New("用户状态异常")
|
||||
}
|
||||
|
||||
// 检查角色名是否已存在
|
||||
existingName, err := s.profileRepo.FindByName(name)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("查询角色名失败: %w", err)
|
||||
}
|
||||
if existingName != nil {
|
||||
return nil, errors.New("角色名已被使用")
|
||||
}
|
||||
|
||||
// 生成UUID和RSA密钥
|
||||
profileUUID := uuid.New().String()
|
||||
privateKey, err := generateRSAPrivateKeyInternal()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成RSA密钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建档案
|
||||
profile := &model.Profile{
|
||||
UUID: profileUUID,
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
RSAPrivateKey: privateKey,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Create(profile); err != nil {
|
||||
return nil, fmt.Errorf("创建档案失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置活跃状态
|
||||
if err := s.profileRepo.SetActive(profileUUID, userID); err != nil {
|
||||
return nil, fmt.Errorf("设置活跃状态失败: %w", err)
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) GetByUUID(uuid string) (*model.Profile, error) {
|
||||
profile, err := s.profileRepo.FindByUUID(uuid)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrProfileNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("查询档案失败: %w", err)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) GetByUserID(userID int64) ([]*model.Profile, error) {
|
||||
profiles, err := s.profileRepo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询档案列表失败: %w", err)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) Update(uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error) {
|
||||
// 获取档案并验证权限
|
||||
profile, err := s.profileRepo.FindByUUID(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
|
||||
}
|
||||
|
||||
// 检查角色名是否重复
|
||||
if name != nil && *name != profile.Name {
|
||||
existingName, err := s.profileRepo.FindByName(*name)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("查询角色名失败: %w", err)
|
||||
}
|
||||
if existingName != nil {
|
||||
return nil, errors.New("角色名已被使用")
|
||||
}
|
||||
profile.Name = *name
|
||||
}
|
||||
|
||||
// 更新皮肤和披风
|
||||
if skinID != nil {
|
||||
profile.SkinID = skinID
|
||||
}
|
||||
if capeID != nil {
|
||||
profile.CapeID = capeID
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Update(profile); err != nil {
|
||||
return nil, fmt.Errorf("更新档案失败: %w", err)
|
||||
}
|
||||
|
||||
return s.profileRepo.FindByUUID(uuid)
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) Delete(uuid string, userID int64) error {
|
||||
// 获取档案并验证权限
|
||||
profile, err := s.profileRepo.FindByUUID(uuid)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrProfileNotFound
|
||||
}
|
||||
return fmt.Errorf("查询档案失败: %w", err)
|
||||
}
|
||||
|
||||
if profile.UserID != userID {
|
||||
return ErrProfileNoPermission
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Delete(uuid); err != nil {
|
||||
return fmt.Errorf("删除档案失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) SetActive(uuid string, userID int64) error {
|
||||
// 获取档案并验证权限
|
||||
profile, err := s.profileRepo.FindByUUID(uuid)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrProfileNotFound
|
||||
}
|
||||
return fmt.Errorf("查询档案失败: %w", err)
|
||||
}
|
||||
|
||||
if profile.UserID != userID {
|
||||
return ErrProfileNoPermission
|
||||
}
|
||||
|
||||
if err := s.profileRepo.SetActive(uuid, userID); err != nil {
|
||||
return fmt.Errorf("设置活跃状态失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.profileRepo.UpdateLastUsedAt(uuid); err != nil {
|
||||
return fmt.Errorf("更新使用时间失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) CheckLimit(userID int64, maxProfiles int) error {
|
||||
count, err := s.profileRepo.CountByUserID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询档案数量失败: %w", err)
|
||||
}
|
||||
|
||||
if int(count) >= maxProfiles {
|
||||
return fmt.Errorf("已达到档案数量上限(%d个)", maxProfiles)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) GetByNames(names []string) ([]*model.Profile, error) {
|
||||
profiles, err := s.profileRepo.GetByNames(names)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查找失败: %w", err)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (s *profileServiceImpl) GetByProfileName(name string) (*model.Profile, error) {
|
||||
profile, err := s.profileRepo.FindByName(name)
|
||||
if err != nil {
|
||||
return nil, errors.New("用户角色未创建")
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// generateRSAPrivateKeyInternal 生成RSA-2048私钥(PEM格式)
|
||||
func generateRSAPrivateKeyInternal() (string, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
})
|
||||
|
||||
return string(privateKeyPEM), nil
|
||||
}
|
||||
|
||||
216
internal/service/texture_service_impl.go
Normal file
216
internal/service/texture_service_impl.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// textureServiceImpl TextureService的实现
|
||||
type textureServiceImpl struct {
|
||||
textureRepo repository.TextureRepository
|
||||
userRepo repository.UserRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTextureService 创建TextureService实例
|
||||
func NewTextureService(
|
||||
textureRepo repository.TextureRepository,
|
||||
userRepo repository.UserRepository,
|
||||
logger *zap.Logger,
|
||||
) TextureService {
|
||||
return &textureServiceImpl{
|
||||
textureRepo: textureRepo,
|
||||
userRepo: userRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) Create(uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) {
|
||||
// 验证用户存在
|
||||
user, err := s.userRepo.FindByID(uploaderID)
|
||||
if err != nil || user == nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// 检查Hash是否已存在
|
||||
existingTexture, err := s.textureRepo.FindByHash(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingTexture != nil {
|
||||
return nil, errors.New("该材质已存在")
|
||||
}
|
||||
|
||||
// 转换材质类型
|
||||
textureTypeEnum, err := parseTextureTypeInternal(textureType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建材质
|
||||
texture := &model.Texture{
|
||||
UploaderID: uploaderID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: textureTypeEnum,
|
||||
URL: url,
|
||||
Hash: hash,
|
||||
Size: size,
|
||||
IsPublic: isPublic,
|
||||
IsSlim: isSlim,
|
||||
Status: 1,
|
||||
DownloadCount: 0,
|
||||
FavoriteCount: 0,
|
||||
}
|
||||
|
||||
if err := s.textureRepo.Create(texture); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return texture, nil
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) GetByID(id int64) (*model.Texture, error) {
|
||||
texture, err := s.textureRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if texture == nil {
|
||||
return nil, ErrTextureNotFound
|
||||
}
|
||||
if texture.Status == -1 {
|
||||
return nil, errors.New("材质已删除")
|
||||
}
|
||||
return texture, nil
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) GetByUserID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||
page, pageSize = NormalizePagination(page, pageSize)
|
||||
return s.textureRepo.FindByUploaderID(uploaderID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) Search(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||
page, pageSize = NormalizePagination(page, pageSize)
|
||||
return s.textureRepo.Search(keyword, textureType, publicOnly, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) Update(textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) {
|
||||
// 获取材质并验证权限
|
||||
texture, err := s.textureRepo.FindByID(textureID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if texture == nil {
|
||||
return nil, ErrTextureNotFound
|
||||
}
|
||||
if texture.UploaderID != uploaderID {
|
||||
return nil, ErrTextureNoPermission
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
updates := make(map[string]interface{})
|
||||
if name != "" {
|
||||
updates["name"] = name
|
||||
}
|
||||
if description != "" {
|
||||
updates["description"] = description
|
||||
}
|
||||
if isPublic != nil {
|
||||
updates["is_public"] = *isPublic
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := s.textureRepo.UpdateFields(textureID, updates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s.textureRepo.FindByID(textureID)
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) Delete(textureID, uploaderID int64) error {
|
||||
// 获取材质并验证权限
|
||||
texture, err := s.textureRepo.FindByID(textureID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if texture == nil {
|
||||
return ErrTextureNotFound
|
||||
}
|
||||
if texture.UploaderID != uploaderID {
|
||||
return ErrTextureNoPermission
|
||||
}
|
||||
|
||||
return s.textureRepo.Delete(textureID)
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) ToggleFavorite(userID, textureID int64) (bool, error) {
|
||||
// 确保材质存在
|
||||
texture, err := s.textureRepo.FindByID(textureID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if texture == nil {
|
||||
return false, ErrTextureNotFound
|
||||
}
|
||||
|
||||
isFavorited, err := s.textureRepo.IsFavorited(userID, textureID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if isFavorited {
|
||||
// 已收藏 -> 取消收藏
|
||||
if err := s.textureRepo.RemoveFavorite(userID, textureID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := s.textureRepo.DecrementFavoriteCount(textureID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 未收藏 -> 添加收藏
|
||||
if err := s.textureRepo.AddFavorite(userID, textureID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := s.textureRepo.IncrementFavoriteCount(textureID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) GetUserFavorites(userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||
page, pageSize = NormalizePagination(page, pageSize)
|
||||
return s.textureRepo.GetUserFavorites(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *textureServiceImpl) CheckUploadLimit(uploaderID int64, maxTextures int) error {
|
||||
count, err := s.textureRepo.CountByUploaderID(uploaderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count >= int64(maxTextures) {
|
||||
return fmt.Errorf("已达到最大上传数量限制(%d)", maxTextures)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTextureTypeInternal 解析材质类型
|
||||
func parseTextureTypeInternal(textureType string) (model.TextureType, error) {
|
||||
switch textureType {
|
||||
case "SKIN":
|
||||
return model.TextureTypeSkin, nil
|
||||
case "CAPE":
|
||||
return model.TextureTypeCape, nil
|
||||
default:
|
||||
return "", errors.New("无效的材质类型")
|
||||
}
|
||||
}
|
||||
|
||||
278
internal/service/token_service_impl.go
Normal file
278
internal/service/token_service_impl.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// tokenServiceImpl TokenService的实现
|
||||
type tokenServiceImpl struct {
|
||||
tokenRepo repository.TokenRepository
|
||||
profileRepo repository.ProfileRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTokenService 创建TokenService实例
|
||||
func NewTokenService(
|
||||
tokenRepo repository.TokenRepository,
|
||||
profileRepo repository.ProfileRepository,
|
||||
logger *zap.Logger,
|
||||
) TokenService {
|
||||
return &tokenServiceImpl{
|
||||
tokenRepo: tokenRepo,
|
||||
profileRepo: profileRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
tokenExtendedTimeout = 10 * time.Second
|
||||
tokensMaxCount = 10
|
||||
)
|
||||
|
||||
func (s *tokenServiceImpl) Create(userID int64, UUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error) {
|
||||
var (
|
||||
selectedProfileID *model.Profile
|
||||
availableProfiles []*model.Profile
|
||||
)
|
||||
|
||||
// 设置超时上下文
|
||||
_, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 验证用户存在
|
||||
if UUID != "" {
|
||||
_, err := s.profileRepo.FindByUUID(UUID)
|
||||
if err != nil {
|
||||
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成令牌
|
||||
if clientToken == "" {
|
||||
clientToken = uuid.New().String()
|
||||
}
|
||||
|
||||
accessToken := uuid.New().String()
|
||||
token := model.Token{
|
||||
AccessToken: accessToken,
|
||||
ClientToken: clientToken,
|
||||
UserID: userID,
|
||||
Usable: true,
|
||||
IssueDate: time.Now(),
|
||||
}
|
||||
|
||||
// 获取用户配置文件
|
||||
profiles, err := s.profileRepo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果用户只有一个配置文件,自动选择
|
||||
if len(profiles) == 1 {
|
||||
selectedProfileID = profiles[0]
|
||||
token.ProfileId = selectedProfileID.UUID
|
||||
}
|
||||
availableProfiles = profiles
|
||||
|
||||
// 插入令牌
|
||||
err = s.tokenRepo.Create(&token)
|
||||
if err != nil {
|
||||
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("创建Token失败: %w", err)
|
||||
}
|
||||
|
||||
// 清理多余的令牌
|
||||
go s.checkAndCleanupExcessTokens(userID)
|
||||
|
||||
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) Validate(accessToken, clientToken string) bool {
|
||||
if accessToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
token, err := s.tokenRepo.FindByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !token.Usable {
|
||||
return false
|
||||
}
|
||||
|
||||
if clientToken == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return token.ClientToken == clientToken
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) Refresh(accessToken, clientToken, selectedProfileID string) (string, string, error) {
|
||||
if accessToken == "" {
|
||||
return "", "", errors.New("accessToken不能为空")
|
||||
}
|
||||
|
||||
// 查找旧令牌
|
||||
oldToken, err := s.tokenRepo.FindByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", "", errors.New("accessToken无效")
|
||||
}
|
||||
s.logger.Error("查询Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||||
return "", "", fmt.Errorf("查询令牌失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证profile
|
||||
if selectedProfileID != "" {
|
||||
valid, validErr := s.validateProfileByUserID(oldToken.UserID, selectedProfileID)
|
||||
if validErr != nil {
|
||||
s.logger.Error("验证Profile失败",
|
||||
zap.Error(err),
|
||||
zap.Int64("userId", oldToken.UserID),
|
||||
zap.String("profileId", selectedProfileID),
|
||||
)
|
||||
return "", "", fmt.Errorf("验证角色失败: %w", err)
|
||||
}
|
||||
if !valid {
|
||||
return "", "", errors.New("角色与用户不匹配")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 clientToken 是否有效
|
||||
if clientToken != "" && clientToken != oldToken.ClientToken {
|
||||
return "", "", errors.New("clientToken无效")
|
||||
}
|
||||
|
||||
// 检查 selectedProfileID 的逻辑
|
||||
if selectedProfileID != "" {
|
||||
if oldToken.ProfileId != "" && oldToken.ProfileId != selectedProfileID {
|
||||
return "", "", errors.New("原令牌已绑定角色,无法选择新角色")
|
||||
}
|
||||
} else {
|
||||
selectedProfileID = oldToken.ProfileId
|
||||
}
|
||||
|
||||
// 生成新令牌
|
||||
newAccessToken := uuid.New().String()
|
||||
newToken := model.Token{
|
||||
AccessToken: newAccessToken,
|
||||
ClientToken: oldToken.ClientToken,
|
||||
UserID: oldToken.UserID,
|
||||
Usable: true,
|
||||
ProfileId: selectedProfileID,
|
||||
IssueDate: time.Now(),
|
||||
}
|
||||
|
||||
// 先插入新令牌,再删除旧令牌
|
||||
err = s.tokenRepo.Create(&newToken)
|
||||
if err != nil {
|
||||
s.logger.Error("创建新Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||||
return "", "", fmt.Errorf("创建新Token失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.tokenRepo.DeleteByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
s.logger.Warn("删除旧Token失败,但新Token已创建",
|
||||
zap.Error(err),
|
||||
zap.String("oldToken", oldToken.AccessToken),
|
||||
zap.String("newToken", newAccessToken),
|
||||
)
|
||||
}
|
||||
|
||||
s.logger.Info("成功刷新Token", zap.Int64("userId", oldToken.UserID), zap.String("accessToken", newAccessToken))
|
||||
return newAccessToken, oldToken.ClientToken, nil
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) Invalidate(accessToken string) {
|
||||
if accessToken == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.tokenRepo.DeleteByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
s.logger.Error("删除Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||||
return
|
||||
}
|
||||
s.logger.Info("成功删除Token", zap.String("token", accessToken))
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) InvalidateUserTokens(userID int64) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.tokenRepo.DeleteByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("删除用户Token失败", zap.Error(err), zap.Int64("userId", userID))
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("成功删除用户Token", zap.Int64("userId", userID))
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) GetUUIDByAccessToken(accessToken string) (string, error) {
|
||||
return s.tokenRepo.GetUUIDByAccessToken(accessToken)
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) GetUserIDByAccessToken(accessToken string) (int64, error) {
|
||||
return s.tokenRepo.GetUserIDByAccessToken(accessToken)
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
func (s *tokenServiceImpl) checkAndCleanupExcessTokens(userID int64) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := s.tokenRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tokens) <= tokensMaxCount {
|
||||
return
|
||||
}
|
||||
|
||||
tokensToDelete := make([]string, 0, len(tokens)-tokensMaxCount)
|
||||
for i := tokensMaxCount; i < len(tokens); i++ {
|
||||
tokensToDelete = append(tokensToDelete, tokens[i].AccessToken)
|
||||
}
|
||||
|
||||
deletedCount, err := s.tokenRepo.BatchDelete(tokensToDelete)
|
||||
if err != nil {
|
||||
s.logger.Error("清理用户多余Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
||||
return
|
||||
}
|
||||
|
||||
if deletedCount > 0 {
|
||||
s.logger.Info("成功清理用户多余Token", zap.Int64("userId", userID), zap.Int64("count", deletedCount))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tokenServiceImpl) validateProfileByUserID(userID int64, UUID string) (bool, error) {
|
||||
if userID == 0 || UUID == "" {
|
||||
return false, errors.New("用户ID或配置文件ID不能为空")
|
||||
}
|
||||
|
||||
profile, err := s.profileRepo.FindByUUID(UUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, errors.New("配置文件不存在")
|
||||
}
|
||||
return false, fmt.Errorf("验证配置文件失败: %w", err)
|
||||
}
|
||||
return profile.UserID == userID, nil
|
||||
}
|
||||
|
||||
368
internal/service/user_service_impl.go
Normal file
368
internal/service/user_service_impl.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/redis"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// userServiceImpl UserService的实现
|
||||
type userServiceImpl struct {
|
||||
userRepo repository.UserRepository
|
||||
configRepo repository.SystemConfigRepository
|
||||
jwtService *auth.JWTService
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserService 创建UserService实例
|
||||
func NewUserService(
|
||||
userRepo repository.UserRepository,
|
||||
configRepo repository.SystemConfigRepository,
|
||||
jwtService *auth.JWTService,
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
) UserService {
|
||||
return &userServiceImpl{
|
||||
userRepo: userRepo,
|
||||
configRepo: configRepo,
|
||||
jwtService: jwtService,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) Register(username, password, email, avatar string) (*model.User, string, error) {
|
||||
// 检查用户名是否已存在
|
||||
existingUser, err := s.userRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, "", errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, err := s.userRepo.FindByEmail(email)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if existingEmail != nil {
|
||||
return nil, "", errors.New("邮箱已被注册")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("密码加密失败")
|
||||
}
|
||||
|
||||
// 确定头像URL
|
||||
avatarURL := avatar
|
||||
if avatarURL != "" {
|
||||
if err := s.ValidateAvatarURL(avatarURL); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
avatarURL = s.getDefaultAvatar()
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Password: hashedPassword,
|
||||
Email: email,
|
||||
Avatar: avatarURL,
|
||||
Role: "user",
|
||||
Status: 1,
|
||||
Points: 0,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("生成Token失败")
|
||||
}
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) Login(usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 检查账号是否被锁定
|
||||
if s.redis != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
locked, ttl, err := CheckLoginLocked(ctx, s.redis, 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 = s.userRepo.FindByEmail(usernameOrEmail)
|
||||
} else {
|
||||
user, err = s.userRepo.FindByUsername(usernameOrEmail)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if user == nil {
|
||||
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, 0, "用户不存在")
|
||||
return nil, "", errors.New("用户名/邮箱或密码错误")
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if user.Status != 1 {
|
||||
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, user.ID, "账号已被禁用")
|
||||
return nil, "", errors.New("账号已被禁用")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !auth.CheckPassword(user.Password, password) {
|
||||
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, user.ID, "密码错误")
|
||||
return nil, "", errors.New("用户名/邮箱或密码错误")
|
||||
}
|
||||
|
||||
// 登录成功,清除失败计数
|
||||
if s.redis != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
_ = ClearLoginAttempts(ctx, s.redis, identifier)
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("生成Token失败")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
now := time.Now()
|
||||
user.LastLoginAt = &now
|
||||
_ = s.userRepo.UpdateFields(user.ID, map[string]interface{}{
|
||||
"last_login_at": now,
|
||||
})
|
||||
|
||||
// 记录成功登录日志
|
||||
s.logSuccessLogin(user.ID, ipAddress, userAgent)
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetByID(id int64) (*model.User, error) {
|
||||
return s.userRepo.FindByID(id)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetByEmail(email string) (*model.User, error) {
|
||||
return s.userRepo.FindByEmail(email)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) UpdateInfo(user *model.User) error {
|
||||
return s.userRepo.Update(user)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) UpdateAvatar(userID int64, avatarURL string) error {
|
||||
return s.userRepo.UpdateFields(userID, map[string]interface{}{
|
||||
"avatar": avatarURL,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) ChangePassword(userID int64, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil || user == 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 s.userRepo.UpdateFields(userID, map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) ResetPassword(email, newPassword string) error {
|
||||
user, err := s.userRepo.FindByEmail(email)
|
||||
if err != nil || user == nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("密码加密失败")
|
||||
}
|
||||
|
||||
return s.userRepo.UpdateFields(user.ID, map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) ChangeEmail(userID int64, newEmail string) error {
|
||||
existingUser, err := s.userRepo.FindByEmail(newEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existingUser != nil && existingUser.ID != userID {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
}
|
||||
|
||||
return s.userRepo.UpdateFields(userID, map[string]interface{}{
|
||||
"email": newEmail,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) ValidateAvatarURL(avatarURL string) error {
|
||||
if avatarURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 允许相对路径
|
||||
if strings.HasPrefix(avatarURL, "/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析URL
|
||||
parsedURL, err := url.Parse(avatarURL)
|
||||
if err != nil {
|
||||
return errors.New("无效的URL格式")
|
||||
}
|
||||
|
||||
// 必须是HTTP或HTTPS协议
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return errors.New("URL必须使用http或https协议")
|
||||
}
|
||||
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return errors.New("URL缺少主机名")
|
||||
}
|
||||
|
||||
// 从配置获取允许的域名列表
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
allowedDomains := []string{"localhost", "127.0.0.1"}
|
||||
return s.checkDomainAllowed(host, allowedDomains)
|
||||
}
|
||||
|
||||
return s.checkDomainAllowed(host, cfg.Security.AllowedDomains)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetMaxProfilesPerUser() int {
|
||||
config, err := s.configRepo.GetByKey("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
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetMaxTexturesPerUser() int {
|
||||
config, err := s.configRepo.GetByKey("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
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
func (s *userServiceImpl) getDefaultAvatar() string {
|
||||
config, err := s.configRepo.GetByKey("default_avatar")
|
||||
if err != nil || config == nil || config.Value == "" {
|
||||
return ""
|
||||
}
|
||||
return config.Value
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) checkDomainAllowed(host string, allowedDomains []string) error {
|
||||
host = strings.ToLower(host)
|
||||
|
||||
for _, allowed := range allowedDomains {
|
||||
allowed = strings.ToLower(strings.TrimSpace(allowed))
|
||||
if allowed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if host == allowed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(allowed, "*.") {
|
||||
suffix := allowed[1:]
|
||||
if strings.HasSuffix(host, suffix) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("URL域名不在允许的列表中")
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) recordLoginFailure(ctx context.Context, usernameOrEmail, ipAddress, userAgent string, userID int64, reason string) {
|
||||
if s.redis != nil {
|
||||
identifier := usernameOrEmail + ":" + ipAddress
|
||||
count, _ := RecordLoginFailure(ctx, s.redis, identifier)
|
||||
if count >= MaxLoginAttempts {
|
||||
s.logFailedLogin(userID, ipAddress, userAgent, reason+"-账号已锁定")
|
||||
return
|
||||
}
|
||||
}
|
||||
s.logFailedLogin(userID, ipAddress, userAgent, reason)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) logSuccessLogin(userID int64, ipAddress, userAgent string) {
|
||||
log := &model.UserLoginLog{
|
||||
UserID: userID,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
LoginMethod: "PASSWORD",
|
||||
IsSuccess: true,
|
||||
}
|
||||
_ = s.userRepo.CreateLoginLog(log)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) logFailedLogin(userID int64, ipAddress, userAgent, reason string) {
|
||||
log := &model.UserLoginLog{
|
||||
UserID: userID,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
LoginMethod: "PASSWORD",
|
||||
IsSuccess: false,
|
||||
FailureReason: reason,
|
||||
}
|
||||
_ = s.userRepo.CreateLoginLog(log)
|
||||
}
|
||||
Reference in New Issue
Block a user