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 (
|
import (
|
||||||
"carrotskin/internal/repository"
|
"carrotskin/internal/repository"
|
||||||
|
"carrotskin/internal/service"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
"carrotskin/pkg/storage"
|
"carrotskin/pkg/storage"
|
||||||
@@ -26,6 +27,12 @@ type Container struct {
|
|||||||
TextureRepo repository.TextureRepository
|
TextureRepo repository.TextureRepository
|
||||||
TokenRepo repository.TokenRepository
|
TokenRepo repository.TokenRepository
|
||||||
ConfigRepo repository.SystemConfigRepository
|
ConfigRepo repository.SystemConfigRepository
|
||||||
|
|
||||||
|
// Service层
|
||||||
|
UserService service.UserService
|
||||||
|
ProfileService service.ProfileService
|
||||||
|
TextureService service.TextureService
|
||||||
|
TokenService service.TokenService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContainer 创建依赖容器
|
// NewContainer 创建依赖容器
|
||||||
@@ -51,6 +58,12 @@ func NewContainer(
|
|||||||
c.TokenRepo = repository.NewTokenRepository(db)
|
c.TokenRepo = repository.NewTokenRepository(db)
|
||||||
c.ConfigRepo = repository.NewSystemConfigRepository(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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,3 +148,31 @@ func WithConfigRepo(repo repository.SystemConfigRepository) Option {
|
|||||||
c.ConfigRepo = repo
|
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": "设置成功"})
|
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