refactor: 移除全局单例、修复分层违规、统一错误与响应
Some checks failed
Build / build (push) Successful in 7m52s
Build / build-docker (push) Has been cancelled

This commit is contained in:
lafay
2026-06-15 16:40:36 +08:00
parent d9de39a0a3
commit 7d1c78f965
55 changed files with 593 additions and 1744 deletions

View File

@@ -72,13 +72,15 @@ type RedisData struct {
// captchaService CaptchaService的实现
type captchaService struct {
cfg *config.Config
redis *redis.Client
logger *zap.Logger
}
// NewCaptchaService 创建CaptchaService实例
func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaService {
func NewCaptchaService(cfg *config.Config, redisClient *redis.Client, logger *zap.Logger) CaptchaService {
return &captchaService{
cfg: cfg,
redis: redisClient,
logger: logger,
}
@@ -157,8 +159,7 @@ func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, capt
// Verify 验证验证码
func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
if s.cfg.IsTestEnvironment() {
return true, nil
}
@@ -197,8 +198,7 @@ func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (
// CheckVerified 检查验证码是否已验证仅检查captcha_id
func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (bool, error) {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
if s.cfg.IsTestEnvironment() {
return true, nil
}
@@ -216,8 +216,7 @@ func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (b
// ConsumeVerified 消耗已验证的验证码(注册成功后调用)
func (s *captchaService) ConsumeVerified(ctx context.Context, captchaID string) error {
// 测试环境下直接返回成功
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
if s.cfg.IsTestEnvironment() {
return nil
}

View File

@@ -1,18 +1,11 @@
package service
import (
"errors"
"fmt"
)
// 通用错误
var (
ErrProfileNotFound = errors.New("档案不存在")
ErrProfileNoPermission = errors.New("无权操作此档案")
ErrTextureNotFound = errors.New("材质不存在")
ErrTextureNoPermission = errors.New("无权操作此材质")
ErrUserNotFound = errors.New("用户不存在")
)
// 通用辅助函数。
//
// 注意:错误定义已统一到 internal/errors 包。
// 原先在此文件重复定义的 ErrProfileNotFound/ErrProfileNoPermission/
// ErrTextureNotFound/ErrTextureNoPermission/ErrUserNotFound 已删除,
// 请使用 internal/errors别名 apperrors中的对应定义。
// NormalizePagination 规范化分页参数
func NormalizePagination(page, pageSize int) (int, int) {
@@ -27,11 +20,3 @@ func NormalizePagination(page, pageSize int) (int, int) {
}
return page, pageSize
}
// WrapError 包装错误,添加上下文信息
func WrapError(err error, message string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", message, err)
}

View File

@@ -1,7 +1,6 @@
package service
import (
"errors"
"testing"
)
@@ -30,21 +29,3 @@ func TestNormalizePagination_Basic(t *testing.T) {
})
}
}
// TestWrapError 覆盖 WrapError 的 nil 与非 nil 分支
func TestWrapError(t *testing.T) {
if err := WrapError(nil, "msg"); err != nil {
t.Fatalf("WrapError(nil, ...) 应返回 nil, got=%v", err)
}
orig := errors.New("orig")
wrapped := WrapError(orig, "context")
if wrapped == nil {
t.Fatalf("WrapError 应返回非 nil 错误")
}
if wrapped.Error() == orig.Error() {
t.Fatalf("WrapError 应添加上下文信息, got=%v", wrapped)
}
}

View File

@@ -37,6 +37,11 @@ type UserService interface {
// 配置获取
GetMaxProfilesPerUser() int
GetMaxTexturesPerUser() int
// 管理员操作
ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error)
SetRole(ctx context.Context, userID int64, role string) error
SetStatus(ctx context.Context, userID int64, status int16) error
}
// ProfileService 档案服务接口
@@ -73,6 +78,11 @@ type TextureService interface {
// 限制检查
CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error
// 管理员操作
ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error)
AdminDelete(ctx context.Context, textureID int64) error
IncrementDownload(ctx context.Context, textureID int64) error
}
// TokenService 令牌服务接口

View File

@@ -130,6 +130,24 @@ func (m *MockUserRepository) FindByIDs(ctx context.Context, ids []int64) ([]*mod
return result, nil
}
// List 分页查询用户列表管理员mock 实现)
func (m *MockUserRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
var all []*model.User
for _, u := range m.users {
all = append(all, u)
}
total := int64(len(all))
offset := (page - 1) * pageSize
if offset >= len(all) {
return []*model.User{}, total, nil
}
end := offset + pageSize
if end > len(all) {
end = len(all)
}
return all[offset:end], total, nil
}
// MockProfileRepository 模拟ProfileRepository
type MockProfileRepository struct {
profiles map[string]*model.Profile
@@ -474,6 +492,32 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
return deleted, nil
}
// ListForAdmin 管理员列表mock 实现)
func (m *MockTextureRepository) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) {
var all []*model.Texture
for _, t := range m.textures {
all = append(all, t)
}
total := int64(len(all))
offset := (page - 1) * pageSize
if offset >= len(all) {
return []*model.Texture{}, total, nil
}
end := offset + pageSize
if end > len(all) {
end = len(all)
}
return all[offset:end], total, nil
}
// FindByIDForAdmin 管理员查询mock 实现,无权限过滤)
func (m *MockTextureRepository) FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) {
if t, ok := m.textures[id]; ok {
return t, nil
}
return nil, nil
}
// ============================================================================
// Service Mocks

View File

@@ -1,6 +1,7 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/database"
@@ -100,7 +101,7 @@ func (s *profileService) GetByUUID(ctx context.Context, uuid string) (*model.Pro
profile2, err := s.profileRepo.FindByUUID(ctx, uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrProfileNotFound
return nil, apperrors.ErrProfileNotFound
}
return nil, fmt.Errorf("查询档案失败: %w", err)
}
@@ -140,13 +141,13 @@ func (s *profileService) Update(ctx context.Context, uuid string, userID int64,
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrProfileNotFound
return nil, apperrors.ErrProfileNotFound
}
return nil, fmt.Errorf("查询档案失败: %w", err)
}
if profile.UserID != userID {
return nil, ErrProfileNoPermission
return nil, apperrors.ErrProfileNoPermission
}
// 检查角色名是否重复
@@ -187,13 +188,13 @@ func (s *profileService) Delete(ctx context.Context, uuid string, userID int64)
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrProfileNotFound
return apperrors.ErrProfileNotFound
}
return fmt.Errorf("查询档案失败: %w", err)
}
if profile.UserID != userID {
return ErrProfileNoPermission
return apperrors.ErrProfileNoPermission
}
if err := s.profileRepo.Delete(ctx, uuid); err != nil {

View File

@@ -53,7 +53,7 @@ func NewSignatureService(
}
// NewKeyPair 生成新的RSA密钥对
func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
func (s *SignatureService) NewKeyPair(ctx context.Context) (*model.KeyPair, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
if err != nil {
return nil, fmt.Errorf("生成RSA密钥对失败: %w", err)
@@ -85,7 +85,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
refresh := now.AddDate(0, 0, RefreshDays)
// 获取Yggdrasil根密钥并签名公钥
yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair()
yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
if err != nil {
return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err)
}
@@ -132,9 +132,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
}
// GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对
func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKey, error) {
ctx := context.Background()
func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (string, *rsa.PrivateKey, error) {
// 尝试从Redis获取密钥
publicKeyPEM, err := s.redis.Get(ctx, PublicKeyRedisKey)
if err == nil && publicKeyPEM != "" {
@@ -201,15 +199,14 @@ func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKe
}
// GetPublicKeyFromRedis 从Redis获取公钥
func (s *SignatureService) GetPublicKeyFromRedis() (string, error) {
ctx := context.Background()
func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, error) {
publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey)
if err != nil {
return "", fmt.Errorf("从Redis获取公钥失败: %w", err)
}
if publicKey == "" {
// 如果Redis中没有创建新的密钥对
publicKey, _, err = s.GetOrCreateYggdrasilKeyPair()
publicKey, _, err = s.GetOrCreateYggdrasilKeyPair(ctx)
if err != nil {
return "", fmt.Errorf("创建新密钥对失败: %w", err)
}
@@ -218,14 +215,13 @@ func (s *SignatureService) GetPublicKeyFromRedis() (string, error) {
}
// SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串
func (s *SignatureService) SignStringWithSHA1withRSA(data string) (string, error) {
ctx := context.Background()
func (s *SignatureService) SignStringWithSHA1withRSA(ctx context.Context, data string) (string, error) {
// 从Redis获取私钥
privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey)
if err != nil || privateKeyPEM == "" {
// 如果没有私钥,创建新的密钥对
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair()
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
if err != nil {
return "", fmt.Errorf("获取私钥失败: %w", err)
}

View File

@@ -2,6 +2,7 @@ package service
import (
"bytes"
apperrors "carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/database"
@@ -13,8 +14,10 @@ import (
"fmt"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
// textureService TextureService的实现
@@ -26,6 +29,7 @@ type textureService struct {
cacheKeys *database.CacheKeyBuilder
cacheInv *database.CacheInvalidator
logger *zap.Logger
db *gorm.DB
}
// NewTextureService 创建TextureService实例
@@ -35,6 +39,7 @@ func NewTextureService(
storageClient *storage.StorageClient,
cacheManager *database.CacheManager,
logger *zap.Logger,
db *gorm.DB,
) TextureService {
return &textureService{
textureRepo: textureRepo,
@@ -44,6 +49,7 @@ func NewTextureService(
cacheKeys: database.NewCacheKeyBuilder(""),
cacheInv: database.NewCacheInvalidator(cacheManager),
logger: logger,
db: db,
}
}
@@ -62,7 +68,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
return nil, apperrors.ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
@@ -80,7 +86,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
return nil, apperrors.ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
@@ -109,7 +115,7 @@ func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Tex
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
return nil, apperrors.ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
@@ -162,10 +168,10 @@ func (s *textureService) Update(ctx context.Context, textureID, uploaderID int64
return nil, err
}
if texture == nil {
return nil, ErrTextureNotFound
return nil, apperrors.ErrTextureNotFound
}
if texture.UploaderID != uploaderID {
return nil, ErrTextureNoPermission
return nil, apperrors.ErrTextureNoPermission
}
// 更新字段
@@ -200,10 +206,10 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64
return err
}
if texture == nil {
return ErrTextureNotFound
return apperrors.ErrTextureNotFound
}
if texture.UploaderID != uploaderID {
return ErrTextureNoPermission
return apperrors.ErrTextureNoPermission
}
err = s.textureRepo.Delete(ctx, textureID)
@@ -225,33 +231,41 @@ func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID i
return false, err
}
if texture == nil {
return false, ErrTextureNotFound
return false, apperrors.ErrTextureNotFound
}
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
// 在事务中执行"切换收藏 + 增减计数"两步,保证数据一致性
var favorited bool
err = database.TransactionWithTimeout(ctx, s.db, 5*time.Second, func(tx *gorm.DB) error {
isFav, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
if err != nil {
return err
}
if isFav {
// 已收藏 -> 取消收藏
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
return err
}
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
return err
}
favorited = false
return nil
}
// 未收藏 -> 添加收藏
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
return err
}
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
return err
}
favorited = true
return nil
})
if err != nil {
return false, err
}
if isFavorited {
// 已收藏 -> 取消收藏
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return false, nil
}
// 未收藏 -> 添加收藏
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return true, nil
return favorited, nil
}
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
@@ -277,7 +291,7 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, uploaderID)
if err != nil || user == nil {
return nil, ErrUserNotFound
return nil, apperrors.ErrUserNotFound
}
// 验证文件大小和扩展名
@@ -403,3 +417,28 @@ func parseTextureTypeInternal(textureType string) (model.TextureType, error) {
return "", errors.New("无效的材质类型")
}
}
// ==================== 管理员操作 ====================
// ListForAdmin 分页查询材质列表(管理员,含 Uploader 预加载)
func (s *textureService) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)
return s.textureRepo.ListForAdmin(ctx, page, pageSize)
}
// AdminDelete 管理员删除材质(无权限校验)
func (s *textureService) AdminDelete(ctx context.Context, textureID int64) error {
texture, err := s.textureRepo.FindByIDForAdmin(ctx, textureID)
if err != nil {
return err
}
if texture == nil {
return apperrors.ErrTextureNotFound
}
return s.textureRepo.Delete(ctx, textureID)
}
// IncrementDownload 增加下载计数CustomSkinAPI 等场景使用)
func (s *textureService) IncrementDownload(ctx context.Context, textureID int64) error {
return s.textureRepo.IncrementDownloadCount(ctx, textureID)
}

View File

@@ -494,7 +494,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
_ = userRepo.Create(context.Background(), testUser)
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
tests := []struct {
name string
@@ -617,7 +617,7 @@ func TestTextureServiceImpl_GetByID(t *testing.T) {
_ = textureRepo.Create(context.Background(), testTexture)
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
tests := []struct {
name string
@@ -675,7 +675,7 @@ func TestTextureServiceImpl_GetByUserID_And_Search(t *testing.T) {
}
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
ctx := context.Background()
@@ -714,7 +714,7 @@ func TestTextureServiceImpl_Update_And_Delete(t *testing.T) {
_ = textureRepo.Create(context.Background(), texture)
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
ctx := context.Background()
@@ -764,7 +764,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
}
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
ctx := context.Background()
@@ -807,7 +807,7 @@ func TestTextureServiceImpl_ToggleFavorite(t *testing.T) {
_ = textureRepo.Create(context.Background(), testTexture)
cacheManager := NewMockCacheManager()
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
ctx := context.Background()

View File

@@ -10,7 +10,6 @@ import (
"fmt"
"time"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
)
@@ -173,8 +172,9 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin
}
if err := s.tokenStore.Store(ctx, accessToken, metadata, ttl); err != nil {
s.logger.Warn("存储Token到Redis失败", zap.Error(err))
// 不返回错误因为JWT本身已经生成成功
// Redis 存储失败必须返回错误:否则 Validate 拿不到元数据Token 立即失效
s.logger.Error("存储Token到Redis失败", zap.Error(err))
return nil, nil, "", "", fmt.Errorf("存储Token失败: %w", err)
}
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
@@ -473,7 +473,7 @@ func (s *tokenServiceRedis) validateProfileByUserID(ctx context.Context, userID
profile, err := s.profileRepo.FindByUUID(ctx, UUID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
if repository.IsNotFound(err) {
return false, errors.New("配置文件不存在")
}
return false, fmt.Errorf("验证配置文件失败: %w", err)

View File

@@ -26,6 +26,7 @@ import (
// userService UserService的实现
type userService struct {
cfg *config.Config
userRepo repository.UserRepository
jwtService *auth.JWTService
redis *redis.Client
@@ -38,6 +39,7 @@ type userService struct {
// NewUserService 创建UserService实例
func NewUserService(
cfg *config.Config,
userRepo repository.UserRepository,
jwtService *auth.JWTService,
redisClient *redis.Client,
@@ -48,6 +50,7 @@ func NewUserService(
// CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀
// 这样缓存键的格式为: CacheManager前缀 + CacheKeyBuilder生成的键
return &userService{
cfg: cfg,
userRepo: userRepo,
jwtService: jwtService,
redis: redisClient,
@@ -81,7 +84,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a
// 加密密码
hashedPassword, err := auth.HashPassword(password)
if err != nil {
return nil, "", errors.New("密码加密失败")
return nil, "", fmt.Errorf("密码加密失败: %w", err)
}
// 确定头像URL
@@ -112,7 +115,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a
// 生成JWT Token
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
return nil, "", errors.New("生成Token失败")
return nil, "", fmt.Errorf("生成Token失败: %w", err)
}
return user, token, nil
@@ -167,15 +170,20 @@ func (s *userService) Login(ctx context.Context, usernameOrEmail, password, ipAd
// 生成JWT Token
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
return nil, "", errors.New("生成Token失败")
return nil, "", fmt.Errorf("生成Token失败: %w", err)
}
// 更新最后登录时间
// 更新最后登录时间(非关键路径,错误降级为 Warn
now := time.Now()
user.LastLoginAt = &now
_ = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
if err := s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
"last_login_at": now,
})
}); err != nil {
s.logger.Warn("更新最后登录时间失败",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
}
// 记录成功登录日志
s.logSuccessLogin(ctx, user.ID, ipAddress, userAgent)
@@ -239,7 +247,10 @@ func (s *userService) UpdateAvatar(ctx context.Context, userID int64, avatarURL
func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil || user == nil {
if err != nil {
return fmt.Errorf("查询用户失败: %w", err)
}
if user == nil {
return errors.New("用户不存在")
}
@@ -249,7 +260,7 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
return fmt.Errorf("密码加密失败: %w", err)
}
err = s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{
@@ -267,13 +278,16 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw
func (s *userService) ResetPassword(ctx context.Context, email, newPassword string) error {
user, err := s.userRepo.FindByEmail(ctx, email)
if err != nil || user == nil {
if err != nil {
return fmt.Errorf("查询用户失败: %w", err)
}
if user == nil {
return errors.New("用户不存在")
}
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
return fmt.Errorf("密码加密失败: %w", err)
}
err = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
@@ -350,14 +364,13 @@ func (s *userService) ValidateAvatarURL(ctx context.Context, avatarURL string) e
return errors.New("URL缺少主机名")
}
// 从配置获取允许的域名列表
cfg, err := config.GetConfig()
if err != nil {
allowedDomains := []string{"localhost", "127.0.0.1"}
return s.checkDomainAllowed(host, allowedDomains)
// 从注入的配置获取允许的域名列表
allowedDomains := s.cfg.Security.AllowedDomains
if len(allowedDomains) == 0 {
allowedDomains = []string{"localhost", "127.0.0.1"}
}
return s.checkDomainAllowed(host, cfg.Security.AllowedDomains)
return s.checkDomainAllowed(host, allowedDomains)
}
func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error) {
@@ -422,29 +435,23 @@ func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData [
}
func (s *userService) GetMaxProfilesPerUser() int {
cfg, err := config.GetConfig()
if err != nil || cfg.Site.MaxProfilesPerUser <= 0 {
if s.cfg.Site.MaxProfilesPerUser <= 0 {
return 5
}
return cfg.Site.MaxProfilesPerUser
return s.cfg.Site.MaxProfilesPerUser
}
func (s *userService) GetMaxTexturesPerUser() int {
cfg, err := config.GetConfig()
if err != nil || cfg.Site.MaxTexturesPerUser <= 0 {
if s.cfg.Site.MaxTexturesPerUser <= 0 {
return 50
}
return cfg.Site.MaxTexturesPerUser
return s.cfg.Site.MaxTexturesPerUser
}
// 私有辅助方法
func (s *userService) getDefaultAvatar() string {
cfg, err := config.GetConfig()
if err != nil {
return ""
}
return cfg.Site.DefaultAvatar
return s.cfg.Site.DefaultAvatar
}
func (s *userService) checkDomainAllowed(host string, allowedDomains []string) error {
@@ -505,3 +512,21 @@ func (s *userService) logFailedLogin(ctx context.Context, userID int64, ipAddres
}
_ = s.userRepo.CreateLoginLog(ctx, log)
}
// ==================== 管理员操作 ====================
// ListUsers 分页查询用户列表(管理员)
func (s *userService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)
return s.userRepo.List(ctx, page, pageSize)
}
// SetRole 设置用户角色(管理员)
func (s *userService) SetRole(ctx context.Context, userID int64, role string) error {
return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"role": role})
}
// SetStatus 设置用户状态(管理员)
func (s *userService) SetStatus(ctx context.Context, userID int64, status int16) error {
return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"status": status})
}

View File

@@ -3,6 +3,7 @@ package service
import (
"carrotskin/internal/model"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"context"
"testing"
@@ -14,11 +15,12 @@ func TestUserServiceImpl_Register(t *testing.T) {
userRepo := NewMockUserRepository()
jwtService := auth.NewJWTService("secret", 1)
logger := zap.NewNop()
cfg := &config.Config{}
// 初始化Service
// 注意redisClient 和 storageClient 传入 nil因为 Register 方法中没有使用它们
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -128,7 +130,8 @@ func TestUserServiceImpl_Login(t *testing.T) {
_ = userRepo.Create(context.Background(), testUser)
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
cfg := &config.Config{}
userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -208,7 +211,7 @@ func TestUserServiceImpl_BasicGettersAndUpdates(t *testing.T) {
_ = userRepo.Create(context.Background(), user)
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -255,7 +258,7 @@ func TestUserServiceImpl_ChangePassword(t *testing.T) {
_ = userRepo.Create(context.Background(), user)
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -289,7 +292,7 @@ func TestUserServiceImpl_ResetPassword(t *testing.T) {
_ = userRepo.Create(context.Background(), user)
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -316,7 +319,7 @@ func TestUserServiceImpl_ChangeEmail(t *testing.T) {
_ = userRepo.Create(context.Background(), user2)
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -338,7 +341,7 @@ func TestUserServiceImpl_ValidateAvatarURL(t *testing.T) {
logger := zap.NewNop()
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
ctx := context.Background()
@@ -374,7 +377,7 @@ func TestUserServiceImpl_MaxLimits(t *testing.T) {
// 未配置时走默认值
cacheManager := NewMockCacheManager()
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
if got := userService.GetMaxProfilesPerUser(); got != 5 {
t.Fatalf("GetMaxProfilesPerUser 默认值错误, got=%d", got)
}

View File

@@ -26,16 +26,19 @@ const (
// verificationService VerificationService的实现
type verificationService struct {
cfg *config.Config
redis *redis.Client
emailService *email.Service
}
// NewVerificationService 创建VerificationService实例
func NewVerificationService(
cfg *config.Config,
redisClient *redis.Client,
emailService *email.Service,
) VerificationService {
return &verificationService{
cfg: cfg,
redis: redisClient,
emailService: emailService,
}
@@ -44,8 +47,7 @@ func NewVerificationService(
// SendCode 发送验证码
func (s *verificationService) SendCode(ctx context.Context, email, codeType string) error {
// 测试环境下直接跳过,不存储也不发送
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
if s.cfg.IsTestEnvironment() {
return nil
}
@@ -89,8 +91,7 @@ func (s *verificationService) SendCode(ctx context.Context, email, codeType stri
// VerifyCode 验证验证码
func (s *verificationService) VerifyCode(ctx context.Context, email, code, codeType string) error {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
if s.cfg.IsTestEnvironment() {
return nil
}
@@ -166,8 +167,5 @@ func (s *verificationService) sendEmail(to, code, codeType string) error {
}
}
// DeleteVerificationCode 删除验证码(工具函数,保持向后兼容)
func DeleteVerificationCode(ctx context.Context, redisClient *redis.Client, email, codeType string) error {
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
return redisClient.Del(ctx, codeKey)
}
// DeleteVerificationCode 已移除:重构后无调用方。如需删除验证码,请使用
// VerificationService 实现内的 redis.Del 路径。

View File

@@ -9,14 +9,11 @@ import (
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilAuthService Yggdrasil认证服务实现
// 负责认证和密码管理
type yggdrasilAuthService struct {
db *gorm.DB
userRepo repository.UserRepository
yggdrasilRepo repository.YggdrasilRepository
logger *zap.Logger
@@ -24,13 +21,11 @@ type yggdrasilAuthService struct {
// NewYggdrasilAuthService 创建Yggdrasil认证服务实例内部使用
func NewYggdrasilAuthService(
db *gorm.DB,
userRepo repository.UserRepository,
yggdrasilRepo repository.YggdrasilRepository,
logger *zap.Logger,
) *yggdrasilAuthService {
return &yggdrasilAuthService{
db: db,
userRepo: userRepo,
yggdrasilRepo: yggdrasilRepo,
logger: logger,
@@ -78,7 +73,7 @@ func (s *yggdrasilAuthService) ResetYggdrasilPassword(ctx context.Context, userI
ID: userID,
Password: hashedPassword,
}
if err := s.db.Create(&yggdrasil).Error; err != nil {
if err := s.yggdrasilRepo.Create(ctx, &yggdrasil); err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return plainPassword, nil

View File

@@ -64,7 +64,7 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont
s.logger.Info("为用户创建新的密钥对",
zap.String("uuid", uuid),
)
keyPair, err = s.signatureService.NewKeyPair()
keyPair, err = s.signatureService.NewKeyPair(ctx)
if err != nil {
s.logger.Error("生成玩家证书密钥对失败",
zap.Error(err),
@@ -107,6 +107,6 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont
// GetPublicKey 获取公钥
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
return s.signatureService.GetPublicKeyFromRedis()
return s.signatureService.GetPublicKeyFromRedis(ctx)
}

View File

@@ -110,7 +110,7 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
// 只有在 unsigned=false 时才签名
var signature string
if !unsigned {
signature, err = s.signatureService.SignStringWithSHA1withRSA(textureData)
signature, err = s.signatureService.SignStringWithSHA1withRSA(ctx, textureData)
if err != nil {
s.logger.Error("签名textures失败",
zap.Error(err),

View File

@@ -10,8 +10,6 @@ import (
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilServiceComposite 组合服务,保持接口兼容性
@@ -28,20 +26,20 @@ type yggdrasilServiceComposite struct {
// NewYggdrasilServiceComposite 创建组合服务实例
func NewYggdrasilServiceComposite(
db *gorm.DB,
userRepo repository.UserRepository,
profileRepo repository.ProfileRepository,
yggdrasilRepo repository.YggdrasilRepository,
textureRepo repository.TextureRepository,
signatureService *SignatureService,
redisClient *redis.Client,
logger *zap.Logger,
tokenService TokenService, // 新增TokenService接口
) YggdrasilService {
// 创建各个专门的服务
authService := NewYggdrasilAuthService(db, userRepo, yggdrasilRepo, logger)
authService := NewYggdrasilAuthService(userRepo, yggdrasilRepo, logger)
sessionService := NewSessionService(redisClient, logger)
serializationService := NewSerializationService(
repository.NewTextureRepository(db),
textureRepo,
signatureService,
logger,
)