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:
@ -0,0 +1,39 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CategoryRepository 分类仓储接口
|
||||
type CategoryRepository interface {
|
||||
// 基本CRUD操作
|
||||
Create(ctx context.Context, category *entity.Category) error
|
||||
GetByID(ctx context.Context, id uint) (*entity.Category, error)
|
||||
GetBySlug(ctx context.Context, slug string) (*entity.Category, error)
|
||||
Update(ctx context.Context, category *entity.Category) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
|
||||
// 查询操作
|
||||
List(ctx context.Context, parentID *uint) ([]*entity.Category, error)
|
||||
GetTree(ctx context.Context) ([]*entity.CategoryTree, error)
|
||||
GetChildren(ctx context.Context, parentID uint) ([]*entity.Category, error)
|
||||
GetParent(ctx context.Context, categoryID uint) (*entity.Category, error)
|
||||
|
||||
// 排序操作
|
||||
Reorder(ctx context.Context, parentID *uint, categoryIDs []uint) error
|
||||
GetNextSortOrder(ctx context.Context, parentID *uint) (int, error)
|
||||
|
||||
// 验证操作
|
||||
ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error
|
||||
ValidateParentCategory(ctx context.Context, categoryID, parentID uint) error
|
||||
|
||||
// 统计操作
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountActive(ctx context.Context) (int64, error)
|
||||
CountTopLevel(ctx context.Context) (int64, error)
|
||||
GetStats(ctx context.Context) (*entity.CategoryStats, error)
|
||||
|
||||
// 工具方法
|
||||
GenerateUniqueSlug(ctx context.Context, baseName string) (string, error)
|
||||
}
|
||||
33
backend/internal/repository/interfaces/photo_repository.go
Normal file
33
backend/internal/repository/interfaces/photo_repository.go
Normal file
@ -0,0 +1,33 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// PhotoRepository 照片仓储接口
|
||||
type PhotoRepository interface {
|
||||
// 基本CRUD操作
|
||||
Create(ctx context.Context, photo *entity.Photo) error
|
||||
GetByID(ctx context.Context, id uint) (*entity.Photo, error)
|
||||
Update(ctx context.Context, photo *entity.Photo) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
|
||||
// 查询操作
|
||||
List(ctx context.Context, params *entity.PhotoListParams) ([]*entity.Photo, int64, error)
|
||||
ListByUserID(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error)
|
||||
ListByCategory(ctx context.Context, categoryID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error)
|
||||
Search(ctx context.Context, query string, params *entity.PhotoListParams) ([]*entity.Photo, int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchUpdate(ctx context.Context, ids []uint, updates map[string]interface{}) error
|
||||
BatchDelete(ctx context.Context, ids []uint) error
|
||||
BatchUpdateCategories(ctx context.Context, photoIDs []uint, categoryIDs []uint) error
|
||||
BatchUpdateTags(ctx context.Context, photoIDs []uint, tagIDs []uint) error
|
||||
|
||||
// 统计操作
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountByStatus(ctx context.Context, status string) (int64, error)
|
||||
CountByUser(ctx context.Context, userID uint) (int64, error)
|
||||
GetStats(ctx context.Context) (*entity.PhotoStats, error)
|
||||
}
|
||||
42
backend/internal/repository/interfaces/tag_repository.go
Normal file
42
backend/internal/repository/interfaces/tag_repository.go
Normal file
@ -0,0 +1,42 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// TagRepository 标签仓储接口
|
||||
type TagRepository interface {
|
||||
// 基本CRUD操作
|
||||
Create(ctx context.Context, tag *entity.Tag) error
|
||||
GetByID(ctx context.Context, id uint) (*entity.Tag, error)
|
||||
GetBySlug(ctx context.Context, slug string) (*entity.Tag, error)
|
||||
GetByName(ctx context.Context, name string) (*entity.Tag, error)
|
||||
Update(ctx context.Context, tag *entity.Tag) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
|
||||
// 查询操作
|
||||
List(ctx context.Context, params *entity.TagListParams) ([]*entity.Tag, int64, error)
|
||||
Search(ctx context.Context, query string) ([]*entity.Tag, error)
|
||||
GetPopular(ctx context.Context, limit int) ([]*entity.Tag, error)
|
||||
GetByPhotos(ctx context.Context, photoIDs []uint) ([]*entity.Tag, error)
|
||||
|
||||
// 批量操作
|
||||
CreateMultiple(ctx context.Context, tags []*entity.Tag) error
|
||||
GetOrCreateByNames(ctx context.Context, names []string) ([]*entity.Tag, error)
|
||||
BatchDelete(ctx context.Context, ids []uint) error
|
||||
|
||||
// 关联操作
|
||||
AttachToPhoto(ctx context.Context, tagID, photoID uint) error
|
||||
DetachFromPhoto(ctx context.Context, tagID, photoID uint) error
|
||||
GetPhotoTags(ctx context.Context, photoID uint) ([]*entity.Tag, error)
|
||||
|
||||
// 统计操作
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountByPhotos(ctx context.Context) (map[uint]int64, error)
|
||||
GetStats(ctx context.Context) (*entity.TagStats, error)
|
||||
|
||||
// 工具方法
|
||||
GenerateUniqueSlug(ctx context.Context, baseName string) (string, error)
|
||||
ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error
|
||||
}
|
||||
40
backend/internal/repository/interfaces/user_repository.go
Normal file
40
backend/internal/repository/interfaces/user_repository.go
Normal file
@ -0,0 +1,40 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储接口
|
||||
type UserRepository interface {
|
||||
// 基本CRUD操作
|
||||
Create(ctx context.Context, user *entity.User) error
|
||||
GetByID(ctx context.Context, id uint) (*entity.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*entity.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*entity.User, error)
|
||||
Update(ctx context.Context, user *entity.User) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
|
||||
// 查询操作
|
||||
List(ctx context.Context, params *entity.UserListParams) ([]*entity.User, int64, error)
|
||||
Search(ctx context.Context, query string, params *entity.UserListParams) ([]*entity.User, int64, error)
|
||||
|
||||
// 认证相关
|
||||
UpdatePassword(ctx context.Context, userID uint, hashedPassword string) error
|
||||
UpdateLastLogin(ctx context.Context, userID uint) error
|
||||
IncrementLoginCount(ctx context.Context, userID uint) error
|
||||
|
||||
// 状态管理
|
||||
SetActive(ctx context.Context, userID uint, isActive bool) error
|
||||
VerifyEmail(ctx context.Context, userID uint) error
|
||||
|
||||
// 统计操作
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountByRole(ctx context.Context, role entity.UserRole) (int64, error)
|
||||
CountActive(ctx context.Context) (int64, error)
|
||||
GetStats(ctx context.Context) (*entity.UserStats, error)
|
||||
|
||||
// 验证操作
|
||||
ExistsByEmail(ctx context.Context, email string) (bool, error)
|
||||
ExistsByUsername(ctx context.Context, username string) (bool, error)
|
||||
}
|
||||
@ -1,213 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"photography-backend/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CategoryRepository 分类仓库接口
|
||||
type CategoryRepository interface {
|
||||
Create(category *models.Category) error
|
||||
GetByID(id uint) (*models.Category, error)
|
||||
Update(category *models.Category) error
|
||||
Delete(id uint) error
|
||||
List(params *models.CategoryListParams) ([]*models.Category, error)
|
||||
GetTree() ([]*models.Category, error)
|
||||
GetChildren(parentID uint) ([]*models.Category, error)
|
||||
GetStats() (*models.CategoryStats, error)
|
||||
UpdateSort(id uint, sort int) error
|
||||
GetPhotoCount(id uint) (int64, error)
|
||||
}
|
||||
|
||||
// categoryRepository 分类仓库实现
|
||||
type categoryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCategoryRepository 创建分类仓库
|
||||
func NewCategoryRepository(db *gorm.DB) CategoryRepository {
|
||||
return &categoryRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建分类
|
||||
func (r *categoryRepository) Create(category *models.Category) error {
|
||||
if err := r.db.Create(category).Error; err != nil {
|
||||
return fmt.Errorf("failed to create category: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取分类
|
||||
func (r *categoryRepository) GetByID(id uint) (*models.Category, error) {
|
||||
var category models.Category
|
||||
if err := r.db.Preload("Parent").Preload("Children").
|
||||
First(&category, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get category by id: %w", err)
|
||||
}
|
||||
|
||||
// 计算照片数量
|
||||
var photoCount int64
|
||||
if err := r.db.Model(&models.Photo{}).Where("category_id = ?", id).
|
||||
Count(&photoCount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count photos: %w", err)
|
||||
}
|
||||
category.PhotoCount = int(photoCount)
|
||||
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// Update 更新分类
|
||||
func (r *categoryRepository) Update(category *models.Category) error {
|
||||
if err := r.db.Save(category).Error; err != nil {
|
||||
return fmt.Errorf("failed to update category: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除分类
|
||||
func (r *categoryRepository) Delete(id uint) error {
|
||||
// 开启事务
|
||||
tx := r.db.Begin()
|
||||
|
||||
// 将子分类的父分类设置为NULL
|
||||
if err := tx.Model(&models.Category{}).Where("parent_id = ?", id).
|
||||
Update("parent_id", nil).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to update child categories: %w", err)
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
if err := tx.Delete(&models.Category{}, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete category: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// List 获取分类列表
|
||||
func (r *categoryRepository) List(params *models.CategoryListParams) ([]*models.Category, error) {
|
||||
var categories []*models.Category
|
||||
|
||||
query := r.db.Model(&models.Category{})
|
||||
|
||||
// 添加过滤条件
|
||||
if params.ParentID > 0 {
|
||||
query = query.Where("parent_id = ?", params.ParentID)
|
||||
}
|
||||
|
||||
if params.IsActive {
|
||||
query = query.Where("is_active = ?", true)
|
||||
}
|
||||
|
||||
if err := query.Order("sort ASC, created_at DESC").
|
||||
Find(&categories).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to list categories: %w", err)
|
||||
}
|
||||
|
||||
// 如果需要包含统计信息
|
||||
if params.IncludeStats {
|
||||
for _, category := range categories {
|
||||
var photoCount int64
|
||||
if err := r.db.Model(&models.Photo{}).Where("category_id = ?", category.ID).
|
||||
Count(&photoCount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count photos for category %d: %w", category.ID, err)
|
||||
}
|
||||
category.PhotoCount = int(photoCount)
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetTree 获取分类树
|
||||
func (r *categoryRepository) GetTree() ([]*models.Category, error) {
|
||||
var categories []*models.Category
|
||||
|
||||
// 获取所有分类
|
||||
if err := r.db.Where("is_active = ?", true).
|
||||
Order("sort ASC, created_at DESC").
|
||||
Find(&categories).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get categories: %w", err)
|
||||
}
|
||||
|
||||
// 构建分类树
|
||||
categoryMap := make(map[uint]*models.Category)
|
||||
var rootCategories []*models.Category
|
||||
|
||||
// 第一次遍历:建立映射
|
||||
for _, category := range categories {
|
||||
categoryMap[category.ID] = category
|
||||
category.Children = []models.Category{}
|
||||
}
|
||||
|
||||
// 第二次遍历:构建树形结构
|
||||
for _, category := range categories {
|
||||
if category.ParentID == nil {
|
||||
rootCategories = append(rootCategories, category)
|
||||
} else {
|
||||
if parent, exists := categoryMap[*category.ParentID]; exists {
|
||||
parent.Children = append(parent.Children, *category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootCategories, nil
|
||||
}
|
||||
|
||||
// GetChildren 获取子分类
|
||||
func (r *categoryRepository) GetChildren(parentID uint) ([]*models.Category, error) {
|
||||
var categories []*models.Category
|
||||
|
||||
if err := r.db.Where("parent_id = ? AND is_active = ?", parentID, true).
|
||||
Order("sort ASC, created_at DESC").
|
||||
Find(&categories).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get child categories: %w", err)
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetStats 获取分类统计
|
||||
func (r *categoryRepository) GetStats() (*models.CategoryStats, error) {
|
||||
var stats models.CategoryStats
|
||||
|
||||
// 总分类数
|
||||
var totalCount int64
|
||||
if err := r.db.Model(&models.Category{}).Count(&totalCount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count total categories: %w", err)
|
||||
}
|
||||
stats.TotalCategories = int(totalCount)
|
||||
|
||||
// 计算最大层级
|
||||
// 这里简化处理,实际应用中可能需要递归查询
|
||||
stats.MaxLevel = 3
|
||||
|
||||
// 特色分类数量(这里假设有一个is_featured字段,实际可能需要调整)
|
||||
stats.FeaturedCount = 0
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// UpdateSort 更新排序
|
||||
func (r *categoryRepository) UpdateSort(id uint, sort int) error {
|
||||
if err := r.db.Model(&models.Category{}).Where("id = ?", id).
|
||||
Update("sort", sort).Error; err != nil {
|
||||
return fmt.Errorf("failed to update sort: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPhotoCount 获取分类的照片数量
|
||||
func (r *categoryRepository) GetPhotoCount(id uint) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.Photo{}).Where("category_id = ?", id).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, fmt.Errorf("failed to count photos: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
345
backend/internal/repository/postgres/category_repository_impl.go
Normal file
345
backend/internal/repository/postgres/category_repository_impl.go
Normal file
@ -0,0 +1,345 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
"photography-backend/internal/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// categoryRepositoryImpl 分类仓储实现
|
||||
type categoryRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCategoryRepository 创建分类仓储实现
|
||||
func NewCategoryRepository(db *gorm.DB, logger *zap.Logger) interfaces.CategoryRepository {
|
||||
return &categoryRepositoryImpl{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建分类
|
||||
func (r *categoryRepositoryImpl) Create(ctx context.Context, category *entity.Category) error {
|
||||
return r.db.WithContext(ctx).Create(category).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取分类
|
||||
func (r *categoryRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Category, error) {
|
||||
var category entity.Category
|
||||
err := r.db.WithContext(ctx).First(&category, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("category not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// GetBySlug 根据slug获取分类
|
||||
func (r *categoryRepositoryImpl) GetBySlug(ctx context.Context, slug string) (*entity.Category, error) {
|
||||
var category entity.Category
|
||||
err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&category).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("category not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// Update 更新分类
|
||||
func (r *categoryRepositoryImpl) Update(ctx context.Context, category *entity.Category) error {
|
||||
return r.db.WithContext(ctx).Save(category).Error
|
||||
}
|
||||
|
||||
// Delete 删除分类
|
||||
func (r *categoryRepositoryImpl) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&entity.Category{}, id).Error
|
||||
}
|
||||
|
||||
// List 获取分类列表
|
||||
func (r *categoryRepositoryImpl) List(ctx context.Context, parentID *uint) ([]*entity.Category, error) {
|
||||
var categories []*entity.Category
|
||||
|
||||
query := r.db.WithContext(ctx).Order("sort_order ASC, created_at ASC")
|
||||
|
||||
if parentID != nil {
|
||||
query = query.Where("parent_id = ?", *parentID)
|
||||
} else {
|
||||
query = query.Where("parent_id IS NULL")
|
||||
}
|
||||
|
||||
err := query.Find(&categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
// GetTree 获取分类树
|
||||
func (r *categoryRepositoryImpl) GetTree(ctx context.Context) ([]*entity.CategoryTree, error) {
|
||||
var categories []*entity.Category
|
||||
if err := r.db.WithContext(ctx).
|
||||
Order("sort_order ASC, created_at ASC").
|
||||
Find(&categories).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
tree := r.buildCategoryTree(categories, nil)
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// GetChildren 获取子分类
|
||||
func (r *categoryRepositoryImpl) GetChildren(ctx context.Context, parentID uint) ([]*entity.Category, error) {
|
||||
var children []*entity.Category
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("parent_id = ?", parentID).
|
||||
Order("sort_order ASC").
|
||||
Find(&children).Error
|
||||
return children, err
|
||||
}
|
||||
|
||||
// GetParent 获取父分类
|
||||
func (r *categoryRepositoryImpl) GetParent(ctx context.Context, categoryID uint) (*entity.Category, error) {
|
||||
var category entity.Category
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Parent").
|
||||
First(&category, categoryID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return category.Parent, nil
|
||||
}
|
||||
|
||||
// Reorder 重新排序分类
|
||||
func (r *categoryRepositoryImpl) Reorder(ctx context.Context, parentID *uint, categoryIDs []uint) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i, categoryID := range categoryIDs {
|
||||
if err := tx.Model(&entity.Category{}).
|
||||
Where("id = ?", categoryID).
|
||||
Update("sort_order", i+1).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetNextSortOrder 获取下一个排序顺序
|
||||
func (r *categoryRepositoryImpl) GetNextSortOrder(ctx context.Context, parentID *uint) (int, error) {
|
||||
var maxOrder int
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entity.Category{}).Select("COALESCE(MAX(sort_order), 0)")
|
||||
|
||||
if parentID != nil {
|
||||
query = query.Where("parent_id = ?", *parentID)
|
||||
} else {
|
||||
query = query.Where("parent_id IS NULL")
|
||||
}
|
||||
|
||||
err := query.Row().Scan(&maxOrder)
|
||||
return maxOrder + 1, err
|
||||
}
|
||||
|
||||
// ValidateSlugUnique 验证slug唯一性
|
||||
func (r *categoryRepositoryImpl) ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.Category{}).Where("slug = ?", slug)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("slug already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateParentCategory 验证父分类(防止循环引用)
|
||||
func (r *categoryRepositoryImpl) ValidateParentCategory(ctx context.Context, categoryID, parentID uint) error {
|
||||
if categoryID == parentID {
|
||||
return errors.New("category cannot be its own parent")
|
||||
}
|
||||
|
||||
// 检查父分类是否存在
|
||||
var parent entity.Category
|
||||
if err := r.db.WithContext(ctx).First(&parent, parentID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("parent category not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否会形成循环引用
|
||||
current := parentID
|
||||
for current != 0 {
|
||||
var category entity.Category
|
||||
if err := r.db.WithContext(ctx).First(&category, current).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("parent category not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if category.ParentID == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if *category.ParentID == categoryID {
|
||||
return errors.New("circular reference detected")
|
||||
}
|
||||
|
||||
current = *category.ParentID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count 统计分类总数
|
||||
func (r *categoryRepositoryImpl) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.Category{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountActive 统计活跃分类数
|
||||
func (r *categoryRepositoryImpl) CountActive(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.Category{}).
|
||||
Where("is_active = ?", true).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountTopLevel 统计顶级分类数
|
||||
func (r *categoryRepositoryImpl) CountTopLevel(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.Category{}).
|
||||
Where("parent_id IS NULL").Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetStats 获取分类统计信息
|
||||
func (r *categoryRepositoryImpl) GetStats(ctx context.Context) (*entity.CategoryStats, error) {
|
||||
var stats entity.CategoryStats
|
||||
|
||||
// 总分类数
|
||||
if total, err := r.Count(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.Total = total
|
||||
}
|
||||
|
||||
// 活跃分类数
|
||||
if active, err := r.CountActive(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.Active = active
|
||||
}
|
||||
|
||||
// 顶级分类数
|
||||
if topLevel, err := r.CountTopLevel(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.TopLevel = topLevel
|
||||
}
|
||||
|
||||
// 各分类照片数量
|
||||
var categoryPhotoStats []struct {
|
||||
CategoryID uint `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("categories").
|
||||
Select("categories.id as category_id, categories.name, COUNT(photo_categories.photo_id) as photo_count").
|
||||
Joins("LEFT JOIN photo_categories ON categories.id = photo_categories.category_id").
|
||||
Group("categories.id, categories.name").
|
||||
Order("photo_count DESC").
|
||||
Limit(10).
|
||||
Find(&categoryPhotoStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats.PhotoCounts = make(map[string]int64)
|
||||
for _, stat := range categoryPhotoStats {
|
||||
stats.PhotoCounts[stat.Name] = stat.PhotoCount
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GenerateUniqueSlug 生成唯一slug
|
||||
func (r *categoryRepositoryImpl) GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) {
|
||||
baseSlug := utils.GenerateSlug(baseName)
|
||||
slug := baseSlug
|
||||
|
||||
counter := 1
|
||||
for {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Category{}).
|
||||
Where("slug = ?", slug).Count(&count).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
|
||||
counter++
|
||||
}
|
||||
|
||||
return slug, nil
|
||||
}
|
||||
|
||||
// buildCategoryTree 构建分类树
|
||||
func (r *categoryRepositoryImpl) buildCategoryTree(categories []*entity.Category, parentID *uint) []*entity.CategoryTree {
|
||||
var tree []*entity.CategoryTree
|
||||
|
||||
for _, category := range categories {
|
||||
// 检查是否匹配父分类
|
||||
if (parentID == nil && category.ParentID == nil) ||
|
||||
(parentID != nil && category.ParentID != nil && *category.ParentID == *parentID) {
|
||||
|
||||
node := &entity.CategoryTree{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Slug: category.Slug,
|
||||
Description: category.Description,
|
||||
ParentID: category.ParentID,
|
||||
SortOrder: category.SortOrder,
|
||||
IsActive: category.IsActive,
|
||||
PhotoCount: category.PhotoCount,
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
|
||||
// 递归构建子分类
|
||||
children := r.buildCategoryTree(categories, &category.ID)
|
||||
node.Children = make([]entity.CategoryTree, len(children))
|
||||
for i, child := range children {
|
||||
node.Children[i] = *child
|
||||
}
|
||||
|
||||
tree = append(tree, node)
|
||||
}
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/driver/postgres"
|
||||
"photography-backend/internal/config"
|
||||
"photography-backend/internal/models"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// Database 数据库连接
|
||||
@ -52,10 +52,10 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) {
|
||||
// AutoMigrate 自动迁移数据库表结构
|
||||
func (d *Database) AutoMigrate() error {
|
||||
return d.DB.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Category{},
|
||||
&models.Tag{},
|
||||
&models.Photo{},
|
||||
&entity.User{},
|
||||
&entity.Category{},
|
||||
&entity.Tag{},
|
||||
&entity.Photo{},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,303 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"photography-backend/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PhotoRepository 照片仓库接口
|
||||
type PhotoRepository interface {
|
||||
Create(photo *models.Photo) error
|
||||
GetByID(id uint) (*models.Photo, error)
|
||||
Update(photo *models.Photo) error
|
||||
Delete(id uint) error
|
||||
List(params *models.PhotoListParams) ([]*models.Photo, int64, error)
|
||||
GetByCategory(categoryID uint, page, limit int) ([]*models.Photo, int64, error)
|
||||
GetByTag(tagID uint, page, limit int) ([]*models.Photo, int64, error)
|
||||
GetByUser(userID uint, page, limit int) ([]*models.Photo, int64, error)
|
||||
Search(query string, page, limit int) ([]*models.Photo, int64, error)
|
||||
IncrementViewCount(id uint) error
|
||||
IncrementLikeCount(id uint) error
|
||||
UpdateStatus(id uint, status string) error
|
||||
GetStats() (*PhotoStats, error)
|
||||
}
|
||||
|
||||
// PhotoStats 照片统计
|
||||
type PhotoStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Published int64 `json:"published"`
|
||||
Draft int64 `json:"draft"`
|
||||
Archived int64 `json:"archived"`
|
||||
}
|
||||
|
||||
// photoRepository 照片仓库实现
|
||||
type photoRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewPhotoRepository 创建照片仓库
|
||||
func NewPhotoRepository(db *gorm.DB) PhotoRepository {
|
||||
return &photoRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建照片
|
||||
func (r *photoRepository) Create(photo *models.Photo) error {
|
||||
if err := r.db.Create(photo).Error; err != nil {
|
||||
return fmt.Errorf("failed to create photo: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取照片
|
||||
func (r *photoRepository) GetByID(id uint) (*models.Photo, error) {
|
||||
var photo models.Photo
|
||||
if err := r.db.Preload("Category").Preload("Tags").Preload("User").
|
||||
First(&photo, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get photo by id: %w", err)
|
||||
}
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
// Update 更新照片
|
||||
func (r *photoRepository) Update(photo *models.Photo) error {
|
||||
if err := r.db.Save(photo).Error; err != nil {
|
||||
return fmt.Errorf("failed to update photo: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除照片
|
||||
func (r *photoRepository) Delete(id uint) error {
|
||||
if err := r.db.Delete(&models.Photo{}, id).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete photo: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取照片列表
|
||||
func (r *photoRepository) List(params *models.PhotoListParams) ([]*models.Photo, int64, error) {
|
||||
var photos []*models.Photo
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Photo{}).
|
||||
Preload("Category").
|
||||
Preload("Tags").
|
||||
Preload("User")
|
||||
|
||||
// 添加过滤条件
|
||||
if params.CategoryID > 0 {
|
||||
query = query.Where("category_id = ?", params.CategoryID)
|
||||
}
|
||||
|
||||
if params.TagID > 0 {
|
||||
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
||||
Where("photo_tags.tag_id = ?", params.TagID)
|
||||
}
|
||||
|
||||
if params.UserID > 0 {
|
||||
query = query.Where("user_id = ?", params.UserID)
|
||||
}
|
||||
|
||||
if params.Status != "" {
|
||||
query = query.Where("status = ?", params.Status)
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
query = query.Where("title ILIKE ? OR description ILIKE ?",
|
||||
"%"+params.Search+"%", "%"+params.Search+"%")
|
||||
}
|
||||
|
||||
if params.Year > 0 {
|
||||
query = query.Where("EXTRACT(YEAR FROM taken_at) = ?", params.Year)
|
||||
}
|
||||
|
||||
if params.Month > 0 {
|
||||
query = query.Where("EXTRACT(MONTH FROM taken_at) = ?", params.Month)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count photos: %w", err)
|
||||
}
|
||||
|
||||
// 排序
|
||||
orderClause := fmt.Sprintf("%s %s", params.SortBy, params.SortOrder)
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
if err := query.Offset(offset).Limit(params.Limit).
|
||||
Order(orderClause).
|
||||
Find(&photos).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list photos: %w", err)
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// GetByCategory 根据分类获取照片
|
||||
func (r *photoRepository) GetByCategory(categoryID uint, page, limit int) ([]*models.Photo, int64, error) {
|
||||
var photos []*models.Photo
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Photo{}).
|
||||
Where("category_id = ? AND is_public = ?", categoryID, true).
|
||||
Preload("Category").
|
||||
Preload("Tags")
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count photos by category: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).
|
||||
Order("created_at DESC").
|
||||
Find(&photos).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get photos by category: %w", err)
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// GetByTag 根据标签获取照片
|
||||
func (r *photoRepository) GetByTag(tagID uint, page, limit int) ([]*models.Photo, int64, error) {
|
||||
var photos []*models.Photo
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Photo{}).
|
||||
Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
||||
Where("photo_tags.tag_id = ? AND photos.is_public = ?", tagID, true).
|
||||
Preload("Category").
|
||||
Preload("Tags")
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count photos by tag: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).
|
||||
Order("photos.created_at DESC").
|
||||
Find(&photos).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get photos by tag: %w", err)
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// GetByUser 根据用户获取照片
|
||||
func (r *photoRepository) GetByUser(userID uint, page, limit int) ([]*models.Photo, int64, error) {
|
||||
var photos []*models.Photo
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Photo{}).
|
||||
Where("user_id = ?", userID).
|
||||
Preload("Category").
|
||||
Preload("Tags")
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count photos by user: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).
|
||||
Order("created_at DESC").
|
||||
Find(&photos).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get photos by user: %w", err)
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// Search 搜索照片
|
||||
func (r *photoRepository) Search(query string, page, limit int) ([]*models.Photo, int64, error) {
|
||||
var photos []*models.Photo
|
||||
var total int64
|
||||
|
||||
searchQuery := r.db.Model(&models.Photo{}).
|
||||
Where("title ILIKE ? OR description ILIKE ? OR location ILIKE ?",
|
||||
"%"+query+"%", "%"+query+"%", "%"+query+"%").
|
||||
Where("is_public = ?", true).
|
||||
Preload("Category").
|
||||
Preload("Tags")
|
||||
|
||||
// 计算总数
|
||||
if err := searchQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count search results: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := searchQuery.Offset(offset).Limit(limit).
|
||||
Order("created_at DESC").
|
||||
Find(&photos).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to search photos: %w", err)
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加浏览次数
|
||||
func (r *photoRepository) IncrementViewCount(id uint) error {
|
||||
if err := r.db.Model(&models.Photo{}).Where("id = ?", id).
|
||||
Update("view_count", gorm.Expr("view_count + 1")).Error; err != nil {
|
||||
return fmt.Errorf("failed to increment view count: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementLikeCount 增加点赞次数
|
||||
func (r *photoRepository) IncrementLikeCount(id uint) error {
|
||||
if err := r.db.Model(&models.Photo{}).Where("id = ?", id).
|
||||
Update("like_count", gorm.Expr("like_count + 1")).Error; err != nil {
|
||||
return fmt.Errorf("failed to increment like count: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatus 更新状态
|
||||
func (r *photoRepository) UpdateStatus(id uint, status string) error {
|
||||
if err := r.db.Model(&models.Photo{}).Where("id = ?", id).
|
||||
Update("status", status).Error; err != nil {
|
||||
return fmt.Errorf("failed to update status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取照片统计
|
||||
func (r *photoRepository) GetStats() (*PhotoStats, error) {
|
||||
var stats PhotoStats
|
||||
|
||||
// 总数
|
||||
if err := r.db.Model(&models.Photo{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count total photos: %w", err)
|
||||
}
|
||||
|
||||
// 已发布
|
||||
if err := r.db.Model(&models.Photo{}).Where("status = ?", models.StatusPublished).
|
||||
Count(&stats.Published).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count published photos: %w", err)
|
||||
}
|
||||
|
||||
// 草稿
|
||||
if err := r.db.Model(&models.Photo{}).Where("status = ?", models.StatusDraft).
|
||||
Count(&stats.Draft).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count draft photos: %w", err)
|
||||
}
|
||||
|
||||
// 已归档
|
||||
if err := r.db.Model(&models.Photo{}).Where("status = ?", models.StatusArchived).
|
||||
Count(&stats.Archived).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count archived photos: %w", err)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
375
backend/internal/repository/postgres/photo_repository_impl.go
Normal file
375
backend/internal/repository/postgres/photo_repository_impl.go
Normal file
@ -0,0 +1,375 @@
|
||||
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
|
||||
}
|
||||
@ -1,217 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"photography-backend/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TagRepository 标签仓库接口
|
||||
type TagRepository interface {
|
||||
Create(tag *models.Tag) error
|
||||
GetByID(id uint) (*models.Tag, error)
|
||||
GetByName(name string) (*models.Tag, error)
|
||||
Update(tag *models.Tag) error
|
||||
Delete(id uint) error
|
||||
List(params *models.TagListParams) ([]*models.Tag, int64, error)
|
||||
Search(query string, limit int) ([]*models.Tag, error)
|
||||
GetPopular(limit int) ([]*models.Tag, error)
|
||||
GetOrCreate(name string) (*models.Tag, error)
|
||||
IncrementUseCount(id uint) error
|
||||
DecrementUseCount(id uint) error
|
||||
GetCloud(minUsage int, maxTags int) ([]*models.Tag, error)
|
||||
}
|
||||
|
||||
// tagRepository 标签仓库实现
|
||||
type tagRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTagRepository 创建标签仓库
|
||||
func NewTagRepository(db *gorm.DB) TagRepository {
|
||||
return &tagRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建标签
|
||||
func (r *tagRepository) Create(tag *models.Tag) error {
|
||||
if err := r.db.Create(tag).Error; err != nil {
|
||||
return fmt.Errorf("failed to create tag: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取标签
|
||||
func (r *tagRepository) GetByID(id uint) (*models.Tag, error) {
|
||||
var tag models.Tag
|
||||
if err := r.db.First(&tag, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get tag by id: %w", err)
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取标签
|
||||
func (r *tagRepository) GetByName(name string) (*models.Tag, error) {
|
||||
var tag models.Tag
|
||||
if err := r.db.Where("name = ?", name).First(&tag).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get tag by name: %w", err)
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// Update 更新标签
|
||||
func (r *tagRepository) Update(tag *models.Tag) error {
|
||||
if err := r.db.Save(tag).Error; err != nil {
|
||||
return fmt.Errorf("failed to update tag: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除标签
|
||||
func (r *tagRepository) Delete(id uint) error {
|
||||
// 开启事务
|
||||
tx := r.db.Begin()
|
||||
|
||||
// 删除照片标签关联
|
||||
if err := tx.Exec("DELETE FROM photo_tags WHERE tag_id = ?", id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete photo tag relations: %w", err)
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
if err := tx.Delete(&models.Tag{}, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete tag: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// List 获取标签列表
|
||||
func (r *tagRepository) List(params *models.TagListParams) ([]*models.Tag, int64, error) {
|
||||
var tags []*models.Tag
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Tag{})
|
||||
|
||||
// 添加过滤条件
|
||||
if params.Search != "" {
|
||||
query = query.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
|
||||
if params.IsActive {
|
||||
query = query.Where("is_active = ?", true)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count tags: %w", err)
|
||||
}
|
||||
|
||||
// 排序
|
||||
orderClause := fmt.Sprintf("%s %s", params.SortBy, params.SortOrder)
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
if err := query.Offset(offset).Limit(params.Limit).
|
||||
Order(orderClause).
|
||||
Find(&tags).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list tags: %w", err)
|
||||
}
|
||||
|
||||
return tags, total, nil
|
||||
}
|
||||
|
||||
// Search 搜索标签
|
||||
func (r *tagRepository) Search(query string, limit int) ([]*models.Tag, error) {
|
||||
var tags []*models.Tag
|
||||
|
||||
if err := r.db.Where("name ILIKE ? AND is_active = ?", "%"+query+"%", true).
|
||||
Order("use_count DESC").
|
||||
Limit(limit).
|
||||
Find(&tags).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to search tags: %w", err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetPopular 获取热门标签
|
||||
func (r *tagRepository) GetPopular(limit int) ([]*models.Tag, error) {
|
||||
var tags []*models.Tag
|
||||
|
||||
if err := r.db.Where("is_active = ?", true).
|
||||
Order("use_count DESC").
|
||||
Limit(limit).
|
||||
Find(&tags).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get popular tags: %w", err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetOrCreate 获取或创建标签
|
||||
func (r *tagRepository) GetOrCreate(name string) (*models.Tag, error) {
|
||||
var tag models.Tag
|
||||
|
||||
// 先尝试获取
|
||||
if err := r.db.Where("name = ?", name).First(&tag).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 不存在则创建
|
||||
tag = models.Tag{
|
||||
Name: name,
|
||||
UseCount: 0,
|
||||
IsActive: true,
|
||||
}
|
||||
if err := r.db.Create(&tag).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to create tag: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to get tag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// IncrementUseCount 增加使用次数
|
||||
func (r *tagRepository) IncrementUseCount(id uint) error {
|
||||
if err := r.db.Model(&models.Tag{}).Where("id = ?", id).
|
||||
Update("use_count", gorm.Expr("use_count + 1")).Error; err != nil {
|
||||
return fmt.Errorf("failed to increment use count: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecrementUseCount 减少使用次数
|
||||
func (r *tagRepository) DecrementUseCount(id uint) error {
|
||||
if err := r.db.Model(&models.Tag{}).Where("id = ?", id).
|
||||
Update("use_count", gorm.Expr("GREATEST(use_count - 1, 0)")).Error; err != nil {
|
||||
return fmt.Errorf("failed to decrement use count: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCloud 获取标签云数据
|
||||
func (r *tagRepository) GetCloud(minUsage int, maxTags int) ([]*models.Tag, error) {
|
||||
var tags []*models.Tag
|
||||
|
||||
query := r.db.Where("is_active = ?", true)
|
||||
|
||||
if minUsage > 0 {
|
||||
query = query.Where("use_count >= ?", minUsage)
|
||||
}
|
||||
|
||||
if err := query.Order("use_count DESC").
|
||||
Limit(maxTags).
|
||||
Find(&tags).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get tag cloud: %w", err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
468
backend/internal/repository/postgres/tag_repository_impl.go
Normal file
468
backend/internal/repository/postgres/tag_repository_impl.go
Normal file
@ -0,0 +1,468 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
"photography-backend/internal/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// tagRepositoryImpl 标签仓储实现
|
||||
type tagRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTagRepository 创建标签仓储实现
|
||||
func NewTagRepository(db *gorm.DB, logger *zap.Logger) interfaces.TagRepository {
|
||||
return &tagRepositoryImpl{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建标签
|
||||
func (r *tagRepositoryImpl) Create(ctx context.Context, tag *entity.Tag) error {
|
||||
return r.db.WithContext(ctx).Create(tag).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取标签
|
||||
func (r *tagRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.WithContext(ctx).First(&tag, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// GetBySlug 根据slug获取标签
|
||||
func (r *tagRepositoryImpl) GetBySlug(ctx context.Context, slug string) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&tag).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取标签
|
||||
func (r *tagRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// Update 更新标签
|
||||
func (r *tagRepositoryImpl) Update(ctx context.Context, tag *entity.Tag) error {
|
||||
return r.db.WithContext(ctx).Save(tag).Error
|
||||
}
|
||||
|
||||
// Delete 删除标签
|
||||
func (r *tagRepositoryImpl) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 删除照片标签关联
|
||||
if err := tx.Exec("DELETE FROM photo_tags WHERE tag_id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
return tx.Delete(&entity.Tag{}, id).Error
|
||||
})
|
||||
}
|
||||
|
||||
// List 获取标签列表
|
||||
func (r *tagRepositoryImpl) List(ctx context.Context, params *entity.TagListParams) ([]*entity.Tag, int64, error) {
|
||||
var tags []*entity.Tag
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entity.Tag{})
|
||||
|
||||
// 应用过滤条件
|
||||
if params.Search != "" {
|
||||
query = query.Where("name ILIKE ? OR description ILIKE ?",
|
||||
"%"+params.Search+"%", "%"+params.Search+"%")
|
||||
}
|
||||
|
||||
if params.Color != "" {
|
||||
query = query.Where("color = ?", params.Color)
|
||||
}
|
||||
|
||||
if params.CreatedFrom != nil {
|
||||
query = query.Where("created_at >= ?", *params.CreatedFrom)
|
||||
}
|
||||
|
||||
if params.CreatedTo != nil {
|
||||
query = query.Where("created_at <= ?", *params.CreatedTo)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
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)
|
||||
}
|
||||
|
||||
// 如果需要包含照片计数
|
||||
if params.IncludePhotoCount {
|
||||
query = query.Select("tags.*, COUNT(photo_tags.photo_id) as photo_count").
|
||||
Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Group("tags.id")
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
if err := query.Find(&tags).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return tags, total, nil
|
||||
}
|
||||
|
||||
// Search 搜索标签
|
||||
func (r *tagRepositoryImpl) Search(ctx context.Context, query string) ([]*entity.Tag, error) {
|
||||
var tags []*entity.Tag
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("name ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%").
|
||||
Order("name ASC").
|
||||
Limit(50).
|
||||
Find(&tags).Error
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// GetPopular 获取热门标签
|
||||
func (r *tagRepositoryImpl) GetPopular(ctx context.Context, limit int) ([]*entity.Tag, error) {
|
||||
var tags []*entity.Tag
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Select("tags.*, COUNT(photo_tags.photo_id) as photo_count").
|
||||
Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Group("tags.id").
|
||||
Order("photo_count DESC").
|
||||
Limit(limit).
|
||||
Find(&tags).Error
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// GetByPhotos 根据照片IDs获取标签
|
||||
func (r *tagRepositoryImpl) GetByPhotos(ctx context.Context, photoIDs []uint) ([]*entity.Tag, error) {
|
||||
if len(photoIDs) == 0 {
|
||||
return []*entity.Tag{}, nil
|
||||
}
|
||||
|
||||
var tags []*entity.Tag
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Where("photo_tags.photo_id IN ?", photoIDs).
|
||||
Distinct().
|
||||
Find(&tags).Error
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// CreateMultiple 批量创建标签
|
||||
func (r *tagRepositoryImpl) CreateMultiple(ctx context.Context, tags []*entity.Tag) error {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).CreateInBatches(tags, 100).Error
|
||||
}
|
||||
|
||||
// GetOrCreateByNames 根据名称获取或创建标签
|
||||
func (r *tagRepositoryImpl) GetOrCreateByNames(ctx context.Context, names []string) ([]*entity.Tag, error) {
|
||||
if len(names) == 0 {
|
||||
return []*entity.Tag{}, nil
|
||||
}
|
||||
|
||||
var tags []*entity.Tag
|
||||
|
||||
return tags, r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var tag entity.Tag
|
||||
|
||||
// 尝试获取现有标签
|
||||
err := tx.Where("name = ?", name).First(&tag).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 创建新标签
|
||||
slug, err := r.generateUniqueSlug(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag = entity.Tag{
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
Color: r.generateRandomColor(),
|
||||
PhotoCount: 0,
|
||||
}
|
||||
|
||||
if err := tx.Create(&tag).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tags = append(tags, &tag)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除标签
|
||||
func (r *tagRepositoryImpl) 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_tags WHERE tag_id IN ?", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
return tx.Delete(&entity.Tag{}, ids).Error
|
||||
})
|
||||
}
|
||||
|
||||
// AttachToPhoto 为照片添加标签
|
||||
func (r *tagRepositoryImpl) AttachToPhoto(ctx context.Context, tagID, photoID uint) error {
|
||||
// 检查关联是否已存在
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Table("photo_tags").
|
||||
Where("tag_id = ? AND photo_id = ?", tagID, photoID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil // 关联已存在
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
return r.db.WithContext(ctx).Exec(
|
||||
"INSERT INTO photo_tags (tag_id, photo_id) VALUES (?, ?)",
|
||||
tagID, photoID,
|
||||
).Error
|
||||
}
|
||||
|
||||
// DetachFromPhoto 从照片移除标签
|
||||
func (r *tagRepositoryImpl) DetachFromPhoto(ctx context.Context, tagID, photoID uint) error {
|
||||
return r.db.WithContext(ctx).Exec(
|
||||
"DELETE FROM photo_tags WHERE tag_id = ? AND photo_id = ?",
|
||||
tagID, photoID,
|
||||
).Error
|
||||
}
|
||||
|
||||
// GetPhotoTags 获取照片的标签
|
||||
func (r *tagRepositoryImpl) GetPhotoTags(ctx context.Context, photoID uint) ([]*entity.Tag, error) {
|
||||
var tags []*entity.Tag
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Where("photo_tags.photo_id = ?", photoID).
|
||||
Order("tags.name ASC").
|
||||
Find(&tags).Error
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// Count 统计标签总数
|
||||
func (r *tagRepositoryImpl) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.Tag{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountByPhotos 统计各标签的照片数量
|
||||
func (r *tagRepositoryImpl) CountByPhotos(ctx context.Context) (map[uint]int64, error) {
|
||||
var results []struct {
|
||||
TagID uint `json:"tag_id"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("photo_tags").
|
||||
Select("tag_id, COUNT(photo_id) as photo_count").
|
||||
Group("tag_id").
|
||||
Find(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[uint]int64)
|
||||
for _, result := range results {
|
||||
counts[result.TagID] = result.PhotoCount
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// GetStats 获取标签统计信息
|
||||
func (r *tagRepositoryImpl) GetStats(ctx context.Context) (*entity.TagStats, error) {
|
||||
var stats entity.TagStats
|
||||
|
||||
// 总标签数
|
||||
if total, err := r.Count(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.Total = total
|
||||
}
|
||||
|
||||
// 已使用标签数(有照片关联的标签)
|
||||
var usedCount int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("tags").
|
||||
Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Distinct("tags.id").
|
||||
Count(&usedCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.Used = usedCount
|
||||
|
||||
// 未使用标签数
|
||||
stats.Unused = stats.Total - stats.Used
|
||||
|
||||
// 平均每个标签的照片数
|
||||
if stats.Used > 0 {
|
||||
var totalPhotos int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("photo_tags").
|
||||
Count(&totalPhotos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.AvgPhotosPerTag = float64(totalPhotos) / float64(stats.Used)
|
||||
}
|
||||
|
||||
// 最受欢迎的标签(前10)
|
||||
var popularTags []struct {
|
||||
TagID uint `json:"tag_id"`
|
||||
Name string `json:"name"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("tags").
|
||||
Select("tags.id as tag_id, tags.name, COUNT(photo_tags.photo_id) as photo_count").
|
||||
Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Group("tags.id, tags.name").
|
||||
Order("photo_count DESC").
|
||||
Limit(10).
|
||||
Find(&popularTags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats.PopularTags = make(map[string]int64)
|
||||
for _, tag := range popularTags {
|
||||
stats.PopularTags[tag.Name] = tag.PhotoCount
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GenerateUniqueSlug 生成唯一slug
|
||||
func (r *tagRepositoryImpl) GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) {
|
||||
return r.generateUniqueSlug(ctx, baseName)
|
||||
}
|
||||
|
||||
// ValidateSlugUnique 验证slug唯一性
|
||||
func (r *tagRepositoryImpl) ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.Tag{}).Where("slug = ?", slug)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("slug already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateUniqueSlug 生成唯一slug
|
||||
func (r *tagRepositoryImpl) generateUniqueSlug(ctx context.Context, baseName string) (string, error) {
|
||||
baseSlug := utils.GenerateSlug(baseName)
|
||||
slug := baseSlug
|
||||
|
||||
counter := 1
|
||||
for {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Tag{}).
|
||||
Where("slug = ?", slug).Count(&count).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
|
||||
counter++
|
||||
}
|
||||
|
||||
return slug, nil
|
||||
}
|
||||
|
||||
// generateRandomColor 生成随机颜色
|
||||
func (r *tagRepositoryImpl) generateRandomColor() string {
|
||||
colors := []string{
|
||||
"#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7",
|
||||
"#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9",
|
||||
"#F8C471", "#82E0AA", "#F1948A", "#85C1E9", "#D7BDE2",
|
||||
}
|
||||
return colors[len(colors)%15] // 简单的颜色选择
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"photography-backend/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓库接口
|
||||
type UserRepository interface {
|
||||
Create(user *models.User) error
|
||||
GetByID(id uint) (*models.User, error)
|
||||
GetByUsername(username string) (*models.User, error)
|
||||
GetByEmail(email string) (*models.User, error)
|
||||
Update(user *models.User) error
|
||||
Delete(id uint) error
|
||||
List(page, limit int, role string, isActive *bool) ([]*models.User, int64, error)
|
||||
UpdateLastLogin(id uint) error
|
||||
}
|
||||
|
||||
// userRepository 用户仓库实现
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户仓库
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *userRepository) Create(user *models.User) error {
|
||||
if err := r.db.Create(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *userRepository) GetByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.First(&user, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user by id: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
func (r *userRepository) GetByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user by username: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail 根据邮箱获取用户
|
||||
func (r *userRepository) GetByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user by email: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *userRepository) Update(user *models.User) error {
|
||||
if err := r.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
if err := r.db.Delete(&models.User{}, id).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *userRepository) List(page, limit int, role string, isActive *bool) ([]*models.User, int64, error) {
|
||||
var users []*models.User
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.User{})
|
||||
|
||||
// 添加过滤条件
|
||||
if role != "" {
|
||||
query = query.Where("role = ?", role)
|
||||
}
|
||||
if isActive != nil {
|
||||
query = query.Where("is_active = ?", *isActive)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count users: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).
|
||||
Order("created_at DESC").
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// UpdateLastLogin 更新最后登录时间
|
||||
func (r *userRepository) UpdateLastLogin(id uint) error {
|
||||
if err := r.db.Model(&models.User{}).Where("id = ?", id).
|
||||
Update("last_login", gorm.Expr("NOW()")).Error; err != nil {
|
||||
return fmt.Errorf("failed to update last login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
516
backend/internal/repository/postgres/user_repository_impl.go
Normal file
516
backend/internal/repository/postgres/user_repository_impl.go
Normal file
@ -0,0 +1,516 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// userRepositoryImpl 用户仓储实现
|
||||
type userRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户仓储实现
|
||||
func NewUserRepository(db *gorm.DB, logger *zap.Logger) interfaces.UserRepository {
|
||||
return &userRepositoryImpl{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *userRepositoryImpl) Create(ctx context.Context, user *entity.User) error {
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *userRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail 根据邮箱获取用户
|
||||
func (r *userRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
func (r *userRepositoryImpl) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmailOrUsername 根据邮箱或用户名获取用户
|
||||
func (r *userRepositoryImpl) GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("email = ? OR username = ?", emailOrUsername, emailOrUsername).
|
||||
First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *userRepositoryImpl) Update(ctx context.Context, user *entity.User) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *userRepositoryImpl) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *userRepositoryImpl) List(ctx context.Context, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
||||
var users []*entity.User
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entity.User{})
|
||||
|
||||
// 应用过滤条件
|
||||
if params.Role != nil {
|
||||
query = query.Where("role = ?", *params.Role)
|
||||
}
|
||||
|
||||
if params.Status != nil {
|
||||
query = query.Where("status = ?", *params.Status)
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
"%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%")
|
||||
}
|
||||
|
||||
if params.CreatedFrom != nil {
|
||||
query = query.Where("created_at >= ?", *params.CreatedFrom)
|
||||
}
|
||||
|
||||
if params.CreatedTo != nil {
|
||||
query = query.Where("created_at <= ?", *params.CreatedTo)
|
||||
}
|
||||
|
||||
if params.LastLoginFrom != nil {
|
||||
query = query.Where("last_login_at >= ?", *params.LastLoginFrom)
|
||||
}
|
||||
|
||||
if params.LastLoginTo != nil {
|
||||
query = query.Where("last_login_at <= ?", *params.LastLoginTo)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
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)
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
if err := query.Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// ListByRole 根据角色获取用户列表
|
||||
func (r *userRepositoryImpl) ListByRole(ctx context.Context, role entity.UserRole, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
||||
if params == nil {
|
||||
params = &entity.UserListParams{}
|
||||
}
|
||||
params.Role = &role
|
||||
return r.List(ctx, params)
|
||||
}
|
||||
|
||||
// ListByStatus 根据状态获取用户列表
|
||||
func (r *userRepositoryImpl) ListByStatus(ctx context.Context, status entity.UserStatus, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
||||
if params == nil {
|
||||
params = &entity.UserListParams{}
|
||||
}
|
||||
params.Status = &status
|
||||
return r.List(ctx, params)
|
||||
}
|
||||
|
||||
// SearchUsers 搜索用户
|
||||
func (r *userRepositoryImpl) SearchUsers(ctx context.Context, keyword string, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
||||
if params == nil {
|
||||
params = &entity.UserListParams{}
|
||||
}
|
||||
params.Search = keyword
|
||||
return r.List(ctx, params)
|
||||
}
|
||||
|
||||
// Count 统计用户总数
|
||||
func (r *userRepositoryImpl) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.User{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountByRole 根据角色统计用户数
|
||||
func (r *userRepositoryImpl) CountByRole(ctx context.Context, role entity.UserRole) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("role = ?", role).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountByStatus 根据状态统计用户数
|
||||
func (r *userRepositoryImpl) CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("status = ?", status).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountActiveUsers 统计活跃用户数
|
||||
func (r *userRepositoryImpl) CountActiveUsers(ctx context.Context) (int64, error) {
|
||||
return r.CountByStatus(ctx, entity.UserStatusActive)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新用户状态
|
||||
func (r *userRepositoryImpl) UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error {
|
||||
return r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
// UpdateLastLogin 更新最后登录时间
|
||||
func (r *userRepositoryImpl) UpdateLastLogin(ctx context.Context, id uint) error {
|
||||
now := time.Now()
|
||||
return r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("last_login_at", now).Error
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码
|
||||
func (r *userRepositoryImpl) UpdatePassword(ctx context.Context, id uint, hashedPassword string) error {
|
||||
return r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("password", hashedPassword).Error
|
||||
}
|
||||
|
||||
// SoftDelete 软删除用户
|
||||
func (r *userRepositoryImpl) SoftDelete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error
|
||||
}
|
||||
|
||||
// Restore 恢复软删除用户
|
||||
func (r *userRepositoryImpl) Restore(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Unscoped().Model(&entity.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// BatchUpdateStatus 批量更新用户状态
|
||||
func (r *userRepositoryImpl) BatchUpdateStatus(ctx context.Context, ids []uint, status entity.UserStatus) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("id IN ?", ids).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除用户
|
||||
func (r *userRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Delete(&entity.User{}, ids).Error
|
||||
}
|
||||
|
||||
// ValidateEmailUnique 验证邮箱唯一性
|
||||
func (r *userRepositoryImpl) ValidateEmailUnique(ctx context.Context, email string, excludeID uint) error {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.User{}).Where("email = ?", email)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("email already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUsernameUnique 验证用户名唯一性
|
||||
func (r *userRepositoryImpl) ValidateUsernameUnique(ctx context.Context, username string, excludeID uint) error {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.User{}).Where("username = ?", username)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("username already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserPhotos 获取用户照片
|
||||
func (r *userRepositoryImpl) GetUserPhotos(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
||||
var photos []*entity.Photo
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entity.Photo{}).Where("user_id = ?", userID)
|
||||
|
||||
// 应用过滤条件
|
||||
if params != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 应用排序和分页
|
||||
if params != nil {
|
||||
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("Categories").Preload("Tags")
|
||||
|
||||
// 查询数据
|
||||
if err := query.Find(&photos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
// GetUserStats 获取用户统计信息
|
||||
func (r *userRepositoryImpl) GetUserStats(ctx context.Context, userID uint) (*entity.UserStats, error) {
|
||||
var stats entity.UserStats
|
||||
|
||||
// 照片统计
|
||||
var photoCount int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ?", userID).Count(&photoCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.PhotoCount = photoCount
|
||||
|
||||
// 按状态统计照片
|
||||
for _, status := range []entity.PhotoStatus{
|
||||
entity.PhotoStatusActive,
|
||||
entity.PhotoStatusDraft,
|
||||
entity.PhotoStatusArchived,
|
||||
} {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ? AND status = ?", userID, status).
|
||||
Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch status {
|
||||
case entity.PhotoStatusActive:
|
||||
stats.PublishedPhotos = count
|
||||
case entity.PhotoStatusDraft:
|
||||
stats.DraftPhotos = count
|
||||
case entity.PhotoStatusArchived:
|
||||
stats.ArchivedPhotos = count
|
||||
}
|
||||
}
|
||||
|
||||
// 总浏览数
|
||||
var totalViews int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(SUM(view_count), 0)").Row().Scan(&totalViews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalViews = totalViews
|
||||
|
||||
// 总下载数
|
||||
var totalDownloads int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(SUM(download_count), 0)").Row().Scan(&totalDownloads); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalDownloads = totalDownloads
|
||||
|
||||
// 存储空间使用
|
||||
var storageUsed int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(SUM(file_size), 0)").Row().Scan(&storageUsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.StorageUsed = storageUsed
|
||||
|
||||
// 本月新增照片
|
||||
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 monthlyPhotos int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("user_id = ? AND created_at >= ? AND created_at <= ?", userID, startOfMonth, endOfMonth).
|
||||
Count(&monthlyPhotos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyPhotos = monthlyPhotos
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetAllStats 获取全部用户统计信息
|
||||
func (r *userRepositoryImpl) GetAllStats(ctx context.Context) (*entity.UserGlobalStats, error) {
|
||||
var stats entity.UserGlobalStats
|
||||
|
||||
// 总用户数
|
||||
if total, err := r.Count(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.Total = total
|
||||
}
|
||||
|
||||
// 活跃用户数
|
||||
if active, err := r.CountActiveUsers(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
stats.Active = active
|
||||
}
|
||||
|
||||
// 按角色统计
|
||||
for _, role := range []entity.UserRole{
|
||||
entity.UserRoleAdmin,
|
||||
entity.UserRoleEditor,
|
||||
entity.UserRoleUser,
|
||||
} {
|
||||
if count, err := r.CountByRole(ctx, role); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
switch role {
|
||||
case entity.UserRoleAdmin:
|
||||
stats.Admins = count
|
||||
case entity.UserRoleEditor:
|
||||
stats.Editors = count
|
||||
case entity.UserRoleUser:
|
||||
stats.Users = 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 monthlyUsers int64
|
||||
if err := r.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("created_at >= ? AND created_at <= ?", startOfMonth, endOfMonth).
|
||||
Count(&monthlyUsers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyRegistrations = monthlyUsers
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
Reference in New Issue
Block a user