Files
photography/docs/v1/backend/Golang项目架构文档.md
2025-07-09 14:32:52 +08:00

61 KiB
Raw Permalink Blame History

摄影作品集网站 - Golang项目架构文档

1. 项目概述

1.1 架构设计理念

  • Clean Architecture: 采用整洁架构,分离业务逻辑和技术实现
  • Domain-Driven Design: 以领域为核心的设计方法
  • 微服务友好: 模块化设计,便于后续拆分为微服务
  • 高性能: 充分利用Go语言的并发特性
  • 可测试: 依赖注入和接口抽象,便于单元测试

1.2 技术栈选择

// 核心框架
gin-gonic/gin         // Web框架
gorm.io/gorm         // ORM框架
redis/go-redis       // Redis客户端
golang-jwt/jwt       // JWT认证

// 数据库
gorm.io/driver/postgres  // PostgreSQL驱动
golang-migrate/migrate   // 数据库迁移

// 图片处理
h2non/bimg          // 图片处理 (基于libvips)
disintegration/imaging  // 图片处理备选方案

// 文件存储
minio/minio-go      // MinIO/S3客户端
aws/aws-sdk-go      // AWS SDK

// 配置管理
spf13/viper        // 配置管理
joho/godotenv      // 环境变量

// 日志系统
sirupsen/logrus    // 结构化日志
lumberjack.v2      // 日志轮转
opentracing/opentracing-go // 链路追踪
jaegertracing/jaeger-client-go // Jaeger客户端

// 验证和工具
go-playground/validator  // 数据验证
google/uuid             // UUID生成
shopspring/decimal      // 精确小数

// 测试和Mock
stretchr/testify       // 测试框架
golang/mock           // Mock生成

1.3 项目结构概览

photography-backend/
├── cmd/                        # 应用程序入口
│   └── server/
│       └── main.go
├── internal/                   # 私有应用代码
│   ├── api/                   # API层
│   ├── service/               # 业务逻辑层
│   ├── repository/            # 数据访问层
│   ├── domain/                # 领域模型
│   ├── dto/                   # 数据传输对象
│   └── infrastructure/        # 基础设施层
├── pkg/                       # 公共库代码
│   ├── config/               # 配置管理
│   ├── database/             # 数据库连接
│   ├── logger/               # 日志系统
│   ├── tracing/              # 链路追踪
│   ├── middleware/           # 中间件
│   ├── storage/              # 存储服务
│   ├── cache/                # 缓存服务
│   ├── queue/                # 队列服务
│   └── utils/                # 工具函数
├── migrations/                # 数据库迁移
├── scripts/                   # 构建和部署脚本
├── docker/                    # Docker配置
├── docs/                      # 项目文档
├── test/                      # 集成测试
├── go.mod                     # Go模块定义
├── go.sum                     # 依赖版本锁定
├── Makefile                   # 构建任务
└── README.md                  # 项目说明

2. 详细架构设计

2.1 分层架构

2.1.1 架构图

┌─────────────────────────────────────────────────────────┐
│                  API Layer (Gin)                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │   REST API  │ │  WebSocket  │ │   GraphQL   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
├─────────────────────────────────────────────────────────┤
│               Service Layer (Business Logic)            │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │ PhotoService│ │CategorySvc  │ │  AuthService│       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
├─────────────────────────────────────────────────────────┤
│               Repository Layer (Data Access)            │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │  PhotoRepo  │ │CategoryRepo │ │  UserRepo   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
├─────────────────────────────────────────────────────────┤
│              Infrastructure Layer                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │ PostgreSQL  │ │    Redis    │ │   MinIO/S3  │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────────────────────────────────────────┘

2.1.2 依赖关系

API Layer ──→ Service Layer ──→ Repository Layer ──→ Infrastructure Layer
     ↑              ↑                   ↑                      ↑
     │              │                   │                      │
   Handlers      Business Logic    Data Access           External Services

2.2 领域模型设计

2.2.1 核心领域实体

// internal/domain/photo.go
package domain

import (
    "time"
    "github.com/google/uuid"
    "github.com/opentracing/opentracing-go"
)

// Photo 照片领域实体
type Photo struct {
    ID          PhotoID   `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Slug        string    `json:"slug"`
    Status      PhotoStatus `json:"status"`
    Visibility  Visibility  `json:"visibility"`
    
    // 文件信息
    FileInfo    FileInfo    `json:"file_info"`
    Formats     []PhotoFormat `json:"formats"`
    
    // EXIF数据
    EXIF        *EXIFData   `json:"exif,omitempty"`
    
    // 位置信息
    Location    *Location   `json:"location,omitempty"`
    
    // 关联关系
    Categories  []CategoryID `json:"categories"`
    Tags        []TagID      `json:"tags"`
    
    // 统计信息
    Stats       PhotoStats  `json:"stats"`
    
    // 时间信息
    TakenAt     *time.Time  `json:"taken_at,omitempty"`
    CreatedAt   time.Time   `json:"created_at"`
    UpdatedAt   time.Time   `json:"updated_at"`
    
    // 元数据
    Metadata    map[string]interface{} `json:"metadata,omitempty"`
}

// PhotoID 照片ID值对象
type PhotoID struct {
    value uint
}

func NewPhotoID(id uint) PhotoID {
    return PhotoID{value: id}
}

func (p PhotoID) Value() uint {
    return p.value
}

func (p PhotoID) String() string {
    return fmt.Sprintf("photo-%d", p.value)
}

// PhotoStatus 照片状态枚举
type PhotoStatus string

const (
    PhotoStatusDraft     PhotoStatus = "draft"
    PhotoStatusPublished PhotoStatus = "published"
    PhotoStatusArchived  PhotoStatus = "archived"
    PhotoStatusProcessing PhotoStatus = "processing"
)

func (s PhotoStatus) IsValid() bool {
    switch s {
    case PhotoStatusDraft, PhotoStatusPublished, PhotoStatusArchived, PhotoStatusProcessing:
        return true
    default:
        return false
    }
}

// Visibility 可见性枚举
type Visibility string

const (
    VisibilityPublic   Visibility = "public"
    VisibilityPrivate  Visibility = "private" 
    VisibilityPassword Visibility = "password"
)

// FileInfo 文件信息值对象
type FileInfo struct {
    OriginalFilename string `json:"original_filename"`
    FileSize         int64  `json:"file_size"`
    MimeType         string `json:"mime_type"`
    FileHash         string `json:"file_hash"`
}

// PhotoFormat 照片格式值对象
type PhotoFormat struct {
    Type       FormatType `json:"type"`
    FilePath   string     `json:"file_path"`
    FileSize   int64      `json:"file_size"`
    Width      int        `json:"width"`
    Height     int        `json:"height"`
    Quality    int        `json:"quality"`
}

type FormatType string

const (
    FormatOriginal    FormatType = "original"
    FormatJPG         FormatType = "jpg"
    FormatWebP        FormatType = "webp"
    FormatThumbSmall  FormatType = "thumb_small"
    FormatThumbMedium FormatType = "thumb_medium"
    FormatThumbLarge  FormatType = "thumb_large"
    FormatDisplay     FormatType = "display"
)

// EXIFData EXIF数据值对象
type EXIFData struct {
    Camera       string `json:"camera,omitempty"`
    Lens         string `json:"lens,omitempty"`
    ISO          int    `json:"iso,omitempty"`
    Aperture     string `json:"aperture,omitempty"`
    ShutterSpeed string `json:"shutter_speed,omitempty"`
    FocalLength  string `json:"focal_length,omitempty"`
}

// Location 位置信息值对象
type Location struct {
    Name      string  `json:"name"`
    Latitude  float64 `json:"latitude"`
    Longitude float64 `json:"longitude"`
    Country   string  `json:"country,omitempty"`
    City      string  `json:"city,omitempty"`
}

// PhotoStats 照片统计值对象
type PhotoStats struct {
    ViewCount     int `json:"view_count"`
    LikeCount     int `json:"like_count"`
    DownloadCount int `json:"download_count"`
}

2.2.2 领域服务

// internal/domain/service/photo_domain_service.go
package service

import (
    "context"
    "fmt"
    "strings"
    "photography-backend/internal/domain"
)

// PhotoDomainService 照片领域服务
type PhotoDomainService struct {
    slugGenerator SlugGenerator
    exifExtractor EXIFExtractor
}

// SlugGenerator 别名生成器接口
type SlugGenerator interface {
    Generate(title string) string
}

// EXIFExtractor EXIF提取器接口
type EXIFExtractor interface {
    Extract(filePath string) (*domain.EXIFData, error)
}

func NewPhotoDomainService(slugGen SlugGenerator, exifExt EXIFExtractor) *PhotoDomainService {
    return &PhotoDomainService{
        slugGenerator: slugGen,
        exifExtractor: exifExt,
    }
}

// CreatePhoto 创建照片聚合根
func (s *PhotoDomainService) CreatePhoto(cmd CreatePhotoCommand) (*domain.Photo, error) {
    // 生成唯一别名
    slug := s.slugGenerator.Generate(cmd.Title)
    
    // 提取EXIF数据
    exif, err := s.exifExtractor.Extract(cmd.FilePath)
    if err != nil {
        // EXIF提取失败不影响照片创建
        exif = nil
    }
    
    // 创建照片实体
    photo := &domain.Photo{
        ID:          domain.NewPhotoID(0), // 由数据库生成
        Title:       cmd.Title,
        Description: cmd.Description,
        Slug:        slug,
        Status:      domain.PhotoStatusProcessing,
        Visibility:  domain.VisibilityPublic,
        FileInfo: domain.FileInfo{
            OriginalFilename: cmd.OriginalFilename,
            FileSize:         cmd.FileSize,
            MimeType:         cmd.MimeType,
        },
        EXIF:       exif,
        Location:   cmd.Location,
        Categories: cmd.Categories,
        Tags:       cmd.Tags,
        TakenAt:    cmd.TakenAt,
        Metadata:   cmd.Metadata,
    }
    
    // 验证照片数据
    if err := s.validatePhoto(photo); err != nil {
        return nil, fmt.Errorf("photo validation failed: %w", err)
    }
    
    return photo, nil
}

// validatePhoto 验证照片数据
func (s *PhotoDomainService) validatePhoto(photo *domain.Photo) error {
    if strings.TrimSpace(photo.Title) == "" {
        return fmt.Errorf("title cannot be empty")
    }
    
    if !photo.Status.IsValid() {
        return fmt.Errorf("invalid photo status: %s", photo.Status)
    }
    
    // 验证文件信息
    if photo.FileInfo.FileSize <= 0 {
        return fmt.Errorf("invalid file size")
    }
    
    return nil
}

// UpdatePhotoStatus 更新照片状态
func (s *PhotoDomainService) UpdatePhotoStatus(photo *domain.Photo, newStatus domain.PhotoStatus) error {
    if !newStatus.IsValid() {
        return fmt.Errorf("invalid status: %s", newStatus)
    }
    
    // 业务规则:已归档的照片不能直接发布
    if photo.Status == domain.PhotoStatusArchived && newStatus == domain.PhotoStatusPublished {
        return fmt.Errorf("archived photo cannot be published directly")
    }
    
    photo.Status = newStatus
    return nil
}

// CreatePhotoCommand 创建照片命令
type CreatePhotoCommand struct {
    Title            string
    Description      string
    FilePath         string
    OriginalFilename string
    FileSize         int64
    MimeType         string
    Location         *domain.Location
    Categories       []domain.CategoryID
    Tags             []domain.TagID
    TakenAt          *time.Time
    Metadata         map[string]interface{}
}

2.3 服务层设计

2.3.1 应用服务接口

// internal/service/photo_service.go
package service

import (
    "context"
    "photography-backend/internal/domain"
    "photography-backend/internal/dto"
)

// PhotoService 照片应用服务接口
type PhotoService interface {
    // 查询操作
    GetPhotos(ctx context.Context, req dto.PhotoListRequest) (*dto.PhotoListResponse, error)
    GetPhotoByID(ctx context.Context, id uint) (*dto.PhotoResponse, error)
    GetPhotoBySlug(ctx context.Context, slug string) (*dto.PhotoResponse, error)
    SearchPhotos(ctx context.Context, req dto.PhotoSearchRequest) (*dto.PhotoSearchResponse, error)
    
    // 命令操作
    CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error)
    UpdatePhoto(ctx context.Context, id uint, req dto.UpdatePhotoRequest) (*dto.PhotoResponse, error)
    DeletePhoto(ctx context.Context, id uint) error
    UpdatePhotoStatus(ctx context.Context, id uint, status string) error
    
    // 批量操作
    BatchUpdatePhotos(ctx context.Context, req dto.BatchUpdatePhotosRequest) error
    BatchDeletePhotos(ctx context.Context, photoIDs []uint) error
    
    // 统计操作
    GetPhotoStats(ctx context.Context) (*dto.PhotoStatsResponse, error)
}

// photoServiceImpl 照片应用服务实现
type photoServiceImpl struct {
    photoRepo       repository.PhotoRepository
    categoryRepo    repository.CategoryRepository
    tagRepo         repository.TagRepository
    photoDomainSvc  *service.PhotoDomainService
    imageProcessor  ImageProcessor
    cacheService    cache.Service
    eventPublisher  event.Publisher
    logger          *logrus.Logger
}

func NewPhotoService(
    photoRepo repository.PhotoRepository,
    categoryRepo repository.CategoryRepository,
    tagRepo repository.TagRepository,
    photoDomainSvc *service.PhotoDomainService,
    imageProcessor ImageProcessor,
    cacheService cache.Service,
    eventPublisher event.Publisher,
    logger *logrus.Logger,
) PhotoService {
    return &photoServiceImpl{
        photoRepo:      photoRepo,
        categoryRepo:   categoryRepo,
        tagRepo:        tagRepo,
        photoDomainSvc: photoDomainSvc,
        imageProcessor: imageProcessor,
        cacheService:   cacheService,
        eventPublisher: eventPublisher,
        logger:         logger,
    }
}

// GetPhotos 获取照片列表
func (s *photoServiceImpl) GetPhotos(ctx context.Context, req dto.PhotoListRequest) (*dto.PhotoListResponse, error) {
    // 创建span用于链路追踪
    span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.GetPhotos")
    defer span.Finish()
    
    // 获取trace ID
    traceID := s.getTraceID(span)
    logger := s.logger.WithField("trace_id", traceID)
    
    // 参数验证
    if err := req.Validate(); err != nil {
        span.SetTag("error", true)
        span.LogKV("event", "validation_error", "error", err.Error())
        logger.WithError(err).Error("Invalid request parameters")
        return nil, fmt.Errorf("invalid request: %w", err)
    }
    
    // 构建查询条件
    filter := s.buildPhotoFilter(req)
    
    // 尝试从缓存获取
    cacheKey := s.buildCacheKey("photos:list", filter)
    if cached, err := s.cacheService.Get(ctx, cacheKey); err == nil {
        var response dto.PhotoListResponse
        if err := json.Unmarshal(cached, &response); err == nil {
            return &response, nil
        }
    }
    
    // 从数据库查询
    photos, total, err := s.photoRepo.FindWithPagination(ctx, filter)
    if err != nil {
        span.SetTag("error", true)
        span.LogKV("event", "repository_error", "error", err.Error())
        logger.WithError(err).Error("Failed to get photos from repository")
        return nil, fmt.Errorf("failed to get photos: %w", err)
    }
    
    // 转换为DTO
    photoResponses := make([]dto.PhotoSummary, 0, len(photos))
    for _, photo := range photos {
        photoResponses = append(photoResponses, dto.NewPhotoSummary(photo))
    }
    
    response := &dto.PhotoListResponse{
        Photos: photoResponses,
        Pagination: dto.PaginationResponse{
            Page:       req.Page,
            Limit:      req.Limit,
            Total:      total,
            TotalPages: (total + int64(req.Limit) - 1) / int64(req.Limit),
            HasNext:    req.Page < int((total+int64(req.Limit)-1)/int64(req.Limit)),
            HasPrev:    req.Page > 1,
        },
    }
    
    // 缓存结果
    if data, err := json.Marshal(response); err == nil {
        s.cacheService.Set(ctx, cacheKey, data, 10*time.Minute)
    }
    
    // 记录成功日志
    span.SetTag("photos_count", len(photos))
    span.SetTag("total_count", total)
    logger.WithFields(logrus.Fields{
        "photos_count": len(photos),
        "total_count":  total,
        "page":         req.Page,
        "limit":        req.Limit,
    }).Info("Photos retrieved successfully")
    
    return response, nil
}

// getTraceID 获取trace ID
func (s *photoServiceImpl) getTraceID(span opentracing.Span) string {
    if span == nil {
        return ""
    }
    
    // 从span context获取trace ID
    spanContext := span.Context()
    if jaegerSpanContext, ok := spanContext.(interface{ TraceID() string }); ok {
        return jaegerSpanContext.TraceID()
    }
    
    // 如果无法获取trace ID生成一个UUID作为fallback
    return uuid.New().String()
}

// CreatePhoto 创建照片
func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error) {
    // 创建span用于链路追踪
    span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.CreatePhoto")
    defer span.Finish()
    
    // 获取trace ID
    traceID := s.getTraceID(span)
    logger := s.logger.WithField("trace_id", traceID)
    
    // 参数验证
    if err := req.Validate(); err != nil {
        span.SetTag("error", true)
        span.LogKV("event", "validation_error", "error", err.Error())
        logger.WithError(err).Error("Invalid request parameters")
        return nil, fmt.Errorf("invalid request: %w", err)
    }
    
    // 构建领域命令
    cmd := service.CreatePhotoCommand{
        Title:            req.Title,
        Description:      req.Description,
        FilePath:         req.FilePath,
        OriginalFilename: req.OriginalFilename,
        FileSize:         req.FileSize,
        MimeType:         req.MimeType,
        Location:         req.Location,
        Categories:       req.Categories,
        Tags:             req.Tags,
        TakenAt:          req.TakenAt,
        Metadata:         req.Metadata,
    }
    
    // 使用领域服务创建照片
    photo, err := s.photoDomainSvc.CreatePhoto(cmd)
    if err != nil {
        return nil, fmt.Errorf("failed to create photo: %w", err)
    }
    
    // 开始数据库事务
    tx, err := s.photoRepo.BeginTx(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to begin transaction: %w", err)
    }
    defer tx.Rollback()
    
    // 保存照片到数据库
    if err := s.photoRepo.Create(ctx, tx, photo); err != nil {
        span.SetTag("error", true)
        span.LogKV("event", "database_error", "error", err.Error())
        logger.WithError(err).Error("Failed to save photo to database")
        return nil, fmt.Errorf("failed to save photo: %w", err)
    }
    
    // 处理分类关联
    if len(req.Categories) > 0 {
        if err := s.photoRepo.UpdateCategories(ctx, tx, photo.ID, req.Categories); err != nil {
            return nil, fmt.Errorf("failed to update categories: %w", err)
        }
    }
    
    // 处理标签关联
    if len(req.Tags) > 0 {
        if err := s.photoRepo.UpdateTags(ctx, tx, photo.ID, req.Tags); err != nil {
            return nil, fmt.Errorf("failed to update tags: %w", err)
        }
    }
    
    // 提交事务
    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("failed to commit transaction: %w", err)
    }
    
    // 异步处理图片
    go func() {
        if err := s.imageProcessor.ProcessPhoto(photo); err != nil {
            s.logger.WithError(err).WithField("photo_id", photo.ID).Error("Failed to process photo")
        }
    }()
    
    // 发布领域事件
    event := event.PhotoCreated{
        PhotoID:   photo.ID.Value(),
        Title:     photo.Title,
        CreatedAt: photo.CreatedAt,
    }
    s.eventPublisher.Publish(ctx, event)
    
    // 清除相关缓存
    s.invalidateCache(ctx, "photos:*", "categories:*", "stats:*")
    
    // 记录成功日志
    span.SetTag("photo_id", photo.ID.Value())
    span.SetTag("photo_title", photo.Title)
    logger.WithFields(logrus.Fields{
        "photo_id":    photo.ID.Value(),
        "photo_title": photo.Title,
        "photo_slug":  photo.Slug,
    }).Info("Photo created successfully")
    
    return dto.NewPhotoResponse(photo), nil
}

2.4 数据访问层设计

2.4.1 Repository接口

// internal/repository/photo_repository.go
package repository

import (
    "context"
    "database/sql"
    "photography-backend/internal/domain"
)

// PhotoRepository 照片数据访问接口
type PhotoRepository interface {
    // 基础CRUD
    Create(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error
    Update(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error
    Delete(ctx context.Context, tx *sql.Tx, id domain.PhotoID) error
    
    // 查询操作
    FindByID(ctx context.Context, id domain.PhotoID) (*domain.Photo, error)
    FindBySlug(ctx context.Context, slug string) (*domain.Photo, error)
    FindWithPagination(ctx context.Context, filter PhotoFilter) ([]*domain.Photo, int64, error)
    Search(ctx context.Context, query string, filter PhotoFilter) ([]*domain.Photo, int64, error)
    
    // 关联操作
    UpdateCategories(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, categoryIDs []domain.CategoryID) error
    UpdateTags(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, tagIDs []domain.TagID) error
    UpdateFormats(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, formats []domain.PhotoFormat) error
    
    // 批量操作
    BatchUpdate(ctx context.Context, tx *sql.Tx, photoIDs []domain.PhotoID, updates map[string]interface{}) error
    BatchDelete(ctx context.Context, tx *sql.Tx, photoIDs []domain.PhotoID) error
    
    // 统计操作
    Count(ctx context.Context, filter PhotoFilter) (int64, error)
    GetStats(ctx context.Context) (*PhotoStats, error)
    
    // 事务管理
    BeginTx(ctx context.Context) (*sql.Tx, error)
}

// PhotoFilter 照片查询过滤器
type PhotoFilter struct {
    Status      *domain.PhotoStatus
    Visibility  *domain.Visibility
    CategoryIDs []domain.CategoryID
    TagIDs      []domain.TagID
    Search      string
    DateFrom    *time.Time
    DateTo      *time.Time
    Page        int
    Limit       int
    SortBy      string
    SortOrder   string
}

// PhotoStats 照片统计数据
type PhotoStats struct {
    TotalPhotos     int64
    PublishedPhotos int64
    DraftPhotos     int64
    ArchivedPhotos  int64
    TotalViews      int64
    TotalLikes      int64
    StorageUsed     int64
}

2.4.2 GORM实现

// internal/infrastructure/persistence/photo_repository_impl.go
package persistence

import (
    "context"
    "database/sql"
    "fmt"
    "strings"
    "time"
    
    "gorm.io/gorm"
    "photography-backend/internal/domain"
    "photography-backend/internal/repository"
    "photography-backend/internal/infrastructure/persistence/model"
)

// photoRepositoryImpl GORM实现的照片仓库
type photoRepositoryImpl struct {
    db *gorm.DB
}

func NewPhotoRepository(db *gorm.DB) repository.PhotoRepository {
    return &photoRepositoryImpl{db: db}
}

// Create 创建照片
func (r *photoRepositoryImpl) Create(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error {
    // 转换为数据库模型
    photoModel := model.FromDomainPhoto(photo)
    
    // 使用事务或普通连接
    db := r.getDB(tx)
    
    // 创建照片记录
    if err := db.WithContext(ctx).Create(&photoModel).Error; err != nil {
        return fmt.Errorf("failed to create photo: %w", err)
    }
    
    // 更新领域对象ID
    photo.ID = domain.NewPhotoID(photoModel.ID)
    
    return nil
}

// FindWithPagination 分页查询照片
func (r *photoRepositoryImpl) FindWithPagination(ctx context.Context, filter repository.PhotoFilter) ([]*domain.Photo, int64, error) {
    var photoModels []model.Photo
    var total int64
    
    // 构建查询
    query := r.db.WithContext(ctx).Model(&model.Photo{})
    
    // 应用过滤条件
    query = r.applyFilters(query, filter)
    
    // 预加载关联数据
    query = query.Preload("Categories").Preload("Tags").Preload("Formats")
    
    // 统计总数
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to count photos: %w", err)
    }
    
    // 应用分页和排序
    offset := (filter.Page - 1) * filter.Limit
    query = query.Offset(offset).Limit(filter.Limit)
    
    if filter.SortBy != "" {
        order := fmt.Sprintf("%s %s", filter.SortBy, filter.SortOrder)
        query = query.Order(order)
    } else {
        query = query.Order("created_at DESC")
    }
    
    // 执行查询
    if err := query.Find(&photoModels).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to find photos: %w", err)
    }
    
    // 转换为领域对象
    photos := make([]*domain.Photo, 0, len(photoModels))
    for _, photoModel := range photoModels {
        photo := model.ToDomainPhoto(&photoModel)
        photos = append(photos, photo)
    }
    
    return photos, total, nil
}

// applyFilters 应用查询过滤条件
func (r *photoRepositoryImpl) applyFilters(query *gorm.DB, filter repository.PhotoFilter) *gorm.DB {
    // 状态过滤
    if filter.Status != nil {
        query = query.Where("status = ?", *filter.Status)
    }
    
    // 可见性过滤
    if filter.Visibility != nil {
        query = query.Where("visibility = ?", *filter.Visibility)
    }
    
    // 分类过滤
    if len(filter.CategoryIDs) > 0 {
        categoryIDs := make([]uint, len(filter.CategoryIDs))
        for i, id := range filter.CategoryIDs {
            categoryIDs[i] = id.Value()
        }
        query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
            Where("photo_categories.category_id IN ?", categoryIDs)
    }
    
    // 标签过滤
    if len(filter.TagIDs) > 0 {
        tagIDs := make([]uint, len(filter.TagIDs))
        for i, id := range filter.TagIDs {
            tagIDs[i] = id.Value()
        }
        query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
            Where("photo_tags.tag_id IN ?", tagIDs)
    }
    
    // 搜索过滤
    if filter.Search != "" {
        searchTerm := "%" + strings.ToLower(filter.Search) + "%"
        query = query.Where("LOWER(title) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm)
    }
    
    // 日期范围过滤
    if filter.DateFrom != nil {
        query = query.Where("taken_at >= ?", *filter.DateFrom)
    }
    if filter.DateTo != nil {
        query = query.Where("taken_at <= ?", *filter.DateTo)
    }
    
    return query
}

// getDB 获取数据库连接(事务或普通连接)
func (r *photoRepositoryImpl) getDB(tx *sql.Tx) *gorm.DB {
    if tx != nil {
        return r.db.Set("gorm:tx", tx)
    }
    return r.db
}

2.5 数据模型映射

2.5.1 数据库模型

// internal/infrastructure/persistence/model/photo.go
package model

import (
    "time"
    "gorm.io/gorm"
    "photography-backend/internal/domain"
)

// Photo 照片数据库模型
type Photo struct {
    ID          uint      `gorm:"primaryKey" json:"id"`
    Title       string    `gorm:"size:255;not null" json:"title"`
    Description string    `gorm:"type:text" json:"description"`
    Slug        string    `gorm:"size:255;uniqueIndex" json:"slug"`
    Status      string    `gorm:"size:20;default:published" json:"status"`
    Visibility  string    `gorm:"size:20;default:public" json:"visibility"`
    SortOrder   int       `gorm:"default:0" json:"sort_order"`
    
    // 文件信息
    OriginalFilename string `gorm:"size:255" json:"original_filename"`
    FileSize         int64  `json:"file_size"`
    MimeType         string `gorm:"size:100" json:"mime_type"`
    FileHash         string `gorm:"size:64" json:"file_hash"`
    
    // EXIF数据
    Camera       string `gorm:"size:100" json:"camera"`
    Lens         string `gorm:"size:100" json:"lens"`
    ISO          int    `json:"iso"`
    Aperture     string `gorm:"size:10" json:"aperture"`
    ShutterSpeed string `gorm:"size:20" json:"shutter_speed"`
    FocalLength  string `gorm:"size:20" json:"focal_length"`
    
    // 位置信息
    Latitude     *float64 `gorm:"type:decimal(10,8)" json:"latitude"`
    Longitude    *float64 `gorm:"type:decimal(11,8)" json:"longitude"`
    LocationName string   `gorm:"size:255" json:"location_name"`
    Country      string   `gorm:"size:100" json:"country"`
    City         string   `gorm:"size:100" json:"city"`
    
    // 统计信息
    ViewCount     int `gorm:"default:0" json:"view_count"`
    LikeCount     int `gorm:"default:0" json:"like_count"`
    DownloadCount int `gorm:"default:0" json:"download_count"`
    
    // 时间信息
    TakenAt   *time.Time `json:"taken_at"`
    CreatedAt time.Time  `json:"created_at"`
    UpdatedAt time.Time  `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    
    // 元数据
    Metadata JSON `gorm:"type:jsonb" json:"metadata"`
    
    // 关联关系
    Categories []Category    `gorm:"many2many:photo_categories" json:"categories"`
    Tags       []Tag         `gorm:"many2many:photo_tags" json:"tags"`
    Formats    []PhotoFormat `gorm:"foreignKey:PhotoID" json:"formats"`
}

// PhotoFormat 照片格式数据库模型
type PhotoFormat struct {
    ID         uint   `gorm:"primaryKey" json:"id"`
    PhotoID    uint   `gorm:"not null;index" json:"photo_id"`
    FormatType string `gorm:"size:20;not null" json:"format_type"`
    FilePath   string `gorm:"size:500;not null" json:"file_path"`
    FileSize   int64  `json:"file_size"`
    Width      int    `json:"width"`
    Height     int    `json:"height"`
    Quality    int    `json:"quality"`
    CreatedAt  time.Time `json:"created_at"`
    
    // 唯一约束
    Photo Photo `gorm:"foreignKey:PhotoID" json:"-"`
}

// JSON 自定义JSON类型
type JSON map[string]interface{}

// 实现GORM的Valuer和Scanner接口
func (j JSON) Value() (driver.Value, error) {
    if len(j) == 0 {
        return nil, nil
    }
    return json.Marshal(j)
}

func (j *JSON) Scan(value interface{}) error {
    if value == nil {
        *j = nil
        return nil
    }
    
    bytes, ok := value.([]byte)
    if !ok {
        return fmt.Errorf("cannot scan %T into JSON", value)
    }
    
    return json.Unmarshal(bytes, j)
}

// FromDomainPhoto 从领域对象转换为数据库模型
func FromDomainPhoto(photo *domain.Photo) *Photo {
    model := &Photo{
        ID:               photo.ID.Value(),
        Title:            photo.Title,
        Description:      photo.Description,
        Slug:             photo.Slug,
        Status:           string(photo.Status),
        Visibility:       string(photo.Visibility),
        OriginalFilename: photo.FileInfo.OriginalFilename,
        FileSize:         photo.FileInfo.FileSize,
        MimeType:         photo.FileInfo.MimeType,
        FileHash:         photo.FileInfo.FileHash,
        ViewCount:        photo.Stats.ViewCount,
        LikeCount:        photo.Stats.LikeCount,
        DownloadCount:    photo.Stats.DownloadCount,
        TakenAt:          photo.TakenAt,
        CreatedAt:        photo.CreatedAt,
        UpdatedAt:        photo.UpdatedAt,
        Metadata:         JSON(photo.Metadata),
    }
    
    // EXIF数据
    if photo.EXIF != nil {
        model.Camera = photo.EXIF.Camera
        model.Lens = photo.EXIF.Lens
        model.ISO = photo.EXIF.ISO
        model.Aperture = photo.EXIF.Aperture
        model.ShutterSpeed = photo.EXIF.ShutterSpeed
        model.FocalLength = photo.EXIF.FocalLength
    }
    
    // 位置信息
    if photo.Location != nil {
        model.Latitude = &photo.Location.Latitude
        model.Longitude = &photo.Location.Longitude
        model.LocationName = photo.Location.Name
        model.Country = photo.Location.Country
        model.City = photo.Location.City
    }
    
    // 转换格式信息
    formats := make([]PhotoFormat, 0, len(photo.Formats))
    for _, format := range photo.Formats {
        formats = append(formats, PhotoFormat{
            PhotoID:    photo.ID.Value(),
            FormatType: string(format.Type),
            FilePath:   format.FilePath,
            FileSize:   format.FileSize,
            Width:      format.Width,
            Height:     format.Height,
            Quality:    format.Quality,
        })
    }
    model.Formats = formats
    
    return model
}

// ToDomainPhoto 从数据库模型转换为领域对象
func ToDomainPhoto(model *Photo) *domain.Photo {
    photo := &domain.Photo{
        ID:          domain.NewPhotoID(model.ID),
        Title:       model.Title,
        Description: model.Description,
        Slug:        model.Slug,
        Status:      domain.PhotoStatus(model.Status),
        Visibility:  domain.Visibility(model.Visibility),
        FileInfo: domain.FileInfo{
            OriginalFilename: model.OriginalFilename,
            FileSize:         model.FileSize,
            MimeType:         model.MimeType,
            FileHash:         model.FileHash,
        },
        Stats: domain.PhotoStats{
            ViewCount:     model.ViewCount,
            LikeCount:     model.LikeCount,
            DownloadCount: model.DownloadCount,
        },
        TakenAt:   model.TakenAt,
        CreatedAt: model.CreatedAt,
        UpdatedAt: model.UpdatedAt,
        Metadata:  map[string]interface{}(model.Metadata),
    }
    
    // EXIF数据
    if model.Camera != "" || model.Lens != "" || model.ISO != 0 {
        photo.EXIF = &domain.EXIFData{
            Camera:       model.Camera,
            Lens:         model.Lens,
            ISO:          model.ISO,
            Aperture:     model.Aperture,
            ShutterSpeed: model.ShutterSpeed,
            FocalLength:  model.FocalLength,
        }
    }
    
    // 位置信息
    if model.Latitude != nil && model.Longitude != nil {
        photo.Location = &domain.Location{
            Name:      model.LocationName,
            Latitude:  *model.Latitude,
            Longitude: *model.Longitude,
            Country:   model.Country,
            City:      model.City,
        }
    }
    
    // 转换格式信息
    formats := make([]domain.PhotoFormat, 0, len(model.Formats))
    for _, format := range model.Formats {
        formats = append(formats, domain.PhotoFormat{
            Type:     domain.FormatType(format.FormatType),
            FilePath: format.FilePath,
            FileSize: format.FileSize,
            Width:    format.Width,
            Height:   format.Height,
            Quality:  format.Quality,
        })
    }
    photo.Formats = formats
    
    // 转换分类ID
    categoryIDs := make([]domain.CategoryID, 0, len(model.Categories))
    for _, category := range model.Categories {
        categoryIDs = append(categoryIDs, domain.NewCategoryID(category.ID))
    }
    photo.Categories = categoryIDs
    
    // 转换标签ID
    tagIDs := make([]domain.TagID, 0, len(model.Tags))
    for _, tag := range model.Tags {
        tagIDs = append(tagIDs, domain.NewTagID(tag.ID))
    }
    photo.Tags = tagIDs
    
    return photo
}

2.6 依赖注入与容器

2.6.1 依赖注入容器

// pkg/container/container.go
package container

import (
    "log"
    "photography-backend/internal/api/handlers"
    "photography-backend/internal/service"
    "photography-backend/internal/repository"
    "photography-backend/internal/infrastructure/persistence"
    "photography-backend/pkg/config"
    "photography-backend/pkg/database"
    "photography-backend/pkg/cache"
    "photography-backend/pkg/storage"
    "photography-backend/pkg/logger"
)

// Container 依赖注入容器
type Container struct {
    config *config.Config
    
    // 基础设施
    db           *gorm.DB
    redisClient  *redis.Client
    logger       *logrus.Logger
    
    // 服务
    cacheService    cache.Service
    storageService  storage.Service
    
    // 仓库
    photoRepo    repository.PhotoRepository
    categoryRepo repository.CategoryRepository
    tagRepo      repository.TagRepository
    userRepo     repository.UserRepository
    
    // 应用服务
    photoService    service.PhotoService
    categoryService service.CategoryService
    tagService      service.TagService
    authService     service.AuthService
    uploadService   service.UploadService
    
    // 处理器
    photoHandler    *handlers.PhotoHandler
    categoryHandler *handlers.CategoryHandler
    tagHandler      *handlers.TagHandler
    authHandler     *handlers.AuthHandler
    uploadHandler   *handlers.UploadHandler
}

// NewContainer 创建新的容器
func NewContainer(cfg *config.Config) *Container {
    container := &Container{
        config: cfg,
    }
    
    container.initInfrastructure()
    container.initRepositories()
    container.initServices()
    container.initHandlers()
    
    return container
}

// initInfrastructure 初始化基础设施
func (c *Container) initInfrastructure() {
    // 初始化日志器
    c.logger = logger.NewLogger(c.config.Logger)
    
    // 初始化数据库
    db, err := database.NewPostgresDB(c.config.Database)
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
    c.db = db
    
    // 初始化Redis
    redisClient, err := cache.NewRedisClient(c.config.Redis)
    if err != nil {
        log.Fatal("Failed to connect to Redis:", err)
    }
    c.redisClient = redisClient
    
    // 初始化缓存服务
    c.cacheService = cache.NewCacheService(c.redisClient, c.logger)
    
    // 初始化存储服务
    c.storageService = storage.NewStorageService(c.config.Storage, c.logger)
}

// initRepositories 初始化仓库
func (c *Container) initRepositories() {
    c.photoRepo = persistence.NewPhotoRepository(c.db)
    c.categoryRepo = persistence.NewCategoryRepository(c.db)
    c.tagRepo = persistence.NewTagRepository(c.db)
    c.userRepo = persistence.NewUserRepository(c.db)
}

// initServices 初始化服务
func (c *Container) initServices() {
    // 领域服务
    slugGenerator := service.NewSlugGenerator()
    exifExtractor := service.NewEXIFExtractor()
    photoDomainService := service.NewPhotoDomainService(slugGenerator, exifExtractor)
    
    // 图片处理器
    imageProcessor := service.NewImageProcessor(c.storageService, c.logger)
    
    // 事件发布器
    eventPublisher := event.NewEventPublisher(c.redisClient, c.logger)
    
    // 应用服务
    c.photoService = service.NewPhotoService(
        c.photoRepo,
        c.categoryRepo,
        c.tagRepo,
        photoDomainService,
        imageProcessor,
        c.cacheService,
        eventPublisher,
        c.logger,
    )
    
    c.categoryService = service.NewCategoryService(
        c.categoryRepo,
        c.photoRepo,
        c.cacheService,
        c.logger,
    )
    
    c.tagService = service.NewTagService(
        c.tagRepo,
        c.photoRepo,
        c.cacheService,
        c.logger,
    )
    
    c.authService = service.NewAuthService(
        c.userRepo,
        c.config.JWT,
        c.logger,
    )
    
    c.uploadService = service.NewUploadService(
        c.storageService,
        imageProcessor,
        c.logger,
    )
}

// initHandlers 初始化处理器
func (c *Container) initHandlers() {
    c.photoHandler = handlers.NewPhotoHandler(c.photoService, c.logger)
    c.categoryHandler = handlers.NewCategoryHandler(c.categoryService, c.logger)
    c.tagHandler = handlers.NewTagHandler(c.tagService, c.logger)
    c.authHandler = handlers.NewAuthHandler(c.authService, c.logger)
    c.uploadHandler = handlers.NewUploadHandler(c.uploadService, c.logger)
}

// GetPhotoHandler 获取照片处理器
func (c *Container) GetPhotoHandler() *handlers.PhotoHandler {
    return c.photoHandler
}

// ... 其他getter方法

// Close 关闭容器资源
func (c *Container) Close() error {
    if c.redisClient != nil {
        c.redisClient.Close()
    }
    
    if c.db != nil {
        sqlDB, err := c.db.DB()
        if err == nil {
            sqlDB.Close()
        }
    }
    
    return nil
}

2.7 配置管理

2.7.1 配置结构

// pkg/config/config.go
package config

import (
    "fmt"
    "time"
    "github.com/spf13/viper"
)

// Config 应用配置
type Config struct {
    App      AppConfig      `mapstructure:"app"`
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    Storage  StorageConfig  `mapstructure:"storage"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Logger   LoggerConfig   `mapstructure:"logger"`
    Tracing  TracingConfig  `mapstructure:"tracing"`
    Upload   UploadConfig   `mapstructure:"upload"`
    Image    ImageConfig    `mapstructure:"image"`
}

// AppConfig 应用配置
type AppConfig struct {
    Name        string `mapstructure:"name"`
    Version     string `mapstructure:"version"`
    Environment string `mapstructure:"environment"`
    Debug       bool   `mapstructure:"debug"`
}

// ServerConfig 服务器配置
type ServerConfig struct {
    Host            string        `mapstructure:"host"`
    Port            int           `mapstructure:"port"`
    ReadTimeout     time.Duration `mapstructure:"read_timeout"`
    WriteTimeout    time.Duration `mapstructure:"write_timeout"`
    ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
    CORS            CORSConfig    `mapstructure:"cors"`
}

// CORSConfig CORS配置
type CORSConfig struct {
    AllowOrigins     []string `mapstructure:"allow_origins"`
    AllowMethods     []string `mapstructure:"allow_methods"`
    AllowHeaders     []string `mapstructure:"allow_headers"`
    ExposeHeaders    []string `mapstructure:"expose_headers"`
    AllowCredentials bool     `mapstructure:"allow_credentials"`
    MaxAge           int      `mapstructure:"max_age"`
}

// DatabaseConfig 数据库配置
type DatabaseConfig struct {
    Host            string        `mapstructure:"host"`
    Port            int           `mapstructure:"port"`
    User            string        `mapstructure:"user"`
    Password        string        `mapstructure:"password"`
    Database        string        `mapstructure:"database"`
    SSLMode         string        `mapstructure:"ssl_mode"`
    MaxOpenConns    int           `mapstructure:"max_open_conns"`
    MaxIdleConns    int           `mapstructure:"max_idle_conns"`
    ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
    LogLevel        string        `mapstructure:"log_level"`
}

// RedisConfig Redis配置
type RedisConfig struct {
    Host        string        `mapstructure:"host"`
    Port        int           `mapstructure:"port"`
    Password    string        `mapstructure:"password"`
    Database    int           `mapstructure:"database"`
    MaxRetries  int           `mapstructure:"max_retries"`
    PoolSize    int           `mapstructure:"pool_size"`
    IdleTimeout time.Duration `mapstructure:"idle_timeout"`
}

// StorageConfig 存储配置
type StorageConfig struct {
    Type      string      `mapstructure:"type"` // local, s3, minio
    Local     LocalConfig `mapstructure:"local"`
    S3        S3Config    `mapstructure:"s3"`
    CDNBaseURL string     `mapstructure:"cdn_base_url"`
}

// LocalConfig 本地存储配置
type LocalConfig struct {
    UploadDir string `mapstructure:"upload_dir"`
    BaseURL   string `mapstructure:"base_url"`
}

// S3Config S3/MinIO配置
type S3Config struct {
    Endpoint        string `mapstructure:"endpoint"`
    Region          string `mapstructure:"region"`
    AccessKeyID     string `mapstructure:"access_key_id"`
    SecretAccessKey string `mapstructure:"secret_access_key"`
    Bucket          string `mapstructure:"bucket"`
    UseSSL          bool   `mapstructure:"use_ssl"`
}

// JWTConfig JWT配置
type JWTConfig struct {
    SecretKey       string        `mapstructure:"secret_key"`
    Issuer          string        `mapstructure:"issuer"`
    AccessDuration  time.Duration `mapstructure:"access_duration"`
    RefreshDuration time.Duration `mapstructure:"refresh_duration"`
}

// LoggerConfig 日志配置
type LoggerConfig struct {
    Level    string `mapstructure:"level"`
    Format   string `mapstructure:"format"` // json, text
    Output   string `mapstructure:"output"` // stdout, file
    Filename string `mapstructure:"filename"`
    MaxSize  int    `mapstructure:"max_size"`
    MaxAge   int    `mapstructure:"max_age"`
    Compress bool   `mapstructure:"compress"`
}

// TracingConfig 链路追踪配置
type TracingConfig struct {
    Enabled     bool   `mapstructure:"enabled"`
    ServiceName string `mapstructure:"service_name"`
    Jaeger      struct {
        Endpoint string `mapstructure:"endpoint"`
        Username string `mapstructure:"username"`
        Password string `mapstructure:"password"`
    } `mapstructure:"jaeger"`
    SamplingRate float64 `mapstructure:"sampling_rate"`
}

// UploadConfig 上传配置
type UploadConfig struct {
    MaxFileSize      int64    `mapstructure:"max_file_size"`
    AllowedTypes     []string `mapstructure:"allowed_types"`
    MaxFilesPerBatch int      `mapstructure:"max_files_per_batch"`
    TempDir          string   `mapstructure:"temp_dir"`
    CleanupInterval  time.Duration `mapstructure:"cleanup_interval"`
}

// ImageConfig 图片处理配置
type ImageConfig struct {
    QualityJPG  int `mapstructure:"quality_jpg"`
    QualityWebP int `mapstructure:"quality_webp"`
    MaxWidth    int `mapstructure:"max_width"`
    MaxHeight   int `mapstructure:"max_height"`
    
    ThumbnailSizes map[string]ImageSize `mapstructure:"thumbnail_sizes"`
    
    WatermarkEnabled bool   `mapstructure:"watermark_enabled"`
    WatermarkText    string `mapstructure:"watermark_text"`
    WatermarkOpacity int    `mapstructure:"watermark_opacity"`
}

// ImageSize 图片尺寸配置
type ImageSize struct {
    Width   int `mapstructure:"width"`
    Height  int `mapstructure:"height"`
    Quality int `mapstructure:"quality"`
}

// LoadConfig 加载配置
func LoadConfig(configPath string) (*Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(configPath)
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")
    
    // 设置环境变量
    viper.AutomaticEnv()
    viper.SetEnvPrefix("PHOTOGRAPHY")
    
    // 设置默认值
    setDefaults()
    
    // 读取配置文件
    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("failed to read config file: %w", err)
    }
    
    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return nil, fmt.Errorf("failed to unmarshal config: %w", err)
    }
    
    // 验证配置
    if err := validateConfig(&config); err != nil {
        return nil, fmt.Errorf("invalid config: %w", err)
    }
    
    return &config, nil
}

// setDefaults 设置默认配置值
func setDefaults() {
    // 应用默认配置
    viper.SetDefault("app.name", "photography-backend")
    viper.SetDefault("app.version", "1.0.0")
    viper.SetDefault("app.environment", "development")
    viper.SetDefault("app.debug", false)
    
    // 服务器默认配置
    viper.SetDefault("server.host", "0.0.0.0")
    viper.SetDefault("server.port", 8080)
    viper.SetDefault("server.read_timeout", "30s")
    viper.SetDefault("server.write_timeout", "30s")
    viper.SetDefault("server.shutdown_timeout", "10s")
    
    // CORS默认配置
    viper.SetDefault("server.cors.allow_origins", []string{"*"})
    viper.SetDefault("server.cors.allow_methods", []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"})
    viper.SetDefault("server.cors.allow_headers", []string{"Content-Type", "Authorization"})
    viper.SetDefault("server.cors.max_age", 3600)
    
    // 数据库默认配置
    viper.SetDefault("database.host", "localhost")
    viper.SetDefault("database.port", 5432)
    viper.SetDefault("database.ssl_mode", "disable")
    viper.SetDefault("database.max_open_conns", 25)
    viper.SetDefault("database.max_idle_conns", 5)
    viper.SetDefault("database.conn_max_lifetime", "3600s")
    viper.SetDefault("database.log_level", "warn")
    
    // Redis默认配置
    viper.SetDefault("redis.host", "localhost")
    viper.SetDefault("redis.port", 6379)
    viper.SetDefault("redis.database", 0)
    viper.SetDefault("redis.max_retries", 3)
    viper.SetDefault("redis.pool_size", 10)
    viper.SetDefault("redis.idle_timeout", "300s")
    
    // 存储默认配置
    viper.SetDefault("storage.type", "local")
    viper.SetDefault("storage.local.upload_dir", "./uploads")
    viper.SetDefault("storage.local.base_url", "http://localhost:8080")
    
    // JWT默认配置
    viper.SetDefault("jwt.issuer", "photography-backend")
    viper.SetDefault("jwt.access_duration", "24h")
    viper.SetDefault("jwt.refresh_duration", "168h")
    
    // 日志默认配置
    viper.SetDefault("logger.level", "info")
    viper.SetDefault("logger.format", "json")
    viper.SetDefault("logger.output", "stdout")
    viper.SetDefault("logger.max_size", 100)
    viper.SetDefault("logger.max_age", 30)
    viper.SetDefault("logger.compress", true)
    
    // 链路追踪默认配置
    viper.SetDefault("tracing.enabled", true)
    viper.SetDefault("tracing.service_name", "photography-backend")
    viper.SetDefault("tracing.jaeger.endpoint", "http://localhost:14268/api/traces")
    viper.SetDefault("tracing.sampling_rate", 1.0)
    
    // 上传默认配置
    viper.SetDefault("upload.max_file_size", 52428800) // 50MB
    viper.SetDefault("upload.allowed_types", []string{"image/jpeg", "image/png", "image/raw"})
    viper.SetDefault("upload.max_files_per_batch", 50)
    viper.SetDefault("upload.temp_dir", "./temp")
    viper.SetDefault("upload.cleanup_interval", "1h")
    
    // 图片处理默认配置
    viper.SetDefault("image.quality_jpg", 85)
    viper.SetDefault("image.quality_webp", 80)
    viper.SetDefault("image.max_width", 1920)
    viper.SetDefault("image.max_height", 1080)
    viper.SetDefault("image.watermark_enabled", false)
    viper.SetDefault("image.watermark_opacity", 50)
    
    // 缩略图尺寸默认配置
    viper.SetDefault("image.thumbnail_sizes.thumb_small.width", 150)
    viper.SetDefault("image.thumbnail_sizes.thumb_small.height", 150)
    viper.SetDefault("image.thumbnail_sizes.thumb_small.quality", 80)
    
    viper.SetDefault("image.thumbnail_sizes.thumb_medium.width", 300)
    viper.SetDefault("image.thumbnail_sizes.thumb_medium.height", 300)
    viper.SetDefault("image.thumbnail_sizes.thumb_medium.quality", 85)
    
    viper.SetDefault("image.thumbnail_sizes.thumb_large.width", 600)
    viper.SetDefault("image.thumbnail_sizes.thumb_large.height", 600)
    viper.SetDefault("image.thumbnail_sizes.thumb_large.quality", 90)
}

// validateConfig 验证配置
func validateConfig(config *Config) error {
    if config.App.Name == "" {
        return fmt.Errorf("app name cannot be empty")
    }
    
    if config.Server.Port <= 0 || config.Server.Port > 65535 {
        return fmt.Errorf("invalid server port: %d", config.Server.Port)
    }
    
    if config.Database.Host == "" {
        return fmt.Errorf("database host cannot be empty")
    }
    
    if config.JWT.SecretKey == "" {
        return fmt.Errorf("JWT secret key cannot be empty")
    }
    
    return nil
}

2.8 构建和部署

2.8.1 Makefile

# Makefile
.PHONY: build test clean run docker-build docker-run migrate-up migrate-down

# 变量定义
APP_NAME = photography-backend
VERSION = $(shell git describe --tags --always --dirty)
BUILD_TIME = $(shell date +%Y-%m-%dT%H:%M:%S)
GO_VERSION = $(shell go version | awk '{print $$3}')
LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"

# 构建
build:
	@echo "Building $(APP_NAME)..."
	@go build $(LDFLAGS) -o bin/$(APP_NAME) cmd/server/main.go

# 运行
run:
	@echo "Running $(APP_NAME)..."
	@go run cmd/server/main.go

# 测试
test:
	@echo "Running tests..."
	@go test -v -race -coverprofile=coverage.out ./...

# 测试覆盖率
test-coverage: test
	@go tool cover -html=coverage.out -o coverage.html
	@echo "Coverage report generated: coverage.html"

# 清理
clean:
	@echo "Cleaning..."
	@rm -rf bin/
	@rm -f coverage.out coverage.html
	@go clean -cache

# 代码检查
lint:
	@echo "Running linter..."
	@golangci-lint run

# 格式化代码
fmt:
	@echo "Formatting code..."
	@go fmt ./...
	@goimports -w .

# 生成模拟代码
generate:
	@echo "Generating mocks..."
	@go generate ./...

# 数据库迁移
migrate-up:
	@echo "Running database migrations..."
	@migrate -path migrations -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" up

migrate-down:
	@echo "Rolling back database migrations..."
	@migrate -path migrations -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" down

# 创建新的迁移文件
migrate-create:
	@echo "Creating new migration: $(NAME)"
	@migrate create -ext sql -dir migrations $(NAME)

# Docker构建
docker-build:
	@echo "Building Docker image..."
	@docker build -t $(APP_NAME):$(VERSION) -t $(APP_NAME):latest .

# Docker运行
docker-run:
	@echo "Running Docker container..."
	@docker-compose up -d

# Docker停止
docker-stop:
	@echo "Stopping Docker containers..."
	@docker-compose down

# 安装依赖
deps:
	@echo "Installing dependencies..."
	@go mod download
	@go mod tidy

# 安装开发工具
install-tools:
	@echo "Installing development tools..."
	@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
	@go install golang.org/x/tools/cmd/goimports@latest
	@go install github.com/golang/mock/mockgen@latest
	@go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# 开发环境设置
dev-setup: install-tools deps
	@echo "Setting up development environment..."
	@cp config/config.example.yaml config/config.yaml
	@mkdir -p uploads temp logs

# 生产构建
build-prod:
	@echo "Building for production..."
	@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -a -installsuffix cgo -o bin/$(APP_NAME) cmd/server/main.go

# 帮助信息
help:
	@echo "Available commands:"
	@echo "  build         - Build the application"
	@echo "  run           - Run the application"
	@echo "  test          - Run tests"
	@echo "  test-coverage - Run tests with coverage report"
	@echo "  clean         - Clean build artifacts"
	@echo "  lint          - Run linter"
	@echo "  fmt           - Format code"
	@echo "  generate      - Generate mocks"
	@echo "  migrate-up    - Run database migrations"
	@echo "  migrate-down  - Rollback database migrations"
	@echo "  migrate-create- Create new migration file (NAME=migration_name)"
	@echo "  docker-build  - Build Docker image"
	@echo "  docker-run    - Run with Docker Compose"
	@echo "  docker-stop   - Stop Docker containers"
	@echo "  deps          - Install dependencies"
	@echo "  install-tools - Install development tools"
	@echo "  dev-setup     - Setup development environment"
	@echo "  build-prod    - Build for production"
	@echo "  help          - Show this help message"

2.8.2 Dockerfile

# 多阶段构建
FROM golang:1.21-alpine AS builder

# 安装必要的包
RUN apk add --no-cache git ca-certificates tzdata vips-dev gcc musl-dev

# 设置工作目录
WORKDIR /app

# 复制go.mod和go.sum
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go

# 最终镜像
FROM alpine:latest

# 安装运行时依赖
RUN apk --no-cache add ca-certificates vips-dev tzdata

# 创建非root用户
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# 设置工作目录
WORKDIR /app

# 从构建阶段复制二进制文件
COPY --from=builder /app/main .

# 复制配置文件和迁移文件
COPY --from=builder /app/config ./config
COPY --from=builder /app/migrations ./migrations

# 创建必要的目录
RUN mkdir -p uploads temp logs && \
    chown -R appuser:appgroup /app

# 切换到非root用户
USER appuser

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# 运行应用
CMD ["./main"]

2.8.3 docker-compose.yml

version: '3.8'

services:
  # 应用服务
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - PHOTOGRAPHY_DATABASE_HOST=postgres
      - PHOTOGRAPHY_DATABASE_PORT=5432
      - PHOTOGRAPHY_DATABASE_USER=postgres
      - PHOTOGRAPHY_DATABASE_PASSWORD=password
      - PHOTOGRAPHY_DATABASE_DATABASE=photography
      - PHOTOGRAPHY_REDIS_HOST=redis
      - PHOTOGRAPHY_REDIS_PORT=6379
      - PHOTOGRAPHY_STORAGE_TYPE=minio
      - PHOTOGRAPHY_STORAGE_S3_ENDPOINT=minio:9000
      - PHOTOGRAPHY_STORAGE_S3_ACCESS_KEY_ID=minioadmin
      - PHOTOGRAPHY_STORAGE_S3_SECRET_ACCESS_KEY=minioadmin
      - PHOTOGRAPHY_STORAGE_S3_BUCKET=photography
      - PHOTOGRAPHY_STORAGE_S3_USE_SSL=false
    depends_on:
      - postgres
      - redis
      - minio
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    restart: unless-stopped
    networks:
      - photography-network

  # PostgreSQL数据库
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=photography
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    restart: unless-stopped
    networks:
      - photography-network

  # Redis缓存
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    restart: unless-stopped
    networks:
      - photography-network

  # MinIO对象存储
  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      - MINIO_ROOT_USER=minioadmin
      - MINIO_ROOT_PASSWORD=minioadmin
    volumes:
      - minio_data:/data
    ports:
      - "9000:9000"
      - "9001:9001"
    restart: unless-stopped
    networks:
      - photography-network

  # Jaeger链路追踪
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"
      - "14268:14268"
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    restart: unless-stopped
    networks:
      - photography-network

  # Nginx反向代理可选
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - photography-network

volumes:
  postgres_data:
  redis_data:
  minio_data:

networks:
  photography-network:
    driver: bridge

2.9 总结

<function_calls> [{"id": "1", "content": "\u521b\u5efa\u8be6\u7ec6\u7684\u540e\u7aef\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "2", "content": "\u521b\u5efa\u7ba1\u7406\u540e\u53f0\u529f\u80fd\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "3", "content": "\u521b\u5efa\u6570\u636e\u5e93\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "4", "content": "\u521b\u5efaAPI\u63a5\u53e3\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "5", "content": "\u521b\u5efaGolang\u9879\u76ee\u67b6\u6784\u6587\u6863", "status": "completed", "priority": "high"}]