Files
photography/backend/internal/service/CLAUDE.md
xujiang a2f2f66f88
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 1m37s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
refactor: 重构后端架构,采用 Go 风格四层设计模式
## 主要变更

### 🏗️ 架构重构
- 采用简洁的四层架构:API → Service → Repository → Model
- 遵循 Go 语言最佳实践和命名规范
- 实现依赖注入和接口导向设计
- 统一错误处理和响应格式

### 📁 目录结构优化
- 删除重复模块 (application/, domain/, infrastructure/ 等)
- 规范化命名 (使用 Go 风格的 snake_case)
- 清理无关文件 (package.json, node_modules/ 等)
- 新增规范化的测试目录结构

### 📚 文档系统
- 为每个模块创建详细的 CLAUDE.md 指导文件
- 包含开发规范、最佳实践和使用示例
- 支持模块化开发,缩短上下文长度

### 🔧 开发规范
- 统一接口命名规范 (UserServicer, PhotoRepositoryr)
- 标准化错误处理机制
- 完善的测试策略 (单元测试、集成测试、性能测试)
- 规范化的配置管理

### 🗂️ 新增文件
- cmd/server/ - 服务启动入口和配置
- internal/model/ - 数据模型层 (entity, dto, request)
- pkg/ - 共享工具包 (logger, response, validator)
- tests/ - 完整测试结构
- docs/ - API 文档和架构设计
- .gitignore - Git 忽略文件配置

### 🗑️ 清理内容
- 删除 Node.js 相关文件 (package.json, node_modules/)
- 移除重复的架构目录
- 清理临时文件和构建产物
- 删除重复的文档文件

## 影响
- 提高代码可维护性和可扩展性
- 统一开发规范,提升团队协作效率
- 优化项目结构,符合 Go 语言生态标准
- 完善文档体系,降低上手难度
2025-07-10 11:20:59 +08:00

17 KiB

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 风格服务设计

接口定义规范

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

服务实现规范

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

照片服务实现

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

🧪 服务测试

单元测试规范

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

🔍 错误处理

自定义错误类型

// 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 注入

本模块是业务逻辑的核心,确保代码质量和测试覆盖率是关键。