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:
xujiang
2025-07-10 15:05:52 +08:00
parent a2f2f66f88
commit 39a42695d3
52 changed files with 6047 additions and 2349 deletions

View File

@ -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 表