refactor: 重构后端架构为 go-zero 框架,优化项目结构
主要变更: - 采用 go-zero 框架替代 Gin,提升开发效率 - 重构项目结构,API 文件模块化组织 - 将 model 移至 api/internal/model 目录 - 移除 common 包,改为标准 pkg 目录结构 - 实现统一的仓储模式,支持配置驱动数据库切换 - 简化测试策略,专注 API 集成测试 - 更新 CLAUDE.md 文档,提供详细的开发指导 技术栈更新: - 框架: Gin → go-zero v1.6.0+ - 代码生成: 引入 goctl 工具 - 架构模式: 四层架构 → go-zero 三层架构 (Handler→Logic→Model) - 项目布局: 遵循 Go 社区标准和 go-zero 最佳实践
This commit is contained in:
@ -6,29 +6,28 @@ import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/config"
|
||||
"photography-backend/internal/models"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
"photography-backend/internal/service/storage"
|
||||
"photography-backend/internal/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PhotoService struct {
|
||||
db *gorm.DB
|
||||
photoRepo interfaces.PhotoRepository
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
storageService *storage.StorageService
|
||||
}
|
||||
|
||||
func NewPhotoService(db *gorm.DB, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService {
|
||||
func NewPhotoService(photoRepo interfaces.PhotoRepository, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService {
|
||||
return &PhotoService{
|
||||
db: db,
|
||||
photoRepo: photoRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
storageService: storageService,
|
||||
@ -51,7 +50,7 @@ type PhotoListParams struct {
|
||||
|
||||
// PhotoListResponse 照片列表响应
|
||||
type PhotoListResponse struct {
|
||||
Photos []models.Photo `json:"photos"`
|
||||
Photos []entity.Photo `json:"photos"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
@ -130,14 +129,14 @@ func (s *PhotoService) GetPhotos(ctx context.Context, params PhotoListParams) (*
|
||||
// 计算总数
|
||||
var total int64
|
||||
countQuery := query
|
||||
if err := countQuery.Model(&models.Photo{}).Count(&total).Error; err != nil {
|
||||
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 []models.Photo
|
||||
var photos []entity.Photo
|
||||
if err := query.
|
||||
Order(fmt.Sprintf("%s %s", sortBy, sortOrder)).
|
||||
Offset(offset).
|
||||
@ -160,8 +159,8 @@ func (s *PhotoService) GetPhotos(ctx context.Context, params PhotoListParams) (*
|
||||
}
|
||||
|
||||
// GetPhotoByID 根据ID获取照片
|
||||
func (s *PhotoService) GetPhotoByID(ctx context.Context, id uint) (*models.Photo, error) {
|
||||
var photo models.Photo
|
||||
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").
|
||||
@ -178,17 +177,17 @@ func (s *PhotoService) GetPhotoByID(ctx context.Context, id uint) (*models.Photo
|
||||
}
|
||||
|
||||
// CreatePhoto 创建照片
|
||||
func (s *PhotoService) CreatePhoto(ctx context.Context, req *models.CreatePhotoRequest) (*models.Photo, error) {
|
||||
func (s *PhotoService) CreatePhoto(ctx context.Context, req *entity.CreatePhotoRequest) (*entity.Photo, error) {
|
||||
// 生成唯一的文件名
|
||||
uniqueFilename := utils.GenerateUniqueFilename(req.OriginalFilename)
|
||||
|
||||
photo := &models.Photo{
|
||||
photo := &entity.Photo{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
OriginalFilename: req.OriginalFilename,
|
||||
UniqueFilename: uniqueFilename,
|
||||
FileSize: req.FileSize,
|
||||
Status: req.Status,
|
||||
Status: entity.PhotoStatus(req.Status),
|
||||
Camera: req.Camera,
|
||||
Lens: req.Lens,
|
||||
ISO: req.ISO,
|
||||
@ -213,7 +212,7 @@ func (s *PhotoService) CreatePhoto(ctx context.Context, req *models.CreatePhotoR
|
||||
|
||||
// 关联分类
|
||||
if len(req.CategoryIDs) > 0 {
|
||||
var categories []models.Category
|
||||
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
|
||||
@ -226,7 +225,7 @@ func (s *PhotoService) CreatePhoto(ctx context.Context, req *models.CreatePhotoR
|
||||
|
||||
// 关联标签
|
||||
if len(req.TagIDs) > 0 {
|
||||
var tags []models.Tag
|
||||
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
|
||||
@ -258,9 +257,9 @@ func (s *PhotoService) CreatePhoto(ctx context.Context, req *models.CreatePhotoR
|
||||
}
|
||||
|
||||
// UpdatePhoto 更新照片
|
||||
func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *models.UpdatePhotoRequest) (*models.Photo, error) {
|
||||
func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *entity.UpdatePhotoRequest) (*entity.Photo, error) {
|
||||
// 检查照片是否存在
|
||||
var photo models.Photo
|
||||
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")
|
||||
@ -317,7 +316,7 @@ func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *models.Upd
|
||||
|
||||
// 更新分类关联
|
||||
if req.CategoryIDs != nil {
|
||||
var categories []models.Category
|
||||
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))
|
||||
@ -332,7 +331,7 @@ func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *models.Upd
|
||||
|
||||
// 更新标签关联
|
||||
if req.TagIDs != nil {
|
||||
var tags []models.Tag
|
||||
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))
|
||||
@ -368,7 +367,7 @@ func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *models.Upd
|
||||
// DeletePhoto 删除照片
|
||||
func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error {
|
||||
// 检查照片是否存在
|
||||
var photo models.Photo
|
||||
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")
|
||||
@ -384,7 +383,7 @@ func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error {
|
||||
defer tx.Rollback()
|
||||
|
||||
// 删除关联的格式文件
|
||||
if err := tx.Where("photo_id = ?", id).Delete(&models.PhotoFormat{}).Error; err != nil {
|
||||
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
|
||||
}
|
||||
@ -414,7 +413,7 @@ func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error {
|
||||
|
||||
// 异步删除文件
|
||||
go func() {
|
||||
if err := s.storageService.DeletePhoto(photo.UniqueFilename); err != nil {
|
||||
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))
|
||||
}
|
||||
}()
|
||||
@ -424,7 +423,7 @@ func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片
|
||||
func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, header *multipart.FileHeader, req *models.CreatePhotoRequest) (*models.Photo, error) {
|
||||
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")
|
||||
@ -439,7 +438,7 @@ func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, hea
|
||||
uniqueFilename := utils.GenerateUniqueFilename(header.Filename)
|
||||
|
||||
// 上传文件到存储服务
|
||||
uploadedFile, err := s.storageService.UploadPhoto(ctx, file, uniqueFilename)
|
||||
uploadedFile, err := (*s.storageService).UploadPhoto(ctx, file, uniqueFilename)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to upload photo", zap.Error(err))
|
||||
return nil, err
|
||||
@ -453,7 +452,7 @@ func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, hea
|
||||
if err != nil {
|
||||
// 如果创建记录失败,删除已上传的文件
|
||||
go func() {
|
||||
if err := s.storageService.DeletePhoto(uniqueFilename); err != nil {
|
||||
if err := (*s.storageService).DeletePhoto(uniqueFilename); err != nil {
|
||||
s.logger.Error("Failed to cleanup uploaded file", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
@ -469,7 +468,7 @@ func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, hea
|
||||
}
|
||||
|
||||
// BatchUpdatePhotos 批量更新照片
|
||||
func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *models.BatchUpdatePhotosRequest) error {
|
||||
func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *entity.BatchUpdatePhotosRequest) error {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("no photos to update")
|
||||
}
|
||||
@ -489,7 +488,7 @@ func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *m
|
||||
|
||||
// 基础字段更新
|
||||
if len(updates) > 0 {
|
||||
if err := tx.Model(&models.Photo{}).Where("id IN ?", ids).Updates(updates).Error; err != nil {
|
||||
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
|
||||
}
|
||||
@ -550,7 +549,7 @@ func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error
|
||||
}
|
||||
|
||||
// 获取要删除的照片信息
|
||||
var photos []models.Photo
|
||||
var photos []entity.Photo
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&photos).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@ -563,7 +562,7 @@ func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error
|
||||
defer tx.Rollback()
|
||||
|
||||
// 删除关联的格式文件
|
||||
if err := tx.Where("photo_id IN ?", ids).Delete(&models.PhotoFormat{}).Error; err != nil {
|
||||
if err := tx.Where("photo_id IN ?", ids).Delete(&entity.PhotoFormat{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -577,7 +576,7 @@ func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error
|
||||
}
|
||||
|
||||
// 删除照片记录
|
||||
if err := tx.Where("id IN ?", ids).Delete(&models.Photo{}).Error; err != nil {
|
||||
if err := tx.Where("id IN ?", ids).Delete(&entity.Photo{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -589,7 +588,7 @@ func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error
|
||||
// 异步删除文件
|
||||
go func() {
|
||||
for _, photo := range photos {
|
||||
if err := s.storageService.DeletePhoto(photo.UniqueFilename); err != nil {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -600,11 +599,11 @@ func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error
|
||||
}
|
||||
|
||||
// GetPhotoStats 获取照片统计信息
|
||||
func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, error) {
|
||||
var stats models.PhotoStats
|
||||
func (s *PhotoService) GetPhotoStats(ctx context.Context) (*entity.PhotoStats, error) {
|
||||
var stats entity.PhotoStats
|
||||
|
||||
// 总数统计
|
||||
if err := s.db.WithContext(ctx).Model(&models.Photo{}).Count(&stats.Total).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -613,7 +612,7 @@ func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, e
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&models.Photo{}).
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&statusStats).Error; err != nil {
|
||||
@ -627,7 +626,7 @@ func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, e
|
||||
|
||||
// 本月新增
|
||||
startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1)
|
||||
if err := s.db.WithContext(ctx).Model(&models.Photo{}).
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("created_at >= ?", startOfMonth).
|
||||
Count(&stats.ThisMonth).Error; err != nil {
|
||||
return nil, err
|
||||
@ -635,7 +634,7 @@ func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, e
|
||||
|
||||
// 今日新增
|
||||
startOfDay := time.Now().Truncate(24 * time.Hour)
|
||||
if err := s.db.WithContext(ctx).Model(&models.Photo{}).
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("created_at >= ?", startOfDay).
|
||||
Count(&stats.Today).Error; err != nil {
|
||||
return nil, err
|
||||
@ -643,7 +642,7 @@ func (s *PhotoService) GetPhotoStats(ctx context.Context) (*models.PhotoStats, e
|
||||
|
||||
// 总存储大小
|
||||
var totalSize sql.NullInt64
|
||||
if err := s.db.WithContext(ctx).Model(&models.Photo{}).
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Select("SUM(file_size)").
|
||||
Row().Scan(&totalSize); err != nil {
|
||||
return nil, err
|
||||
@ -663,7 +662,7 @@ func (s *PhotoService) isValidImageFile(filename string) bool {
|
||||
}
|
||||
|
||||
// processPhotoFormats 处理照片格式转换
|
||||
func (s *PhotoService) processPhotoFormats(ctx context.Context, photo *models.Photo, uploadedFile *storage.UploadedFile) {
|
||||
func (s *PhotoService) processPhotoFormats(ctx context.Context, photo *entity.Photo, uploadedFile *storage.UploadedFile) {
|
||||
// 这里将实现图片格式转换逻辑
|
||||
// 生成不同尺寸和格式的图片
|
||||
// 更新 photo_formats 表
|
||||
|
||||
Reference in New Issue
Block a user