主要变更: - 采用 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 最佳实践
375 lines
10 KiB
Go
375 lines
10 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
|
||
"photography-backend/internal/model/entity"
|
||
"photography-backend/internal/repository/interfaces"
|
||
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// photoRepositoryImpl 照片仓储实现
|
||
type photoRepositoryImpl struct {
|
||
db *gorm.DB
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewPhotoRepository 创建照片仓储实现
|
||
func NewPhotoRepository(db *gorm.DB, logger *zap.Logger) interfaces.PhotoRepository {
|
||
return &photoRepositoryImpl{
|
||
db: db,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// Create 创建照片
|
||
func (r *photoRepositoryImpl) Create(ctx context.Context, photo *entity.Photo) error {
|
||
return r.db.WithContext(ctx).Create(photo).Error
|
||
}
|
||
|
||
// GetByID 根据ID获取照片
|
||
func (r *photoRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Photo, error) {
|
||
var photo entity.Photo
|
||
err := r.db.WithContext(ctx).
|
||
Preload("User").
|
||
Preload("Categories").
|
||
Preload("Tags").
|
||
First(&photo, id).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("photo not found")
|
||
}
|
||
return nil, err
|
||
}
|
||
return &photo, nil
|
||
}
|
||
|
||
// GetByFilename 根据文件名获取照片
|
||
func (r *photoRepositoryImpl) GetByFilename(ctx context.Context, filename string) (*entity.Photo, error) {
|
||
var photo entity.Photo
|
||
err := r.db.WithContext(ctx).Where("filename = ?", filename).First(&photo).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("photo not found")
|
||
}
|
||
return nil, err
|
||
}
|
||
return &photo, nil
|
||
}
|
||
|
||
// Update 更新照片
|
||
func (r *photoRepositoryImpl) Update(ctx context.Context, photo *entity.Photo) error {
|
||
return r.db.WithContext(ctx).Save(photo).Error
|
||
}
|
||
|
||
// Delete 删除照片
|
||
func (r *photoRepositoryImpl) Delete(ctx context.Context, id uint) error {
|
||
return r.db.WithContext(ctx).Delete(&entity.Photo{}, id).Error
|
||
}
|
||
|
||
// List 获取照片列表
|
||
func (r *photoRepositoryImpl) List(ctx context.Context, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||
var photos []*entity.Photo
|
||
var total int64
|
||
|
||
query := r.db.WithContext(ctx).Model(&entity.Photo{})
|
||
|
||
// 应用过滤条件
|
||
if params.UserID != nil {
|
||
query = query.Where("user_id = ?", *params.UserID)
|
||
}
|
||
|
||
if params.Status != nil {
|
||
query = query.Where("status = ?", *params.Status)
|
||
}
|
||
|
||
if params.CategoryID != nil {
|
||
query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
|
||
Where("photo_categories.category_id = ?", *params.CategoryID)
|
||
}
|
||
|
||
if params.TagID != nil {
|
||
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
||
Where("photo_tags.tag_id = ?", *params.TagID)
|
||
}
|
||
|
||
if params.DateFrom != nil {
|
||
query = query.Where("taken_at >= ?", *params.DateFrom)
|
||
}
|
||
|
||
if params.DateTo != nil {
|
||
query = query.Where("taken_at <= ?", *params.DateTo)
|
||
}
|
||
|
||
if params.Search != "" {
|
||
query = query.Where("title ILIKE ? OR description ILIKE ?",
|
||
"%"+params.Search+"%", "%"+params.Search+"%")
|
||
}
|
||
|
||
// 获取总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 应用排序
|
||
orderBy := "created_at DESC"
|
||
if params.Sort != "" {
|
||
order := "ASC"
|
||
if params.Order == "desc" {
|
||
order = "DESC"
|
||
}
|
||
orderBy = fmt.Sprintf("%s %s", params.Sort, order)
|
||
}
|
||
query = query.Order(orderBy)
|
||
|
||
// 应用分页
|
||
if params.Page > 0 && params.Limit > 0 {
|
||
offset := (params.Page - 1) * params.Limit
|
||
query = query.Offset(offset).Limit(params.Limit)
|
||
}
|
||
|
||
// 预加载关联数据
|
||
query = query.Preload("User").Preload("Categories").Preload("Tags")
|
||
|
||
// 查询数据
|
||
if err := query.Find(&photos).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return photos, total, nil
|
||
}
|
||
|
||
// ListByUserID 根据用户ID获取照片列表
|
||
func (r *photoRepositoryImpl) ListByUserID(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||
if params == nil {
|
||
params = &entity.PhotoListParams{}
|
||
}
|
||
params.UserID = &userID
|
||
return r.List(ctx, params)
|
||
}
|
||
|
||
// ListByStatus 根据状态获取照片列表
|
||
func (r *photoRepositoryImpl) ListByStatus(ctx context.Context, status entity.PhotoStatus, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||
if params == nil {
|
||
params = &entity.PhotoListParams{}
|
||
}
|
||
params.Status = &status
|
||
return r.List(ctx, params)
|
||
}
|
||
|
||
// ListByCategory 根据分类获取照片列表
|
||
func (r *photoRepositoryImpl) ListByCategory(ctx context.Context, categoryID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||
if params == nil {
|
||
params = &entity.PhotoListParams{}
|
||
}
|
||
params.CategoryID = &categoryID
|
||
return r.List(ctx, params)
|
||
}
|
||
|
||
// Search 搜索照片
|
||
func (r *photoRepositoryImpl) Search(ctx context.Context, query string, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||
if params == nil {
|
||
params = &entity.PhotoListParams{}
|
||
}
|
||
params.Search = query
|
||
return r.List(ctx, params)
|
||
}
|
||
|
||
// Count 统计照片总数
|
||
func (r *photoRepositoryImpl) Count(ctx context.Context) (int64, error) {
|
||
var count int64
|
||
err := r.db.WithContext(ctx).Model(&entity.Photo{}).Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// CountByUser 统计用户照片数
|
||
func (r *photoRepositoryImpl) CountByUser(ctx context.Context, userID uint) (int64, error) {
|
||
var count int64
|
||
err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||
Where("user_id = ?", userID).Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// CountByStatus 统计指定状态照片数
|
||
func (r *photoRepositoryImpl) CountByStatus(ctx context.Context, status entity.PhotoStatus) (int64, error) {
|
||
var count int64
|
||
err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||
Where("status = ?", status).Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// CountByCategory 统计分类照片数
|
||
func (r *photoRepositoryImpl) CountByCategory(ctx context.Context, categoryID uint) (int64, error) {
|
||
var count int64
|
||
err := r.db.WithContext(ctx).
|
||
Table("photo_categories").
|
||
Where("category_id = ?", categoryID).
|
||
Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// CountByStatus 统计指定状态照片数
|
||
func (r *photoRepositoryImpl) CountByStatus(ctx context.Context, status string) (int64, error) {
|
||
var count int64
|
||
err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||
Where("status = ?", status).Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
|
||
// BatchUpdate 批量更新
|
||
func (r *photoRepositoryImpl) BatchUpdate(ctx context.Context, ids []uint, updates map[string]interface{}) error {
|
||
if len(ids) == 0 || len(updates) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||
Where("id IN ?", ids).
|
||
Updates(updates).Error
|
||
}
|
||
|
||
// BatchUpdateCategories 批量更新分类
|
||
func (r *photoRepositoryImpl) BatchUpdateCategories(ctx context.Context, photoIDs []uint, categoryIDs []uint) error {
|
||
if len(photoIDs) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
// 删除现有关联
|
||
if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", photoIDs).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 添加新关联
|
||
for _, photoID := range photoIDs {
|
||
for _, categoryID := range categoryIDs {
|
||
if err := tx.Exec("INSERT INTO photo_categories (photo_id, category_id) VALUES (?, ?)",
|
||
photoID, categoryID).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// BatchUpdateTags 批量更新标签
|
||
func (r *photoRepositoryImpl) BatchUpdateTags(ctx context.Context, photoIDs []uint, tagIDs []uint) error {
|
||
if len(photoIDs) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
// 删除现有关联
|
||
if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", photoIDs).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 添加新关联
|
||
for _, photoID := range photoIDs {
|
||
for _, tagID := range tagIDs {
|
||
if err := tx.Exec("INSERT INTO photo_tags (photo_id, tag_id) VALUES (?, ?)",
|
||
photoID, tagID).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// BatchDelete 批量删除
|
||
func (r *photoRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error {
|
||
if len(ids) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
// 删除关联关系
|
||
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
|
||
}
|
||
|
||
// 删除照片记录
|
||
return tx.Delete(&entity.Photo{}, ids).Error
|
||
})
|
||
}
|
||
|
||
// GetStats 获取照片统计信息
|
||
func (r *photoRepositoryImpl) GetStats(ctx context.Context) (*entity.PhotoStats, error) {
|
||
var stats entity.PhotoStats
|
||
|
||
// 总照片数
|
||
if total, err := r.Count(ctx); err != nil {
|
||
return nil, err
|
||
} else {
|
||
stats.Total = total
|
||
}
|
||
|
||
// 按状态统计
|
||
for _, status := range []entity.PhotoStatus{
|
||
entity.PhotoStatusActive,
|
||
entity.PhotoStatusDraft,
|
||
entity.PhotoStatusArchived,
|
||
} {
|
||
if count, err := r.CountByStatus(ctx, status); err != nil {
|
||
return nil, err
|
||
} else {
|
||
switch status {
|
||
case entity.PhotoStatusActive:
|
||
stats.Published = count
|
||
case entity.PhotoStatusDraft:
|
||
stats.Draft = count
|
||
case entity.PhotoStatusArchived:
|
||
stats.Archived = count
|
||
}
|
||
}
|
||
}
|
||
|
||
// 本月新增照片数
|
||
now := time.Now()
|
||
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||
|
||
var monthlyCount int64
|
||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||
Where("created_at >= ? AND created_at <= ?", startOfMonth, endOfMonth).
|
||
Count(&monthlyCount).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
stats.ThisMonth = monthlyCount
|
||
|
||
// 用户照片分布(Top 10)
|
||
var userPhotoStats []struct {
|
||
UserID uint `json:"user_id"`
|
||
Username string `json:"username"`
|
||
PhotoCount int64 `json:"photo_count"`
|
||
}
|
||
|
||
if err := r.db.WithContext(ctx).
|
||
Table("photos").
|
||
Select("photos.user_id, users.username, COUNT(photos.id) as photo_count").
|
||
Joins("LEFT JOIN users ON photos.user_id = users.id").
|
||
Group("photos.user_id, users.username").
|
||
Order("photo_count DESC").
|
||
Limit(10).
|
||
Find(&userPhotoStats).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
stats.UserPhotoCounts = make(map[string]int64)
|
||
for _, stat := range userPhotoStats {
|
||
stats.UserPhotoCounts[stat.Username] = stat.PhotoCount
|
||
}
|
||
|
||
return &stats, nil
|
||
} |