🚀 主要功能: - 完善后端API服务层,实现完整的CRUD操作 - 开发管理后台所有核心页面 (仪表板、照片、分类、标签、用户、设置) - 完成前后端完全集成,所有API接口正常对接 - 配置完整的CI/CD流水线,支持自动化部署 🎯 后端完善: - 实现PhotoService, CategoryService, TagService, UserService - 添加完整的API处理器和路由配置 - 支持Docker容器化部署 - 添加数据库迁移和健康检查 🎨 管理后台完成: - 仪表板: 实时统计数据展示 - 照片管理: 完整的CRUD操作,支持批量处理 - 分类管理: 树形结构展示和管理 - 标签管理: 颜色标签和统计信息 - 用户管理: 角色权限控制 - 系统设置: 多标签配置界面 - 添加pre-commit代码质量检查 🔧 部署配置: - Docker Compose完整配置 - 后端CI/CD流水线 (Docker部署) - 管理后台CI/CD流水线 (静态文件部署) - 前端CI/CD流水线优化 - 自动化脚本: 部署、备份、监控 - 完整的部署文档和运维指南 ✅ 集成完成: - 所有API接口正常连接 - 认证系统完整集成 - 数据获取和状态管理 - 错误处理和用户反馈 - 响应式设计优化
678 lines
18 KiB
Go
678 lines
18 KiB
Go
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. 更新数据库记录
|
|
} |