package service import ( "context" "errors" "fmt" "mime/multipart" "path/filepath" "strconv" "strings" "time" "photography-backend/internal/config" "photography-backend/internal/models" "photography-backend/internal/service/storage" "photography-backend/internal/utils" "go.uber.org/zap" "gorm.io/gorm" ) type PhotoService struct { db *gorm.DB config *config.Config logger *zap.Logger storageService *storage.StorageService } func NewPhotoService(db *gorm.DB, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService { return &PhotoService{ db: db, config: config, logger: logger, storageService: storageService, } } // PhotoListParams 照片列表查询参数 type PhotoListParams struct { Page int `json:"page" form:"page"` Limit int `json:"limit" form:"limit"` Search string `json:"search" form:"search"` Status string `json:"status" form:"status"` CategoryID uint `json:"category_id" form:"category_id"` Tags []string `json:"tags" form:"tags"` StartDate string `json:"start_date" form:"start_date"` EndDate string `json:"end_date" form:"end_date"` SortBy string `json:"sort_by" form:"sort_by"` SortOrder string `json:"sort_order" form:"sort_order"` } // PhotoListResponse 照片列表响应 type PhotoListResponse struct { Photos []models.Photo `json:"photos"` Total int64 `json:"total"` Page int `json:"page"` Limit int `json:"limit"` Pages int `json:"pages"` } // GetPhotos 获取照片列表 func (s *PhotoService) GetPhotos(ctx context.Context, params PhotoListParams) (*PhotoListResponse, error) { // 设置默认值 if params.Page <= 0 { params.Page = 1 } if params.Limit <= 0 { params.Limit = 20 } if params.Limit > 100 { params.Limit = 100 } // 构建查询 query := s.db.WithContext(ctx). Preload("Categories"). Preload("Tags"). Preload("Formats") // 搜索过滤 if params.Search != "" { searchPattern := "%" + params.Search + "%" query = query.Where("title ILIKE ? OR description ILIKE ? OR original_filename ILIKE ?", searchPattern, searchPattern, searchPattern) } // 状态过滤 if params.Status != "" { query = query.Where("status = ?", params.Status) } // 分类过滤 if params.CategoryID > 0 { query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). Where("photo_categories.category_id = ?", params.CategoryID) } // 标签过滤 if len(params.Tags) > 0 { query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). Joins("JOIN tags ON photo_tags.tag_id = tags.id"). Where("tags.slug IN ?", params.Tags) } // 日期过滤 if params.StartDate != "" { if startDate, err := time.Parse("2006-01-02", params.StartDate); err == nil { query = query.Where("taken_at >= ?", startDate) } } if params.EndDate != "" { if endDate, err := time.Parse("2006-01-02", params.EndDate); err == nil { query = query.Where("taken_at <= ?", endDate) } } // 排序 sortBy := "created_at" sortOrder := "desc" if params.SortBy != "" { allowedSortFields := []string{"created_at", "updated_at", "taken_at", "title", "file_size"} if utils.Contains(allowedSortFields, params.SortBy) { sortBy = params.SortBy } } if params.SortOrder == "asc" { sortOrder = "asc" } // 计算总数 var total int64 countQuery := query if err := countQuery.Model(&models.Photo{}).Count(&total).Error; err != nil { s.logger.Error("Failed to count photos", zap.Error(err)) return nil, err } // 分页查询 offset := (params.Page - 1) * params.Limit var photos []models.Photo if err := query. Order(fmt.Sprintf("%s %s", sortBy, sortOrder)). Offset(offset). Limit(params.Limit). Find(&photos).Error; err != nil { s.logger.Error("Failed to get photos", zap.Error(err)) return nil, err } // 计算总页数 pages := int((total + int64(params.Limit) - 1) / int64(params.Limit)) return &PhotoListResponse{ Photos: photos, Total: total, Page: params.Page, Limit: params.Limit, Pages: pages, }, nil } // GetPhotoByID 根据ID获取照片 func (s *PhotoService) GetPhotoByID(ctx context.Context, id uint) (*models.Photo, error) { var photo models.Photo if err := s.db.WithContext(ctx). Preload("Categories"). Preload("Tags"). Preload("Formats"). First(&photo, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("photo not found") } s.logger.Error("Failed to get photo by ID", zap.Error(err), zap.Uint("id", id)) return nil, err } return &photo, nil } // CreatePhoto 创建照片 func (s *PhotoService) CreatePhoto(ctx context.Context, req *models.CreatePhotoRequest) (*models.Photo, error) { // 生成唯一的文件名 uniqueFilename := utils.GenerateUniqueFilename(req.OriginalFilename) photo := &models.Photo{ Title: req.Title, Description: req.Description, OriginalFilename: req.OriginalFilename, UniqueFilename: uniqueFilename, FileSize: req.FileSize, Status: req.Status, Camera: req.Camera, Lens: req.Lens, ISO: req.ISO, Aperture: req.Aperture, ShutterSpeed: req.ShutterSpeed, FocalLength: req.FocalLength, TakenAt: req.TakenAt, } // 开始事务 tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return nil, tx.Error } defer tx.Rollback() // 创建照片记录 if err := tx.Create(photo).Error; err != nil { s.logger.Error("Failed to create photo", zap.Error(err)) return nil, err } // 关联分类 if len(req.CategoryIDs) > 0 { var categories []models.Category if err := tx.Where("id IN ?", req.CategoryIDs).Find(&categories).Error; err != nil { s.logger.Error("Failed to find categories", zap.Error(err)) return nil, err } if err := tx.Model(photo).Association("Categories").Replace(categories); err != nil { s.logger.Error("Failed to associate categories", zap.Error(err)) return nil, err } } // 关联标签 if len(req.TagIDs) > 0 { var tags []models.Tag if err := tx.Where("id IN ?", req.TagIDs).Find(&tags).Error; err != nil { s.logger.Error("Failed to find tags", zap.Error(err)) return nil, err } if err := tx.Model(photo).Association("Tags").Replace(tags); err != nil { s.logger.Error("Failed to associate tags", zap.Error(err)) return nil, err } } // 提交事务 if err := tx.Commit().Error; err != nil { s.logger.Error("Failed to commit transaction", zap.Error(err)) return nil, err } // 重新加载关联数据 if err := s.db.WithContext(ctx). Preload("Categories"). Preload("Tags"). Preload("Formats"). First(photo, photo.ID).Error; err != nil { s.logger.Error("Failed to reload photo", zap.Error(err)) return nil, err } s.logger.Info("Photo created successfully", zap.Uint("id", photo.ID)) return photo, nil } // UpdatePhoto 更新照片 func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *models.UpdatePhotoRequest) (*models.Photo, error) { // 检查照片是否存在 var photo models.Photo if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("photo not found") } return nil, err } // 开始事务 tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return nil, tx.Error } defer tx.Rollback() // 更新照片基本信息 updates := map[string]interface{}{} if req.Title != nil { updates["title"] = *req.Title } if req.Description != nil { updates["description"] = *req.Description } if req.Status != nil { updates["status"] = *req.Status } if req.Camera != nil { updates["camera"] = *req.Camera } if req.Lens != nil { updates["lens"] = *req.Lens } if req.ISO != nil { updates["iso"] = *req.ISO } if req.Aperture != nil { updates["aperture"] = *req.Aperture } if req.ShutterSpeed != nil { updates["shutter_speed"] = *req.ShutterSpeed } if req.FocalLength != nil { updates["focal_length"] = *req.FocalLength } if req.TakenAt != nil { updates["taken_at"] = *req.TakenAt } if len(updates) > 0 { if err := tx.Model(&photo).Updates(updates).Error; err != nil { s.logger.Error("Failed to update photo", zap.Error(err)) return nil, err } } // 更新分类关联 if req.CategoryIDs != nil { var categories []models.Category if len(*req.CategoryIDs) > 0 { if err := tx.Where("id IN ?", *req.CategoryIDs).Find(&categories).Error; err != nil { s.logger.Error("Failed to find categories", zap.Error(err)) return nil, err } } if err := tx.Model(&photo).Association("Categories").Replace(categories); err != nil { s.logger.Error("Failed to update categories", zap.Error(err)) return nil, err } } // 更新标签关联 if req.TagIDs != nil { var tags []models.Tag if len(*req.TagIDs) > 0 { if err := tx.Where("id IN ?", *req.TagIDs).Find(&tags).Error; err != nil { s.logger.Error("Failed to find tags", zap.Error(err)) return nil, err } } if err := tx.Model(&photo).Association("Tags").Replace(tags); err != nil { s.logger.Error("Failed to update tags", zap.Error(err)) return nil, err } } // 提交事务 if err := tx.Commit().Error; err != nil { s.logger.Error("Failed to commit transaction", zap.Error(err)) return nil, err } // 重新加载照片数据 if err := s.db.WithContext(ctx). Preload("Categories"). Preload("Tags"). Preload("Formats"). First(&photo, id).Error; err != nil { s.logger.Error("Failed to reload photo", zap.Error(err)) return nil, err } s.logger.Info("Photo updated successfully", zap.Uint("id", id)) return &photo, nil } // DeletePhoto 删除照片 func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error { // 检查照片是否存在 var photo models.Photo if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("photo not found") } return err } // 开始事务 tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return tx.Error } defer tx.Rollback() // 删除关联的格式文件 if err := tx.Where("photo_id = ?", id).Delete(&models.PhotoFormat{}).Error; err != nil { s.logger.Error("Failed to delete photo formats", zap.Error(err)) return err } // 删除关联关系 if err := tx.Model(&photo).Association("Categories").Clear(); err != nil { s.logger.Error("Failed to clear categories", zap.Error(err)) return err } if err := tx.Model(&photo).Association("Tags").Clear(); err != nil { s.logger.Error("Failed to clear tags", zap.Error(err)) return err } // 删除照片记录 if err := tx.Delete(&photo).Error; err != nil { s.logger.Error("Failed to delete photo", zap.Error(err)) return err } // 提交事务 if err := tx.Commit().Error; err != nil { s.logger.Error("Failed to commit transaction", zap.Error(err)) return err } // 异步删除文件 go func() { if err := s.storageService.DeletePhoto(photo.UniqueFilename); err != nil { s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename)) } }() s.logger.Info("Photo deleted successfully", zap.Uint("id", id)) return nil } // UploadPhoto 上传照片 func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, header *multipart.FileHeader, req *models.CreatePhotoRequest) (*models.Photo, error) { // 验证文件类型 if !s.isValidImageFile(header.Filename) { return nil, errors.New("invalid file type") } // 验证文件大小 if header.Size > s.config.Upload.MaxFileSize { return nil, errors.New("file size too large") } // 生成唯一文件名 uniqueFilename := utils.GenerateUniqueFilename(header.Filename) // 上传文件到存储服务 uploadedFile, err := s.storageService.UploadPhoto(ctx, file, uniqueFilename) if err != nil { s.logger.Error("Failed to upload photo", zap.Error(err)) return nil, err } // 创建照片记录 req.OriginalFilename = header.Filename req.FileSize = header.Size photo, err := s.CreatePhoto(ctx, req) if err != nil { // 如果创建记录失败,删除已上传的文件 go func() { if err := s.storageService.DeletePhoto(uniqueFilename); err != nil { s.logger.Error("Failed to cleanup uploaded file", zap.Error(err)) } }() return nil, err } // 异步处理图片格式转换 go func() { s.processPhotoFormats(context.Background(), photo, uploadedFile) }() return photo, nil } // BatchUpdatePhotos 批量更新照片 func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *models.BatchUpdatePhotosRequest) error { if len(ids) == 0 { return errors.New("no photos to update") } // 开始事务 tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return tx.Error } defer tx.Rollback() // 构建更新数据 updates := map[string]interface{}{} if req.Status != nil { updates["status"] = *req.Status } // 基础字段更新 if len(updates) > 0 { if err := tx.Model(&models.Photo{}).Where("id IN ?", ids).Updates(updates).Error; err != nil { s.logger.Error("Failed to batch update photos", zap.Error(err)) return err } } // 批量更新分类 if req.CategoryIDs != nil { // 先删除现有关联 if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil { return err } // 添加新关联 if len(*req.CategoryIDs) > 0 { for _, photoID := range ids { for _, categoryID := range *req.CategoryIDs { if err := tx.Exec("INSERT INTO photo_categories (photo_id, category_id) VALUES (?, ?)", photoID, categoryID).Error; err != nil { return err } } } } } // 批量更新标签 if req.TagIDs != nil { // 先删除现有关联 if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil { return err } // 添加新关联 if len(*req.TagIDs) > 0 { for _, photoID := range ids { for _, tagID := range *req.TagIDs { if err := tx.Exec("INSERT INTO photo_tags (photo_id, tag_id) VALUES (?, ?)", photoID, tagID).Error; err != nil { return err } } } } } // 提交事务 if err := tx.Commit().Error; err != nil { s.logger.Error("Failed to commit batch update", zap.Error(err)) return err } s.logger.Info("Batch update completed", zap.Int("count", len(ids))) return nil } // BatchDeletePhotos 批量删除照片 func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error { if len(ids) == 0 { return errors.New("no photos to delete") } // 获取要删除的照片信息 var photos []models.Photo if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&photos).Error; err != nil { return err } // 开始事务 tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return tx.Error } defer tx.Rollback() // 删除关联的格式文件 if err := tx.Where("photo_id IN ?", ids).Delete(&models.PhotoFormat{}).Error; err != nil { return err } // 删除关联关系 if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil { return err } if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil { return err } // 删除照片记录 if err := tx.Where("id IN ?", ids).Delete(&models.Photo{}).Error; err != nil { return err } // 提交事务 if err := tx.Commit().Error; err != nil { return err } // 异步删除文件 go func() { for _, photo := range photos { if err := s.storageService.DeletePhoto(photo.UniqueFilename); err != nil { s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename)) } } }() s.logger.Info("Batch delete completed", zap.Int("count", len(ids))) return nil } // GetPhotoStats 获取照片统计信息 func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, error) { var stats models.PhotoStats // 总数统计 if err := s.db.WithContext(ctx).Model(&models.Photo{}).Count(&stats.Total).Error; err != nil { return nil, err } // 按状态统计 var statusStats []struct { Status string `json:"status"` Count int64 `json:"count"` } if err := s.db.WithContext(ctx).Model(&models.Photo{}). Select("status, COUNT(*) as count"). Group("status"). Find(&statusStats).Error; err != nil { return nil, err } stats.StatusStats = make(map[string]int64) for _, stat := range statusStats { stats.StatusStats[stat.Status] = stat.Count } // 本月新增 startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1) if err := s.db.WithContext(ctx).Model(&models.Photo{}). Where("created_at >= ?", startOfMonth). Count(&stats.ThisMonth).Error; err != nil { return nil, err } // 今日新增 startOfDay := time.Now().Truncate(24 * time.Hour) if err := s.db.WithContext(ctx).Model(&models.Photo{}). Where("created_at >= ?", startOfDay). Count(&stats.Today).Error; err != nil { return nil, err } // 总存储大小 var totalSize sql.NullInt64 if err := s.db.WithContext(ctx).Model(&models.Photo{}). Select("SUM(file_size)"). Row().Scan(&totalSize); err != nil { return nil, err } if totalSize.Valid { stats.TotalSize = totalSize.Int64 } return &stats, nil } // isValidImageFile 验证图片文件类型 func (s *PhotoService) isValidImageFile(filename string) bool { ext := strings.ToLower(filepath.Ext(filename)) allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"} return utils.Contains(allowedExts, ext) } // processPhotoFormats 处理照片格式转换 func (s *PhotoService) processPhotoFormats(ctx context.Context, photo *models.Photo, uploadedFile *storage.UploadedFile) { // 这里将实现图片格式转换逻辑 // 生成不同尺寸和格式的图片 // 更新 photo_formats 表 s.logger.Info("Processing photo formats", zap.Uint("photo_id", photo.ID)) // TODO: 实现图片处理逻辑 // 1. 生成缩略图 // 2. 生成不同尺寸的图片 // 3. 转换为不同格式 (WebP, AVIF) // 4. 更新数据库记录 }