feat: 实现后端和管理后台基础架构
## 后端架构 (Go + Gin + GORM) - ✅ 完整的分层架构 (API/Service/Repository) - ✅ PostgreSQL数据库设计和迁移脚本 - ✅ JWT认证系统和权限控制 - ✅ 用户、照片、分类、标签等核心模型 - ✅ 中间件系统 (认证、CORS、日志) - ✅ 配置管理和环境变量支持 - ✅ 结构化日志和错误处理 - ✅ Makefile构建和部署脚本 ## 管理后台架构 (React + TypeScript) - ✅ Vite + React 18 + TypeScript现代化架构 - ✅ 路由系统和状态管理 (Zustand + TanStack Query) - ✅ 基于Radix UI的组件库基础 - ✅ 认证流程和权限控制 - ✅ 响应式设计和主题系统 ## 数据库设计 - ✅ 用户表 (角色权限、认证信息) - ✅ 照片表 (元数据、EXIF、状态管理) - ✅ 分类表 (层级结构、封面图片) - ✅ 标签表 (使用统计、标签云) - ✅ 关联表 (照片-标签多对多) ## 技术特点 - 🚀 高性能: Gin框架 + GORM ORM - 🔐 安全: JWT认证 + 密码加密 + 权限控制 - 📊 监控: 结构化日志 + 健康检查 - 🎨 现代化: React 18 + TypeScript + Vite - 📱 响应式: Tailwind CSS + Radix UI 参考文档: docs/development/saved-docs/
This commit is contained in:
303
backend/internal/repository/postgres/photo_repository.go
Normal file
303
backend/internal/repository/postgres/photo_repository.go
Normal file
@ -0,0 +1,303 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user