## 后端架构 (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/
303 lines
8.5 KiB
Go
303 lines
8.5 KiB
Go
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
|
|
} |