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) }