- Updated main.go to initialize email service and include it in the dependency injection container. - Refactored handlers to utilize context in service method calls, improving consistency and error handling. - Introduced new service options for upload, security, and captcha services, enhancing modularity and testability. - Removed unused repository implementations to streamline the codebase. This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
442 lines
11 KiB
Go
442 lines
11 KiB
Go
package service
|
|
|
|
import (
|
|
"carrotskin/internal/model"
|
|
"carrotskin/internal/repository"
|
|
"carrotskin/pkg/auth"
|
|
"carrotskin/pkg/config"
|
|
"carrotskin/pkg/database"
|
|
"carrotskin/pkg/redis"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// userService UserService的实现
|
|
type userService struct {
|
|
userRepo repository.UserRepository
|
|
configRepo repository.SystemConfigRepository
|
|
jwtService *auth.JWTService
|
|
redis *redis.Client
|
|
cache *database.CacheManager
|
|
cacheKeys *database.CacheKeyBuilder
|
|
cacheInv *database.CacheInvalidator
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewUserService 创建UserService实例
|
|
func NewUserService(
|
|
userRepo repository.UserRepository,
|
|
configRepo repository.SystemConfigRepository,
|
|
jwtService *auth.JWTService,
|
|
redisClient *redis.Client,
|
|
cacheManager *database.CacheManager,
|
|
logger *zap.Logger,
|
|
) UserService {
|
|
// CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀
|
|
// 这样缓存键的格式为: CacheManager前缀 + CacheKeyBuilder生成的键
|
|
return &userService{
|
|
userRepo: userRepo,
|
|
configRepo: configRepo,
|
|
jwtService: jwtService,
|
|
redis: redisClient,
|
|
cache: cacheManager,
|
|
cacheKeys: database.NewCacheKeyBuilder(""),
|
|
cacheInv: database.NewCacheInvalidator(cacheManager),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (s *userService) Register(ctx context.Context, 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(ctx, 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 *userService) Login(ctx context.Context, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
|
|
// 检查账号是否被锁定
|
|
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 *userService) GetByID(ctx context.Context, id int64) (*model.User, error) {
|
|
// 使用 Cached 装饰器自动处理缓存
|
|
cacheKey := s.cacheKeys.User(id)
|
|
return database.Cached(ctx, s.cache, cacheKey, func() (*model.User, error) {
|
|
return s.userRepo.FindByID(id)
|
|
}, 5*time.Minute)
|
|
}
|
|
|
|
func (s *userService) GetByEmail(ctx context.Context, email string) (*model.User, error) {
|
|
// 使用 Cached 装饰器自动处理缓存
|
|
cacheKey := s.cacheKeys.UserByEmail(email)
|
|
return database.Cached(ctx, s.cache, cacheKey, func() (*model.User, error) {
|
|
return s.userRepo.FindByEmail(email)
|
|
}, 5*time.Minute)
|
|
}
|
|
|
|
func (s *userService) UpdateInfo(ctx context.Context, user *model.User) error {
|
|
err := s.userRepo.Update(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 清除缓存
|
|
s.cacheInv.OnUpdate(ctx,
|
|
s.cacheKeys.User(user.ID),
|
|
s.cacheKeys.UserByEmail(user.Email),
|
|
s.cacheKeys.UserByUsername(user.Username),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) UpdateAvatar(ctx context.Context, userID int64, avatarURL string) error {
|
|
err := s.userRepo.UpdateFields(userID, map[string]interface{}{
|
|
"avatar": avatarURL,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 清除用户缓存
|
|
s.cacheInv.OnUpdate(ctx, s.cacheKeys.User(userID))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) ChangePassword(ctx context.Context, 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("密码加密失败")
|
|
}
|
|
|
|
err = s.userRepo.UpdateFields(userID, map[string]interface{}{
|
|
"password": hashedPassword,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 清除用户缓存
|
|
s.cacheInv.OnUpdate(ctx, s.cacheKeys.User(userID))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) ResetPassword(ctx context.Context, 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("密码加密失败")
|
|
}
|
|
|
|
err = s.userRepo.UpdateFields(user.ID, map[string]interface{}{
|
|
"password": hashedPassword,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 清除用户缓存
|
|
s.cacheInv.OnUpdate(ctx,
|
|
s.cacheKeys.User(user.ID),
|
|
s.cacheKeys.UserByEmail(email),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) ChangeEmail(ctx context.Context, userID int64, newEmail string) error {
|
|
// 获取旧邮箱
|
|
oldUser, _ := s.userRepo.FindByID(userID)
|
|
|
|
existingUser, err := s.userRepo.FindByEmail(newEmail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingUser != nil && existingUser.ID != userID {
|
|
return errors.New("邮箱已被其他用户使用")
|
|
}
|
|
|
|
err = s.userRepo.UpdateFields(userID, map[string]interface{}{
|
|
"email": newEmail,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 清除旧邮箱和用户ID的缓存
|
|
keysToInvalidate := []string{
|
|
s.cacheKeys.User(userID),
|
|
s.cacheKeys.UserByEmail(newEmail),
|
|
}
|
|
if oldUser != nil {
|
|
keysToInvalidate = append(keysToInvalidate, s.cacheKeys.UserByEmail(oldUser.Email))
|
|
}
|
|
s.cacheInv.OnUpdate(ctx, keysToInvalidate...)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *userService) ValidateAvatarURL(ctx context.Context, 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 *userService) 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 *userService) 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 *userService) getDefaultAvatar() string {
|
|
config, err := s.configRepo.GetByKey("default_avatar")
|
|
if err != nil || config == nil || config.Value == "" {
|
|
return ""
|
|
}
|
|
return config.Value
|
|
}
|
|
|
|
func (s *userService) 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 *userService) 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 *userService) 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 *userService) 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)
|
|
}
|