2025-11-28 23:30:49 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"carrotskin/internal/model"
|
|
|
|
|
"carrotskin/internal/repository"
|
|
|
|
|
"carrotskin/pkg/auth"
|
2025-12-02 17:40:39 +08:00
|
|
|
"carrotskin/pkg/config"
|
2025-12-02 10:33:19 +08:00
|
|
|
"carrotskin/pkg/redis"
|
|
|
|
|
"context"
|
2025-11-28 23:30:49 +08:00
|
|
|
"errors"
|
2025-12-02 10:33:19 +08:00
|
|
|
"fmt"
|
2025-12-02 17:40:39 +08:00
|
|
|
"net/url"
|
2025-11-28 23:30:49 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
2025-11-28 23:30:49 +08:00
|
|
|
)
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
// 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) {
|
2025-11-28 23:30:49 +08:00
|
|
|
// 检查用户名是否已存在
|
2025-12-02 19:43:39 +08:00
|
|
|
existingUser, err := s.userRepo.FindByUsername(username)
|
2025-11-28 23:30:49 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
if existingUser != nil {
|
|
|
|
|
return nil, "", errors.New("用户名已存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查邮箱是否已存在
|
2025-12-02 19:43:39 +08:00
|
|
|
existingEmail, err := s.userRepo.FindByEmail(email)
|
2025-11-28 23:30:49 +08:00
|
|
|
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("密码加密失败")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
// 确定头像URL
|
2025-11-28 23:30:49 +08:00
|
|
|
avatarURL := avatar
|
2025-12-02 10:33:19 +08:00
|
|
|
if avatarURL != "" {
|
2025-12-02 19:43:39 +08:00
|
|
|
if err := s.ValidateAvatarURL(avatarURL); err != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-12-02 19:43:39 +08:00
|
|
|
avatarURL = s.getDefaultAvatar()
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建用户
|
|
|
|
|
user := &model.User{
|
|
|
|
|
Username: username,
|
|
|
|
|
Password: hashedPassword,
|
|
|
|
|
Email: email,
|
|
|
|
|
Avatar: avatarURL,
|
|
|
|
|
Role: "user",
|
|
|
|
|
Status: 1,
|
2025-12-02 10:33:19 +08:00
|
|
|
Points: 0,
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
if err := s.userRepo.Create(user); err != nil {
|
2025-11-28 23:30:49 +08:00
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成JWT Token
|
2025-12-02 19:43:39 +08:00
|
|
|
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
2025-11-28 23:30:49 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", errors.New("生成Token失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user, token, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) Login(usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
// 检查账号是否被锁定
|
|
|
|
|
if s.redis != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
identifier := usernameOrEmail + ":" + ipAddress
|
2025-12-02 19:43:39 +08:00
|
|
|
locked, ttl, err := CheckLoginLocked(ctx, s.redis, identifier)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err == nil && locked {
|
|
|
|
|
return nil, "", fmt.Errorf("登录尝试次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
// 查找用户
|
2025-11-28 23:30:49 +08:00
|
|
|
var user *model.User
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
if strings.Contains(usernameOrEmail, "@") {
|
2025-12-02 19:43:39 +08:00
|
|
|
user, err = s.userRepo.FindByEmail(usernameOrEmail)
|
2025-11-28 23:30:49 +08:00
|
|
|
} else {
|
2025-12-02 19:43:39 +08:00
|
|
|
user, err = s.userRepo.FindByUsername(usernameOrEmail)
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, 0, "用户不存在")
|
2025-11-28 23:30:49 +08:00
|
|
|
return nil, "", errors.New("用户名/邮箱或密码错误")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查用户状态
|
|
|
|
|
if user.Status != 1 {
|
2025-12-02 19:43:39 +08:00
|
|
|
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, user.ID, "账号已被禁用")
|
2025-11-28 23:30:49 +08:00
|
|
|
return nil, "", errors.New("账号已被禁用")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证密码
|
|
|
|
|
if !auth.CheckPassword(user.Password, password) {
|
2025-12-02 19:43:39 +08:00
|
|
|
s.recordLoginFailure(ctx, usernameOrEmail, ipAddress, userAgent, user.ID, "密码错误")
|
2025-11-28 23:30:49 +08:00
|
|
|
return nil, "", errors.New("用户名/邮箱或密码错误")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 登录成功,清除失败计数
|
2025-12-02 19:43:39 +08:00
|
|
|
if s.redis != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
identifier := usernameOrEmail + ":" + ipAddress
|
2025-12-02 19:43:39 +08:00
|
|
|
_ = ClearLoginAttempts(ctx, s.redis, identifier)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
// 生成JWT Token
|
2025-12-02 19:43:39 +08:00
|
|
|
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
2025-11-28 23:30:49 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", errors.New("生成Token失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新最后登录时间
|
|
|
|
|
now := time.Now()
|
|
|
|
|
user.LastLoginAt = &now
|
2025-12-02 19:43:39 +08:00
|
|
|
_ = s.userRepo.UpdateFields(user.ID, map[string]interface{}{
|
2025-11-28 23:30:49 +08:00
|
|
|
"last_login_at": now,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 记录成功登录日志
|
2025-12-02 19:43:39 +08:00
|
|
|
s.logSuccessLogin(user.ID, ipAddress, userAgent)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
|
|
|
return user, token, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) GetByID(id int64) (*model.User, error) {
|
|
|
|
|
return s.userRepo.FindByID(id)
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) GetByEmail(email string) (*model.User, error) {
|
|
|
|
|
return s.userRepo.FindByEmail(email)
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
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{}{
|
2025-11-28 23:30:49 +08:00
|
|
|
"avatar": avatarURL,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) ChangePassword(userID int64, oldPassword, newPassword string) error {
|
|
|
|
|
user, err := s.userRepo.FindByID(userID)
|
|
|
|
|
if err != nil || user == nil {
|
2025-11-28 23:30:49 +08:00
|
|
|
return errors.New("用户不存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !auth.CheckPassword(user.Password, oldPassword) {
|
|
|
|
|
return errors.New("原密码错误")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hashedPassword, err := auth.HashPassword(newPassword)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New("密码加密失败")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
return s.userRepo.UpdateFields(userID, map[string]interface{}{
|
2025-11-28 23:30:49 +08:00
|
|
|
"password": hashedPassword,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) ResetPassword(email, newPassword string) error {
|
|
|
|
|
user, err := s.userRepo.FindByEmail(email)
|
|
|
|
|
if err != nil || user == nil {
|
2025-11-28 23:30:49 +08:00
|
|
|
return errors.New("用户不存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hashedPassword, err := auth.HashPassword(newPassword)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New("密码加密失败")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
return s.userRepo.UpdateFields(user.ID, map[string]interface{}{
|
2025-11-28 23:30:49 +08:00
|
|
|
"password": hashedPassword,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) ChangeEmail(userID int64, newEmail string) error {
|
|
|
|
|
existingUser, err := s.userRepo.FindByEmail(newEmail)
|
2025-11-28 23:30:49 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if existingUser != nil && existingUser.ID != userID {
|
|
|
|
|
return errors.New("邮箱已被其他用户使用")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
return s.userRepo.UpdateFields(userID, map[string]interface{}{
|
2025-11-28 23:30:49 +08:00
|
|
|
"email": newEmail,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) ValidateAvatarURL(avatarURL string) error {
|
2025-12-02 10:33:19 +08:00
|
|
|
if avatarURL == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 17:40:39 +08:00
|
|
|
// 允许相对路径
|
|
|
|
|
if strings.HasPrefix(avatarURL, "/") {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析URL
|
2025-12-02 19:43:39 +08:00
|
|
|
parsedURL, err := url.Parse(avatarURL)
|
2025-12-02 17:40:39 +08:00
|
|
|
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"}
|
2025-12-02 19:43:39 +08:00
|
|
|
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
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
return value
|
|
|
|
|
}
|
2025-12-02 10:33:19 +08:00
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
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
|
2025-12-02 17:40:39 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
// 私有辅助方法
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-12-02 17:40:39 +08:00
|
|
|
host = strings.ToLower(host)
|
|
|
|
|
|
|
|
|
|
for _, allowed := range allowedDomains {
|
|
|
|
|
allowed = strings.ToLower(strings.TrimSpace(allowed))
|
|
|
|
|
if allowed == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if host == allowed {
|
2025-12-02 10:33:19 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 17:40:39 +08:00
|
|
|
if strings.HasPrefix(allowed, "*.") {
|
2025-12-02 19:43:39 +08:00
|
|
|
suffix := allowed[1:]
|
2025-12-02 17:40:39 +08:00
|
|
|
if strings.HasSuffix(host, suffix) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 17:40:39 +08:00
|
|
|
return errors.New("URL域名不在允许的列表中")
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
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
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
s.logFailedLogin(userID, ipAddress, userAgent, reason)
|
2025-11-28 23:30:49 +08:00
|
|
|
}
|
2025-12-02 10:33:19 +08:00
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
func (s *userServiceImpl) logSuccessLogin(userID int64, ipAddress, userAgent string) {
|
|
|
|
|
log := &model.UserLoginLog{
|
|
|
|
|
UserID: userID,
|
|
|
|
|
IPAddress: ipAddress,
|
|
|
|
|
UserAgent: userAgent,
|
|
|
|
|
LoginMethod: "PASSWORD",
|
|
|
|
|
IsSuccess: true,
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
_ = s.userRepo.CreateLoginLog(log)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
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,
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
_ = s.userRepo.CreateLoginLog(log)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|