Files
photography/backend-old/internal/service/photo_service.go
xujiang 010fe2a8c7
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 5m8s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
fix
2025-07-10 18:09:11 +08:00

677 lines
18 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"mime/multipart"
"path/filepath"
"strings"
"time"
"photography-backend/internal/config"
"photography-backend/internal/model/entity"
"photography-backend/internal/repository/interfaces"
"photography-backend/internal/service/storage"
"photography-backend/internal/utils"
"go.uber.org/zap"
)
type PhotoService struct {
photoRepo interfaces.PhotoRepository
config *config.Config
logger *zap.Logger
storageService *storage.StorageService
}
func NewPhotoService(photoRepo interfaces.PhotoRepository, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService {
return &PhotoService{
photoRepo: photoRepo,
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 []entity.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(&entity.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 []entity.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) (*entity.Photo, error) {
var photo entity.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 *entity.CreatePhotoRequest) (*entity.Photo, error) {
// 生成唯一的文件名
uniqueFilename := utils.GenerateUniqueFilename(req.OriginalFilename)
photo := &entity.Photo{
Title: req.Title,
Description: req.Description,
OriginalFilename: req.OriginalFilename,
UniqueFilename: uniqueFilename,
FileSize: req.FileSize,
Status: entity.PhotoStatus(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 []entity.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 []entity.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 *entity.UpdatePhotoRequest) (*entity.Photo, error) {
// 检查照片是否存在
var photo entity.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 []entity.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 []entity.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 entity.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(&entity.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 *entity.CreatePhotoRequest) (*entity.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 *entity.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(&entity.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 []entity.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(&entity.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(&entity.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) (*entity.PhotoStats, error) {
var stats entity.PhotoStats
// 总数统计
if err := s.db.WithContext(ctx).Model(&entity.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(&entity.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(&entity.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(&entity.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(&entity.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 *entity.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. 更新数据库记录
}