# Service Layer - CLAUDE.md 本文件为 Claude Code 在业务逻辑层中工作时提供指导。 ## 🎯 模块概览 Service 层是业务逻辑的核心,负责处理业务规则、数据转换和服务编排。 ### 主要职责 - 📋 业务逻辑处理和规则验证 - 🔄 数据转换和格式化 - 🧩 服务编排和组合 - 🚀 事务管理和数据一致性 - 🔐 业务权限控制 - 📊 性能优化和缓存策略 ## 📁 模块结构 ``` internal/service/ ├── CLAUDE.md # 📋 当前文件 - 业务逻辑开发指导 ├── interfaces.go # 🔗 服务接口定义 ├── auth/ # 🔐 认证服务 │ ├── auth_service.go # 认证业务逻辑 │ ├── jwt_service.go # JWT 令牌服务 │ └── auth_service_test.go # 认证服务测试 ├── user/ # 👤 用户服务 │ ├── user_service.go # 用户业务逻辑 │ ├── user_validator.go # 用户数据验证 │ └── user_service_test.go # 用户服务测试 ├── photo/ # 📸 照片服务 │ ├── photo_service.go # 照片业务逻辑 │ ├── photo_processor.go # 照片处理器 │ └── photo_service_test.go # 照片服务测试 ├── category/ # 📂 分类服务 │ ├── category_service.go # 分类业务逻辑 │ └── category_service_test.go # 分类服务测试 └── storage/ # 📁 存储服务 ├── storage_service.go # 存储业务逻辑 ├── local_storage.go # 本地存储实现 └── s3_storage.go # S3 存储实现 ``` ## 🔧 Go 风格服务设计 ### 接口定义规范 ```go // interfaces.go - 服务接口定义 package service import ( "context" "mime/multipart" "photography-backend/internal/model/dto" "photography-backend/internal/model/entity" ) // UserServicer 用户服务接口 type UserServicer interface { // 用户管理 CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) GetUser(ctx context.Context, id uint) (*entity.User, error) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) (*entity.User, error) DeleteUser(ctx context.Context, id uint) error ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error) // 用户验证 ValidateUser(ctx context.Context, email, password string) (*entity.User, error) ChangePassword(ctx context.Context, id uint, req *dto.ChangePasswordRequest) error } // PhotoServicer 照片服务接口 type PhotoServicer interface { // 照片管理 CreatePhoto(ctx context.Context, req *dto.CreatePhotoRequest) (*entity.Photo, error) GetPhoto(ctx context.Context, id uint) (*entity.Photo, error) UpdatePhoto(ctx context.Context, id uint, req *dto.UpdatePhotoRequest) (*entity.Photo, error) DeletePhoto(ctx context.Context, id uint) error ListPhotos(ctx context.Context, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) // 照片处理 UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) ProcessPhoto(ctx context.Context, photoID uint, opts *dto.ProcessPhotoOptions) error // 照片查询 GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) GetPhotosByCategory(ctx context.Context, categoryID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) SearchPhotos(ctx context.Context, query string, opts *dto.SearchPhotosOptions) ([]*entity.Photo, int64, error) } // AuthServicer 认证服务接口 type AuthServicer interface { // 认证 Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error) Register(ctx context.Context, req *dto.RegisterRequest) (*dto.RegisterResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error) Logout(ctx context.Context, token string) error // 令牌管理 ValidateToken(ctx context.Context, token string) (*dto.TokenClaims, error) RevokeToken(ctx context.Context, token string) error } ``` ### 服务实现规范 ```go // user/user_service.go - 用户服务实现 package user import ( "context" "errors" "fmt" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "photography-backend/internal/model/dto" "photography-backend/internal/model/entity" "photography-backend/internal/repository" "photography-backend/pkg/logger" ) type UserService struct { userRepo repository.UserRepositoryr logger logger.Logger } func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) *UserService { return &UserService{ userRepo: userRepo, logger: logger, } } func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) { // 1. 验证输入 if err := s.validateCreateUserRequest(req); err != nil { s.logger.Error("invalid create user request", zap.Error(err)) return nil, err } // 2. 检查用户是否存在 existingUser, err := s.userRepo.GetByEmail(ctx, req.Email) if err != nil && !errors.Is(err, repository.ErrNotFound) { s.logger.Error("failed to check existing user", zap.Error(err)) return nil, err } if existingUser != nil { return nil, errors.New("user already exists") } // 3. 加密密码 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { s.logger.Error("failed to hash password", zap.Error(err)) return nil, err } // 4. 创建用户实体 user := &entity.User{ Email: req.Email, Username: req.Username, Password: string(hashedPassword), Role: entity.UserRoleUser, Status: entity.UserStatusActive, } // 5. 保存用户 createdUser, err := s.userRepo.Create(ctx, user) if err != nil { s.logger.Error("failed to create user", zap.Error(err)) return nil, err } s.logger.Info("user created successfully", zap.String("user_id", fmt.Sprintf("%d", createdUser.ID)), zap.String("email", createdUser.Email)) return createdUser, nil } func (s *UserService) GetUser(ctx context.Context, id uint) (*entity.User, error) { user, err := s.userRepo.GetByID(ctx, id) if err != nil { if errors.Is(err, repository.ErrNotFound) { return nil, errors.New("user not found") } s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", id)) return nil, err } return user, nil } func (s *UserService) ValidateUser(ctx context.Context, email, password string) (*entity.User, error) { // 1. 获取用户 user, err := s.userRepo.GetByEmail(ctx, email) if err != nil { if errors.Is(err, repository.ErrNotFound) { return nil, errors.New("invalid credentials") } s.logger.Error("failed to get user by email", zap.Error(err)) return nil, err } // 2. 验证密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { return nil, errors.New("invalid credentials") } // 3. 检查用户状态 if user.Status != entity.UserStatusActive { return nil, errors.New("user account is not active") } return user, nil } func (s *UserService) validateCreateUserRequest(req *dto.CreateUserRequest) error { if req.Email == "" { return errors.New("email is required") } if req.Username == "" { return errors.New("username is required") } if req.Password == "" { return errors.New("password is required") } if len(req.Password) < 6 { return errors.New("password must be at least 6 characters") } return nil } ``` ### 照片服务实现 ```go // photo/photo_service.go - 照片服务实现 package photo import ( "context" "errors" "fmt" "mime/multipart" "path/filepath" "strings" "time" "go.uber.org/zap" "photography-backend/internal/model/dto" "photography-backend/internal/model/entity" "photography-backend/internal/repository" "photography-backend/internal/service" "photography-backend/pkg/logger" ) type PhotoService struct { photoRepo repository.PhotoRepositoryr userRepo repository.UserRepositoryr storageService service.StorageServicer logger logger.Logger } func NewPhotoService( photoRepo repository.PhotoRepositoryr, userRepo repository.UserRepositoryr, storageService service.StorageServicer, logger logger.Logger, ) *PhotoService { return &PhotoService{ photoRepo: photoRepo, userRepo: userRepo, storageService: storageService, logger: logger, } } func (s *PhotoService) UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) { // 1. 验证文件 if err := s.validatePhotoFile(file); err != nil { s.logger.Error("invalid photo file", zap.Error(err)) return nil, err } // 2. 验证用户 user, err := s.userRepo.GetByID(ctx, userID) if err != nil { s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", userID)) return nil, err } // 3. 生成文件名 filename := s.generatePhotoFilename(file.Filename, userID) // 4. 上传文件 uploadedFile, err := s.storageService.UploadFile(ctx, file, filename) if err != nil { s.logger.Error("failed to upload photo", zap.Error(err)) return nil, err } // 5. 创建照片记录 photo := &entity.Photo{ UserID: userID, Title: strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)), Description: "", Filename: uploadedFile.Filename, FilePath: uploadedFile.Path, FileSize: uploadedFile.Size, MimeType: uploadedFile.MimeType, Width: uploadedFile.Width, Height: uploadedFile.Height, Status: entity.PhotoStatusActive, CreatedAt: time.Now(), UpdatedAt: time.Now(), } // 6. 保存照片记录 createdPhoto, err := s.photoRepo.Create(ctx, photo) if err != nil { // 如果数据库保存失败,删除已上传的文件 s.storageService.DeleteFile(ctx, uploadedFile.Path) s.logger.Error("failed to create photo record", zap.Error(err)) return nil, err } s.logger.Info("photo uploaded successfully", zap.String("photo_id", fmt.Sprintf("%d", createdPhoto.ID)), zap.String("filename", createdPhoto.Filename), zap.Uint("user_id", userID)) return createdPhoto, nil } func (s *PhotoService) GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) { // 设置默认选项 if opts == nil { opts = &dto.ListPhotosOptions{ Page: 1, Limit: 20, Sort: "created_at", Order: "desc", } } // 查询照片 photos, total, err := s.photoRepo.ListByUserID(ctx, userID, &repository.ListPhotosOptions{ Page: opts.Page, Limit: opts.Limit, Sort: opts.Sort, Order: opts.Order, Status: string(entity.PhotoStatusActive), }) if err != nil { s.logger.Error("failed to list photos by user", zap.Error(err), zap.Uint("user_id", userID)) return nil, 0, err } return photos, total, nil } func (s *PhotoService) validatePhotoFile(file *multipart.FileHeader) error { // 检查文件大小 (10MB 限制) const maxFileSize = 10 * 1024 * 1024 if file.Size > maxFileSize { return errors.New("file size exceeds limit") } // 检查文件类型 ext := strings.ToLower(filepath.Ext(file.Filename)) allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} isAllowed := false for _, allowedExt := range allowedExts { if ext == allowedExt { isAllowed = true break } } if !isAllowed { return errors.New("file type not allowed") } return nil } func (s *PhotoService) generatePhotoFilename(originalFilename string, userID uint) string { ext := filepath.Ext(originalFilename) timestamp := time.Now().Unix() return fmt.Sprintf("photos/%d_%d%s", userID, timestamp, ext) } ``` ## 🧪 服务测试 ### 单元测试规范 ```go // user/user_service_test.go - 用户服务测试 package user import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "photography-backend/internal/model/dto" "photography-backend/internal/model/entity" "photography-backend/internal/repository/mocks" "photography-backend/pkg/logger" ) func TestUserService_CreateUser(t *testing.T) { // Setup mockUserRepo := new(mocks.UserRepositoryr) mockLogger := logger.NewNoop() userService := NewUserService(mockUserRepo, mockLogger) ctx := context.Background() req := &dto.CreateUserRequest{ Email: "test@example.com", Username: "testuser", Password: "password123", } // Mock expectations mockUserRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound) mockUserRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")). Return(&entity.User{ ID: 1, Email: req.Email, Username: req.Username, Role: entity.UserRoleUser, Status: entity.UserStatusActive, }, nil) // Execute user, err := userService.CreateUser(ctx, req) // Assert assert.NoError(t, err) assert.NotNil(t, user) assert.Equal(t, req.Email, user.Email) assert.Equal(t, req.Username, user.Username) assert.Equal(t, entity.UserRoleUser, user.Role) assert.Equal(t, entity.UserStatusActive, user.Status) mockUserRepo.AssertExpectations(t) } func TestUserService_CreateUser_UserAlreadyExists(t *testing.T) { // Setup mockUserRepo := new(mocks.UserRepositoryr) mockLogger := logger.NewNoop() userService := NewUserService(mockUserRepo, mockLogger) ctx := context.Background() req := &dto.CreateUserRequest{ Email: "test@example.com", Username: "testuser", Password: "password123", } existingUser := &entity.User{ ID: 1, Email: req.Email, } // Mock expectations mockUserRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil) // Execute user, err := userService.CreateUser(ctx, req) // Assert assert.Error(t, err) assert.Nil(t, user) assert.Contains(t, err.Error(), "user already exists") mockUserRepo.AssertExpectations(t) } ``` ## 🔍 错误处理 ### 自定义错误类型 ```go // errors.go - 服务层错误定义 package service import "errors" var ( // 通用错误 ErrInvalidInput = errors.New("invalid input") ErrInternalError = errors.New("internal error") ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ErrForbidden = errors.New("forbidden") // 用户相关错误 ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") ErrInvalidCredentials = errors.New("invalid credentials") ErrUserInactive = errors.New("user account is not active") // 照片相关错误 ErrPhotoNotFound = errors.New("photo not found") ErrInvalidFileType = errors.New("invalid file type") ErrFileSizeExceeded = errors.New("file size exceeded") ErrUploadFailed = errors.New("file upload failed") // 认证相关错误 ErrInvalidToken = errors.New("invalid token") ErrTokenExpired = errors.New("token expired") ErrTokenRevoked = errors.New("token revoked") ) ``` ## 💡 最佳实践 ### 服务设计原则 1. **单一职责**: 每个服务只负责一个业务领域 2. **接口优先**: 先定义接口,再实现具体逻辑 3. **依赖注入**: 通过构造函数注入依赖 4. **错误处理**: 明确的错误类型和处理 5. **日志记录**: 关键操作的日志记录 ### 性能优化 1. **缓存策略**: 合理使用缓存减少数据库查询 2. **批量操作**: 优化批量数据处理 3. **异步处理**: 使用 goroutine 处理耗时操作 4. **数据库优化**: 合理使用索引和查询优化 ### 安全考虑 1. **输入验证**: 严格验证所有输入参数 2. **权限检查**: 确保用户只能操作自己的数据 3. **敏感信息**: 避免在日志中记录敏感信息 4. **SQL 注入**: 使用参数化查询防止 SQL 注入 本模块是业务逻辑的核心,确保代码质量和测试覆盖率是关键。