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:
xujiang
2025-07-09 14:56:22 +08:00
parent 180fbd2ae9
commit c57ec3aa82
34 changed files with 3432 additions and 0 deletions

View 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
}