## 主要变更 - 创建版本化文档目录结构 (v1/, v2/) - 移动核心设计文档到对应版本目录 - 更新文档总览和版本说明 - 保留原有目录结构的兼容性 ## 新增文档 - docs/v1/README.md - v1.0版本开发指南 - docs/v2/README.md - v2.0版本规划文档 - docs/v1/admin/管理后台开发文档.md - docs/v1/backend/Golang项目架构文档.md - docs/v1/database/数据库设计文档.md - docs/v1/api/API接口设计文档.md ## 文档结构优化 - 清晰的版本划分,便于开发者快速定位 - 完整的开发进度跟踪 - 详细的技术栈说明和架构设计 - 未来版本功能规划和技术演进路径 ## 开发者体验提升 - 角色导向的文档导航 - 快速开始指南 - 详细的API和数据库设计文档 - 版本化管理便于迭代开发
1460 lines
54 KiB
Markdown
1460 lines
54 KiB
Markdown
# 摄影作品集网站 - 管理后台开发文档
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 项目定位
|
||
基于现有摄影作品集网站的管理后台系统,提供完整的内容管理、用户管理和系统配置功能。
|
||
|
||
### 1.2 技术栈
|
||
- **后端**: Golang + Gin + GORM + PostgreSQL + Redis
|
||
- **前端**: React + TypeScript + Tailwind CSS + shadcn/ui
|
||
- **文件存储**: MinIO/AWS S3 + 本地存储
|
||
- **图片处理**: libvips + 多格式转换
|
||
- **认证**: JWT + Session管理
|
||
- **部署**: Docker + Caddy
|
||
|
||
### 1.3 设计原则
|
||
- **用户友好**: 直观的界面设计,简化操作流程
|
||
- **高性能**: 异步图片处理,智能缓存策略
|
||
- **可扩展**: 模块化设计,支持功能扩展
|
||
- **安全可靠**: 多层权限控制,操作日志审计
|
||
|
||
## 2. 管理后台功能模块详细设计
|
||
|
||
### 2.1 仪表板模块 (Dashboard)
|
||
|
||
#### 2.1.1 核心功能
|
||
- **数据统计**: 照片总数、分类数量、标签数量、存储使用情况
|
||
- **近期活动**: 最近上传、最近修改、访问统计
|
||
- **快捷操作**: 快速上传、批量处理、系统设置
|
||
- **系统状态**: 服务器状态、缓存状态、队列状态
|
||
|
||
#### 2.1.2 界面设计
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 仪表板 Dashboard │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 📊 统计卡片 │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ 总照片 │ │ 总分类 │ │ 总标签 │ │ 存储用量│ │
|
||
│ │ 1,234 │ │ 12 │ │ 45 │ │ 2.5GB │ │
|
||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||
│ │
|
||
│ 📈 上传趋势图表 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ [折线图显示最近30天上传趋势] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📋 近期活动 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ • 上传了 "城市夜景" 系列 5 张照片 │ │
|
||
│ │ • 创建了新分类 "建筑摄影" │ │
|
||
│ │ • 更新了标签 "城市风光" │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 2.1.3 数据接口
|
||
```go
|
||
// GET /api/admin/dashboard/stats
|
||
type DashboardStats struct {
|
||
TotalPhotos int `json:"total_photos"`
|
||
TotalCategories int `json:"total_categories"`
|
||
TotalTags int `json:"total_tags"`
|
||
StorageUsed int64 `json:"storage_used"`
|
||
StorageLimit int64 `json:"storage_limit"`
|
||
RecentUploads int `json:"recent_uploads"`
|
||
|
||
// 上传趋势 (最近30天)
|
||
UploadTrend []struct {
|
||
Date string `json:"date"`
|
||
Count int `json:"count"`
|
||
} `json:"upload_trend"`
|
||
|
||
// 热门分类
|
||
PopularCategories []struct {
|
||
Name string `json:"name"`
|
||
Count int `json:"count"`
|
||
} `json:"popular_categories"`
|
||
|
||
// 系统状态
|
||
SystemStatus struct {
|
||
DatabaseStatus string `json:"database_status"`
|
||
RedisStatus string `json:"redis_status"`
|
||
StorageStatus string `json:"storage_status"`
|
||
QueueStatus string `json:"queue_status"`
|
||
} `json:"system_status"`
|
||
}
|
||
```
|
||
|
||
### 2.2 照片管理模块 (Photo Management)
|
||
|
||
#### 2.2.1 照片列表页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 照片管理 Photo Management │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 🔍 [搜索框] 📂 [分类筛选] 🏷️ [标签筛选] 📅 [时间筛选] │
|
||
│ ➕ 上传照片 📤 批量操作 ⚙️ 设置 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 📷 照片网格 (支持列表/网格视图切换) │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │
|
||
│ │ 标题 │ │ 标题 │ │ 标题 │ │ 标题 │ │
|
||
│ │ 分类 │ │ 分类 │ │ 分类 │ │ 分类 │ │
|
||
│ │ 2024/01 │ │ 2024/01 │ │ 2024/01 │ │ 2024/01 │ │
|
||
│ │ [编辑] │ │ [编辑] │ │ [编辑] │ │ [编辑] │ │
|
||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||
│ │
|
||
│ 📄 分页: [← 上一页] [1] [2] [3] [下一页 →] │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 2.2.2 照片详情编辑页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 编辑照片 Edit Photo │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ ← 返回列表 │
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||
│ │ │ │ 📝 基本信息 │ │
|
||
│ │ [大图预览] │ │ 标题: [输入框] │ │
|
||
│ │ │ │ 描述: [文本域] │ │
|
||
│ │ [图片信息] │ │ 状态: [下拉选择] │ │
|
||
│ │ - 尺寸: 1920x1080 │ │ │ │
|
||
│ │ - 大小: 2.5MB │ │ 🏷️ 分类管理 │ │
|
||
│ │ - 格式: JPG │ │ [分类选择器] │ │
|
||
│ │ │ │ │ │
|
||
│ │ [EXIF信息] │ │ 🔖 标签管理 │ │
|
||
│ │ - 相机: Canon │ │ [标签输入] │ │
|
||
│ │ - 镜头: 24-70mm │ │ │ │
|
||
│ │ - ISO: 100 │ │ 📅 时间设置 │ │
|
||
│ │ - 光圈: f/2.8 │ │ 拍摄时间: [日期选择器] │ │
|
||
│ │ │ │ │ │
|
||
│ └─────────────────┘ │ 💾 [保存] [取消] [删除] │ │
|
||
│ └─────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 2.2.3 批量上传功能
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 批量上传 Batch Upload │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 📂 拖拽上传区域 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 拖拽文件到此处 │ │
|
||
│ │ 或点击选择文件 │ │
|
||
│ │ │ │
|
||
│ │ 支持: JPG, PNG, RAW, HEIC │ │
|
||
│ │ 最大: 50MB/文件, 100文件/次 │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📋 上传队列 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 📷 photo1.jpg [████████████] 100% ✅ 完成 │ │
|
||
│ │ 📷 photo2.raw [██████░░░░░░] 60% ⏳ 处理中 │ │
|
||
│ │ 📷 photo3.jpg [░░░░░░░░░░░░] 0% ⏸️ 等待 │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ⚙️ 批量设置 │
|
||
│ 分类: [选择分类] 标签: [输入标签] 状态: [发布状态] │
|
||
│ │
|
||
│ 🚀 [开始上传] ⏸️ [暂停] 🗑️ [清空队列] │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.3 分类管理模块 (Category Management)
|
||
|
||
#### 2.3.1 分类列表页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 分类管理 Category Management │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ ➕ 新建分类 📊 分类统计 ⚙️ 批量操作 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 🌳 分类树形结构 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 📂 全部作品 (234张) [编辑] [删除] │ │
|
||
│ │ ├─ 📂 城市风光 (89张) [编辑] [删除] │ │
|
||
│ │ │ ├─ 📂 夜景 (34张) [编辑] [删除] │ │
|
||
│ │ │ └─ 📂 建筑 (55张) [编辑] [删除] │ │
|
||
│ │ ├─ 📂 自然风景 (78张) [编辑] [删除] │ │
|
||
│ │ │ ├─ 📂 山景 (45张) [编辑] [删除] │ │
|
||
│ │ │ └─ 📂 海景 (33张) [编辑] [删除] │ │
|
||
│ │ └─ 📂 人像摄影 (67张) [编辑] [删除] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📊 分类统计 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 总分类: 8个 | 最热门: 城市风光 | 最新: 建筑摄影 │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 2.3.2 分类编辑页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 编辑分类 Edit Category │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ ← 返回列表 │
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||
│ │ [封面预览] │ │ 📝 基本信息 │ │
|
||
│ │ │ │ 名称: [输入框] │ │
|
||
│ │ [选择封面] │ │ 别名: [输入框] │ │
|
||
│ │ │ │ 描述: [文本域] │ │
|
||
│ │ [颜色选择] │ │ │ │
|
||
│ │ 🎨 #d4af37 │ │ 🏗️ 结构设置 │ │
|
||
│ │ │ │ 父分类: [选择器] │ │
|
||
│ │ 统计信息 │ │ 排序: [数字输入] │ │
|
||
│ │ - 照片数: 89 │ │ 状态: [开关] │ │
|
||
│ │ - 子分类: 2 │ │ │ │
|
||
│ │ - 创建: 2024/01 │ │ 💾 [保存] [取消] [删除] │ │
|
||
│ └─────────────────┘ └─────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.4 标签管理模块 (Tag Management)
|
||
|
||
#### 2.4.1 标签列表页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 标签管理 Tag Management │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ ➕ 新建标签 🔍 [搜索框] 📊 使用统计 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 🏷️ 标签云 (按使用频率大小显示) │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 城市风光 自然风景 人像摄影 │ │
|
||
│ │ 夜景 建筑摄影 山景 海景 │ │
|
||
│ │ 街头摄影 微距摄影 黑白 彩色 日出 日落 │ │
|
||
│ │ 雨天 晴天 多云 雪景 春天 夏天 │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📊 标签列表 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 标签名 颜色 使用次数 创建时间 操作 │ │
|
||
│ │ 城市风光 🔵 89次 2024/01/01 [编辑] │ │
|
||
│ │ 自然风景 🟢 78次 2024/01/02 [编辑] │ │
|
||
│ │ 人像摄影 🔴 67次 2024/01/03 [编辑] │ │
|
||
│ │ 夜景 🟡 45次 2024/01/04 [编辑] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.5 时间线管理模块 (Timeline Management)
|
||
|
||
#### 2.5.1 时间线编辑页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 时间线管理 Timeline Management │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 📅 [年份选择] 📊 统计信息 ⚙️ 批量操作 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 🗓️ 时间线编辑器 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 2024年 总计: 234张 │ │
|
||
│ │ ├─ 📅 1月 (45张) [编辑] │ │
|
||
│ │ │ ├─ 📷 城市夜景系列 (12张) [调整] │ │
|
||
│ │ │ ├─ 📷 雪景作品 (8张) [调整] │ │
|
||
│ │ │ └─ 📷 春节街拍 (25张) [调整] │ │
|
||
│ │ ├─ 📅 2月 (38张) [编辑] │ │
|
||
│ │ │ ├─ 📷 梅花摄影 (18张) [调整] │ │
|
||
│ │ │ └─ 📷 古建筑 (20张) [调整] │ │
|
||
│ │ └─ 📅 3月 (51张) [编辑] │ │
|
||
│ │ ├─ 📷 春天花卉 (25张) [调整] │ │
|
||
│ │ └─ 📷 风景写真 (26张) [调整] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 🎯 里程碑事件 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ ➕ 添加里程碑 │ │
|
||
│ │ 📍 2024/01/15 - 首次拍摄城市夜景 │ │
|
||
│ │ 📍 2024/02/20 - 获得摄影比赛奖项 │ │
|
||
│ │ 📍 2024/03/10 - 新镜头首次使用 │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.6 系统设置模块 (System Settings)
|
||
|
||
#### 2.6.1 基本设置页面
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 系统设置 System Settings │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 🔧 基本设置 📷 上传设置 🎨 主题配置 🗂️ 缓存管理 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 📝 网站基本信息 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 网站标题: [摄影作品集] │ │
|
||
│ │ 网站描述: [专业摄影师作品展示] │ │
|
||
│ │ 关键词: [摄影,作品集,艺术] │ │
|
||
│ │ 联系邮箱: [contact@example.com] │ │
|
||
│ │ 版权信息: [© 2024 摄影师姓名] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📷 上传配置 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 最大文件大小: [50] MB │ │
|
||
│ │ 支持格式: [JPG, PNG, RAW, HEIC] │ │
|
||
│ │ 图片质量: [85] % │ │
|
||
│ │ 缩略图尺寸: 小[150px] 中[300px] 大[600px] │ │
|
||
│ │ 自动发布: [开启] 水印添加: [关闭] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 🎨 主题配置 │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 主色调: [🎨 #d4af37] 辅助色: [🎨 #2d2d2d] │ │
|
||
│ │ 字体: [Inter] 布局: [网格] 动画: [开启] │ │
|
||
│ │ 深色模式: [自动] 响应式: [开启] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 💾 [保存设置] 🔄 [重置默认] 📥 [导出配置] │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 3. 技术架构设计
|
||
|
||
### 3.1 后端架构 (Golang)
|
||
|
||
#### 3.1.1 项目结构
|
||
```
|
||
backend/
|
||
├── cmd/
|
||
│ └── server/
|
||
│ └── main.go # 程序入口
|
||
├── internal/
|
||
│ ├── api/ # API层
|
||
│ │ ├── handlers/ # 处理器
|
||
│ │ │ ├── auth.go # 认证相关
|
||
│ │ │ ├── photo.go # 照片管理
|
||
│ │ │ ├── category.go # 分类管理
|
||
│ │ │ ├── tag.go # 标签管理
|
||
│ │ │ ├── timeline.go # 时间线管理
|
||
│ │ │ ├── settings.go # 系统设置
|
||
│ │ │ └── upload.go # 文件上传
|
||
│ │ ├── middleware/ # 中间件
|
||
│ │ │ ├── auth.go # 认证中间件
|
||
│ │ │ ├── cors.go # CORS中间件
|
||
│ │ │ ├── logger.go # 日志中间件
|
||
│ │ │ └── ratelimit.go # 限流中间件
|
||
│ │ └── routes/ # 路由
|
||
│ │ ├── api.go # API路由
|
||
│ │ └── admin.go # 管理路由
|
||
│ ├── service/ # 业务逻辑层
|
||
│ │ ├── auth_service.go # 认证服务
|
||
│ │ ├── photo_service.go # 照片服务
|
||
│ │ ├── category_service.go # 分类服务
|
||
│ │ ├── tag_service.go # 标签服务
|
||
│ │ ├── timeline_service.go # 时间线服务
|
||
│ │ ├── upload_service.go # 上传服务
|
||
│ │ └── settings_service.go # 设置服务
|
||
│ ├── repository/ # 数据访问层
|
||
│ │ ├── photo_repo.go # 照片数据访问
|
||
│ │ ├── category_repo.go # 分类数据访问
|
||
│ │ ├── tag_repo.go # 标签数据访问
|
||
│ │ └── user_repo.go # 用户数据访问
|
||
│ ├── models/ # 数据模型
|
||
│ │ ├── photo.go # 照片模型
|
||
│ │ ├── category.go # 分类模型
|
||
│ │ ├── tag.go # 标签模型
|
||
│ │ └── user.go # 用户模型
|
||
│ └── utils/ # 工具函数
|
||
│ ├── response.go # 响应工具
|
||
│ ├── validator.go # 验证工具
|
||
│ └── image.go # 图片工具
|
||
├── pkg/ # 公共包
|
||
│ ├── config/ # 配置管理
|
||
│ │ └── config.go
|
||
│ ├── database/ # 数据库
|
||
│ │ └── postgres.go
|
||
│ ├── cache/ # 缓存
|
||
│ │ └── redis.go
|
||
│ ├── storage/ # 存储
|
||
│ │ ├── local.go
|
||
│ │ └── s3.go
|
||
│ ├── logger/ # 日志
|
||
│ │ └── logger.go
|
||
│ └── queue/ # 队列
|
||
│ └── redis_queue.go
|
||
├── migrations/ # 数据库迁移
|
||
│ ├── 001_create_photos_table.sql
|
||
│ ├── 002_create_categories_table.sql
|
||
│ └── 003_create_tags_table.sql
|
||
├── scripts/ # 脚本
|
||
│ ├── build.sh
|
||
│ └── deploy.sh
|
||
├── docker/ # Docker配置
|
||
│ ├── Dockerfile
|
||
│ └── docker-compose.yml
|
||
└── docs/ # 文档
|
||
├── api.md
|
||
└── deployment.md
|
||
```
|
||
|
||
#### 3.1.2 依赖管理
|
||
```go
|
||
// go.mod
|
||
module photography-backend
|
||
|
||
go 1.21
|
||
|
||
require (
|
||
github.com/gin-gonic/gin v1.9.1
|
||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||
github.com/spf13/viper v1.16.0
|
||
github.com/sirupsen/logrus v1.9.3
|
||
github.com/go-playground/validator/v10 v10.14.0
|
||
gorm.io/gorm v1.25.4
|
||
gorm.io/driver/postgres v1.5.2
|
||
github.com/redis/go-redis/v9 v9.1.0
|
||
github.com/minio/minio-go/v7 v7.0.63
|
||
github.com/h2non/bimg v1.1.9
|
||
github.com/gorilla/sessions v1.2.1
|
||
github.com/google/uuid v1.3.0
|
||
github.com/swaggo/gin-swagger v1.6.0
|
||
github.com/swaggo/swag v1.16.1
|
||
)
|
||
```
|
||
|
||
### 3.2 前端架构 (React)
|
||
|
||
#### 3.2.1 项目结构
|
||
```
|
||
admin/
|
||
├── src/
|
||
│ ├── components/ # 组件
|
||
│ │ ├── ui/ # 基础UI组件
|
||
│ │ │ ├── button.tsx
|
||
│ │ │ ├── input.tsx
|
||
│ │ │ ├── modal.tsx
|
||
│ │ │ └── table.tsx
|
||
│ │ ├── layout/ # 布局组件
|
||
│ │ │ ├── header.tsx
|
||
│ │ │ ├── sidebar.tsx
|
||
│ │ │ └── main-layout.tsx
|
||
│ │ ├── photo/ # 照片管理组件
|
||
│ │ │ ├── photo-list.tsx
|
||
│ │ │ ├── photo-form.tsx
|
||
│ │ │ ├── photo-upload.tsx
|
||
│ │ │ └── photo-detail.tsx
|
||
│ │ ├── category/ # 分类管理组件
|
||
│ │ │ ├── category-tree.tsx
|
||
│ │ │ ├── category-form.tsx
|
||
│ │ │ └── category-stats.tsx
|
||
│ │ └── common/ # 通用组件
|
||
│ │ ├── loading.tsx
|
||
│ │ ├── error-boundary.tsx
|
||
│ │ └── confirmation.tsx
|
||
│ ├── pages/ # 页面
|
||
│ │ ├── dashboard/
|
||
│ │ │ └── index.tsx
|
||
│ │ ├── photos/
|
||
│ │ │ ├── index.tsx
|
||
│ │ │ ├── edit.tsx
|
||
│ │ │ └── upload.tsx
|
||
│ │ ├── categories/
|
||
│ │ │ └── index.tsx
|
||
│ │ ├── tags/
|
||
│ │ │ └── index.tsx
|
||
│ │ └── settings/
|
||
│ │ └── index.tsx
|
||
│ ├── hooks/ # 自定义Hooks
|
||
│ │ ├── useAuth.ts
|
||
│ │ ├── usePhotos.ts
|
||
│ │ ├── useCategories.ts
|
||
│ │ └── useUpload.ts
|
||
│ ├── services/ # API服务
|
||
│ │ ├── api.ts
|
||
│ │ ├── auth.ts
|
||
│ │ ├── photo.ts
|
||
│ │ ├── category.ts
|
||
│ │ └── upload.ts
|
||
│ ├── store/ # 状态管理
|
||
│ │ ├── auth.ts
|
||
│ │ ├── photo.ts
|
||
│ │ └── ui.ts
|
||
│ ├── utils/ # 工具函数
|
||
│ │ ├── format.ts
|
||
│ │ ├── validation.ts
|
||
│ │ └── constants.ts
|
||
│ └── types/ # 类型定义
|
||
│ ├── api.ts
|
||
│ ├── photo.ts
|
||
│ └── user.ts
|
||
├── public/ # 静态资源
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── tailwind.config.js
|
||
└── vite.config.ts
|
||
```
|
||
|
||
## 4. 核心功能实现
|
||
|
||
### 4.1 图片上传与处理
|
||
|
||
#### 4.1.1 上传流程
|
||
```go
|
||
// internal/service/upload_service.go
|
||
type UploadService struct {
|
||
storage storage.Storage
|
||
imageQueue queue.Queue
|
||
repo repository.PhotoRepository
|
||
}
|
||
|
||
func (s *UploadService) UploadPhoto(file *multipart.FileHeader, metadata PhotoMetadata) (*Photo, error) {
|
||
// 1. 验证文件
|
||
if err := s.validateFile(file); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 生成唯一文件名
|
||
filename := s.generateFilename(file.Filename)
|
||
|
||
// 3. 保存原始文件
|
||
originalPath, err := s.storage.SaveOriginal(file, filename)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 4. 创建照片记录
|
||
photo := &Photo{
|
||
Title: metadata.Title,
|
||
Description: metadata.Description,
|
||
OriginalFilename: file.Filename,
|
||
FileSize: file.Size,
|
||
Status: "processing",
|
||
Categories: metadata.Categories,
|
||
Tags: metadata.Tags,
|
||
}
|
||
|
||
if err := s.repo.Create(photo); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 5. 添加到处理队列
|
||
s.imageQueue.Push(ProcessImageJob{
|
||
PhotoID: photo.ID,
|
||
OriginalPath: originalPath,
|
||
})
|
||
|
||
return photo, nil
|
||
}
|
||
```
|
||
|
||
#### 4.1.2 图片处理队列
|
||
```go
|
||
// internal/service/image_processor.go
|
||
type ImageProcessor struct {
|
||
storage storage.Storage
|
||
repo repository.PhotoRepository
|
||
}
|
||
|
||
func (p *ImageProcessor) ProcessImage(job ProcessImageJob) error {
|
||
// 1. 加载原始图片
|
||
img, err := bimg.NewFromFile(job.OriginalPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 2. 提取EXIF信息
|
||
exif, err := p.extractEXIF(img)
|
||
if err != nil {
|
||
logrus.Warn("Failed to extract EXIF:", err)
|
||
}
|
||
|
||
// 3. 生成多种格式和尺寸
|
||
formats := []FormatConfig{
|
||
{Type: "thumb_small", Width: 150, Height: 150, Quality: 80},
|
||
{Type: "thumb_medium", Width: 300, Height: 300, Quality: 85},
|
||
{Type: "thumb_large", Width: 600, Height: 600, Quality: 90},
|
||
{Type: "display", Width: 1200, Height: 0, Quality: 90},
|
||
{Type: "webp", Width: 1200, Height: 0, Quality: 85, Format: "webp"},
|
||
}
|
||
|
||
var photoFormats []PhotoFormat
|
||
for _, config := range formats {
|
||
processedImg, err := p.processFormat(img, config)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
path, err := p.storage.Save(processedImg, config.Type)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
photoFormats = append(photoFormats, PhotoFormat{
|
||
PhotoID: job.PhotoID,
|
||
FormatType: config.Type,
|
||
FilePath: path,
|
||
FileSize: int64(len(processedImg)),
|
||
Width: config.Width,
|
||
Height: config.Height,
|
||
})
|
||
}
|
||
|
||
// 4. 更新照片信息
|
||
updates := map[string]interface{}{
|
||
"status": "published",
|
||
"camera": exif.Camera,
|
||
"lens": exif.Lens,
|
||
"iso": exif.ISO,
|
||
"aperture": exif.Aperture,
|
||
"shutter_speed": exif.ShutterSpeed,
|
||
"focal_length": exif.FocalLength,
|
||
"taken_at": exif.DateTime,
|
||
"formats": photoFormats,
|
||
}
|
||
|
||
return p.repo.Update(job.PhotoID, updates)
|
||
}
|
||
```
|
||
|
||
### 4.2 分类管理系统
|
||
|
||
#### 4.2.1 树形分类结构
|
||
```go
|
||
// internal/service/category_service.go
|
||
type CategoryService struct {
|
||
repo repository.CategoryRepository
|
||
}
|
||
|
||
func (s *CategoryService) GetCategoryTree() ([]CategoryTree, error) {
|
||
categories, err := s.repo.GetAll()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return s.buildTree(categories, nil), nil
|
||
}
|
||
|
||
func (s *CategoryService) buildTree(categories []Category, parentID *uint) []CategoryTree {
|
||
var tree []CategoryTree
|
||
|
||
for _, category := range categories {
|
||
if category.ParentID == parentID {
|
||
node := CategoryTree{
|
||
Category: category,
|
||
Children: s.buildTree(categories, &category.ID),
|
||
}
|
||
tree = append(tree, node)
|
||
}
|
||
}
|
||
|
||
return tree
|
||
}
|
||
```
|
||
|
||
### 4.3 标签自动建议
|
||
|
||
#### 4.3.1 智能标签推荐
|
||
```go
|
||
// internal/service/tag_service.go
|
||
type TagService struct {
|
||
repo repository.TagRepository
|
||
cache cache.Cache
|
||
}
|
||
|
||
func (s *TagService) GetTagSuggestions(query string, limit int) ([]Tag, error) {
|
||
cacheKey := fmt.Sprintf("tag_suggestions:%s:%d", query, limit)
|
||
|
||
// 检查缓存
|
||
if cached, err := s.cache.Get(cacheKey); err == nil {
|
||
var tags []Tag
|
||
if err := json.Unmarshal(cached, &tags); err == nil {
|
||
return tags, nil
|
||
}
|
||
}
|
||
|
||
// 数据库查询
|
||
tags, err := s.repo.SearchByName(query, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 缓存结果
|
||
if data, err := json.Marshal(tags); err == nil {
|
||
s.cache.Set(cacheKey, data, 5*time.Minute)
|
||
}
|
||
|
||
return tags, nil
|
||
}
|
||
```
|
||
|
||
### 4.4 系统设置管理
|
||
|
||
#### 4.4.1 配置管理
|
||
```go
|
||
// internal/service/settings_service.go
|
||
type SettingsService struct {
|
||
repo repository.SettingsRepository
|
||
cache cache.Cache
|
||
}
|
||
|
||
func (s *SettingsService) GetSettings() (map[string]interface{}, error) {
|
||
cacheKey := "system_settings"
|
||
|
||
// 检查缓存
|
||
if cached, err := s.cache.Get(cacheKey); err == nil {
|
||
var settings map[string]interface{}
|
||
if err := json.Unmarshal(cached, &settings); err == nil {
|
||
return settings, nil
|
||
}
|
||
}
|
||
|
||
// 数据库查询
|
||
rawSettings, err := s.repo.GetAll()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 类型转换
|
||
settings := make(map[string]interface{})
|
||
for _, setting := range rawSettings {
|
||
switch setting.Type {
|
||
case "number":
|
||
if val, err := strconv.Atoi(setting.Value); err == nil {
|
||
settings[setting.Key] = val
|
||
}
|
||
case "boolean":
|
||
settings[setting.Key] = setting.Value == "true"
|
||
case "json":
|
||
var jsonVal interface{}
|
||
if err := json.Unmarshal([]byte(setting.Value), &jsonVal); err == nil {
|
||
settings[setting.Key] = jsonVal
|
||
}
|
||
default:
|
||
settings[setting.Key] = setting.Value
|
||
}
|
||
}
|
||
|
||
// 缓存结果
|
||
if data, err := json.Marshal(settings); err == nil {
|
||
s.cache.Set(cacheKey, data, 10*time.Minute)
|
||
}
|
||
|
||
return settings, nil
|
||
}
|
||
```
|
||
|
||
## 5. 安全性设计
|
||
|
||
### 5.1 用户认证与授权
|
||
|
||
#### 5.1.1 JWT认证
|
||
```go
|
||
// internal/service/auth_service.go
|
||
type AuthService struct {
|
||
userRepo repository.UserRepository
|
||
jwtKey []byte
|
||
}
|
||
|
||
func (s *AuthService) Login(username, password string) (*LoginResponse, error) {
|
||
user, err := s.userRepo.GetByUsername(username)
|
||
if err != nil {
|
||
return nil, ErrInvalidCredentials
|
||
}
|
||
|
||
if !s.verifyPassword(user.PasswordHash, password) {
|
||
return nil, ErrInvalidCredentials
|
||
}
|
||
|
||
token, err := s.generateToken(user)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 记录登录日志
|
||
s.logUserActivity(user.ID, "login", nil)
|
||
|
||
return &LoginResponse{
|
||
Token: token,
|
||
User: user,
|
||
}, nil
|
||
}
|
||
|
||
func (s *AuthService) generateToken(user *User) (string, error) {
|
||
claims := jwt.MapClaims{
|
||
"user_id": user.ID,
|
||
"username": user.Username,
|
||
"role": user.Role,
|
||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString(s.jwtKey)
|
||
}
|
||
```
|
||
|
||
#### 5.1.2 权限中间件
|
||
```go
|
||
// internal/api/middleware/auth.go
|
||
func AuthMiddleware(jwtKey []byte) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
tokenString := c.GetHeader("Authorization")
|
||
if tokenString == "" {
|
||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
|
||
|
||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||
return jwtKey, nil
|
||
})
|
||
|
||
if err != nil || !token.Valid {
|
||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
claims, ok := token.Claims.(jwt.MapClaims)
|
||
if !ok {
|
||
c.JSON(401, gin.H{"error": "Invalid token claims"})
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
c.Set("user_id", claims["user_id"])
|
||
c.Set("username", claims["username"])
|
||
c.Set("role", claims["role"])
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 文件上传安全
|
||
|
||
#### 5.2.1 文件验证
|
||
```go
|
||
// internal/utils/validator.go
|
||
func ValidateImageFile(file *multipart.FileHeader) error {
|
||
// 检查文件大小
|
||
maxSize := int64(50 * 1024 * 1024) // 50MB
|
||
if file.Size > maxSize {
|
||
return ErrFileTooLarge
|
||
}
|
||
|
||
// 检查文件扩展名
|
||
allowedExts := []string{".jpg", ".jpeg", ".png", ".raw", ".heic"}
|
||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||
if !contains(allowedExts, ext) {
|
||
return ErrInvalidFileType
|
||
}
|
||
|
||
// 检查MIME类型
|
||
src, err := file.Open()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer src.Close()
|
||
|
||
buffer := make([]byte, 512)
|
||
if _, err := src.Read(buffer); err != nil {
|
||
return err
|
||
}
|
||
|
||
mimeType := http.DetectContentType(buffer)
|
||
allowedMimes := []string{"image/jpeg", "image/png", "image/x-canon-cr2"}
|
||
if !contains(allowedMimes, mimeType) {
|
||
return ErrInvalidMimeType
|
||
}
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
## 6. 性能优化
|
||
|
||
### 6.1 数据库优化
|
||
|
||
#### 6.1.1 索引设计
|
||
```sql
|
||
-- 照片表索引
|
||
CREATE INDEX idx_photos_status ON photos(status);
|
||
CREATE INDEX idx_photos_created_at ON photos(created_at);
|
||
CREATE INDEX idx_photos_taken_at ON photos(taken_at);
|
||
CREATE INDEX idx_photos_status_created_at ON photos(status, created_at);
|
||
|
||
-- 分类表索引
|
||
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
|
||
CREATE INDEX idx_categories_slug ON categories(slug);
|
||
CREATE INDEX idx_categories_sort_order ON categories(sort_order);
|
||
|
||
-- 标签表索引
|
||
CREATE INDEX idx_tags_name ON tags(name);
|
||
CREATE INDEX idx_tags_usage_count ON tags(usage_count);
|
||
|
||
-- 关联表索引
|
||
CREATE INDEX idx_photo_categories_photo_id ON photo_categories(photo_id);
|
||
CREATE INDEX idx_photo_categories_category_id ON photo_categories(category_id);
|
||
CREATE INDEX idx_photo_tags_photo_id ON photo_tags(photo_id);
|
||
CREATE INDEX idx_photo_tags_tag_id ON photo_tags(tag_id);
|
||
```
|
||
|
||
#### 6.1.2 查询优化
|
||
```go
|
||
// internal/repository/photo_repo.go
|
||
func (r *PhotoRepository) GetPhotosWithPagination(req PhotoListRequest) ([]Photo, int64, error) {
|
||
var photos []Photo
|
||
var total int64
|
||
|
||
query := r.db.Model(&Photo{}).
|
||
Preload("Categories").
|
||
Preload("Tags").
|
||
Preload("Formats")
|
||
|
||
// 条件过滤
|
||
if req.Status != "" {
|
||
query = query.Where("status = ?", req.Status)
|
||
}
|
||
|
||
if req.CategoryID != 0 {
|
||
query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
|
||
Where("photo_categories.category_id = ?", req.CategoryID)
|
||
}
|
||
|
||
if req.TagID != 0 {
|
||
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
||
Where("photo_tags.tag_id = ?", req.TagID)
|
||
}
|
||
|
||
if req.Search != "" {
|
||
query = query.Where("title ILIKE ? OR description ILIKE ?",
|
||
"%"+req.Search+"%", "%"+req.Search+"%")
|
||
}
|
||
|
||
// 统计总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页查询
|
||
offset := (req.Page - 1) * req.Limit
|
||
if err := query.Offset(offset).Limit(req.Limit).
|
||
Order(fmt.Sprintf("%s %s", req.SortBy, req.SortOrder)).
|
||
Find(&photos).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return photos, total, nil
|
||
}
|
||
```
|
||
|
||
### 6.2 缓存策略
|
||
|
||
#### 6.2.1 多级缓存
|
||
```go
|
||
// internal/service/cache_service.go
|
||
type CacheService struct {
|
||
redis *redis.Client
|
||
memCache *cache.Cache
|
||
}
|
||
|
||
func (s *CacheService) Get(key string) ([]byte, error) {
|
||
// L1: 内存缓存
|
||
if data, found := s.memCache.Get(key); found {
|
||
return data.([]byte), nil
|
||
}
|
||
|
||
// L2: Redis缓存
|
||
data, err := s.redis.Get(context.Background(), key).Bytes()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 回写到内存缓存
|
||
s.memCache.Set(key, data, 5*time.Minute)
|
||
|
||
return data, nil
|
||
}
|
||
|
||
func (s *CacheService) Set(key string, value []byte, ttl time.Duration) error {
|
||
// 设置内存缓存
|
||
s.memCache.Set(key, value, ttl)
|
||
|
||
// 设置Redis缓存
|
||
return s.redis.Set(context.Background(), key, value, ttl).Err()
|
||
}
|
||
```
|
||
|
||
## 7. 部署方案
|
||
|
||
### 7.1 Docker部署
|
||
|
||
#### 7.1.1 Dockerfile
|
||
```dockerfile
|
||
# 后端Dockerfile
|
||
FROM golang:1.21-alpine AS builder
|
||
|
||
WORKDIR /app
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
|
||
COPY . .
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/server/main.go
|
||
|
||
FROM alpine:latest
|
||
RUN apk --no-cache add ca-certificates vips-dev
|
||
WORKDIR /root/
|
||
|
||
COPY --from=builder /app/main .
|
||
COPY --from=builder /app/migrations ./migrations
|
||
|
||
EXPOSE 8080
|
||
CMD ["./main"]
|
||
```
|
||
|
||
#### 7.1.2 docker-compose.yml
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
backend:
|
||
build: .
|
||
ports:
|
||
- "8080:8080"
|
||
depends_on:
|
||
- postgres
|
||
- redis
|
||
environment:
|
||
- DB_HOST=postgres
|
||
- DB_PORT=5432
|
||
- DB_USER=postgres
|
||
- DB_PASSWORD=password
|
||
- DB_NAME=photography
|
||
- REDIS_HOST=redis
|
||
- REDIS_PORT=6379
|
||
volumes:
|
||
- ./uploads:/app/uploads
|
||
|
||
postgres:
|
||
image: postgres:15
|
||
environment:
|
||
- POSTGRES_DB=photography
|
||
- POSTGRES_USER=postgres
|
||
- POSTGRES_PASSWORD=password
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
ports:
|
||
- "5432:5432"
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
ports:
|
||
- "6379:6379"
|
||
volumes:
|
||
- redis_data:/data
|
||
|
||
minio:
|
||
image: minio/minio
|
||
ports:
|
||
- "9000:9000"
|
||
- "9001:9001"
|
||
environment:
|
||
- MINIO_ROOT_USER=admin
|
||
- MINIO_ROOT_PASSWORD=password123
|
||
command: server /data --console-address ":9001"
|
||
volumes:
|
||
- minio_data:/data
|
||
|
||
volumes:
|
||
postgres_data:
|
||
redis_data:
|
||
minio_data:
|
||
```
|
||
|
||
### 7.2 生产环境配置
|
||
|
||
#### 7.2.1 Caddy配置
|
||
```
|
||
# /etc/caddy/Caddyfile
|
||
admin.photography.iriver.top {
|
||
reverse_proxy localhost:3000
|
||
|
||
# 上传限制
|
||
request_body {
|
||
max_size 100MB
|
||
}
|
||
|
||
# 安全头
|
||
header {
|
||
X-Frame-Options "DENY"
|
||
X-Content-Type-Options "nosniff"
|
||
X-XSS-Protection "1; mode=block"
|
||
Referrer-Policy "strict-origin-when-cross-origin"
|
||
}
|
||
|
||
# 日志
|
||
log {
|
||
output file /var/log/caddy/admin.log
|
||
format json
|
||
}
|
||
}
|
||
|
||
api.photography.iriver.top {
|
||
reverse_proxy localhost:8080
|
||
|
||
# CORS设置
|
||
header {
|
||
Access-Control-Allow-Origin "https://admin.photography.iriver.top"
|
||
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||
Access-Control-Allow-Headers "Content-Type, Authorization"
|
||
}
|
||
|
||
# API限流
|
||
rate_limit {
|
||
zone api_zone
|
||
key remote_addr
|
||
events 100
|
||
window 1m
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 监控与日志
|
||
|
||
### 8.1 应用监控
|
||
|
||
#### 8.1.1 健康检查
|
||
```go
|
||
// internal/api/handlers/health.go
|
||
func (h *HealthHandler) CheckHealth(c *gin.Context) {
|
||
checks := map[string]string{
|
||
"database": h.checkDatabase(),
|
||
"redis": h.checkRedis(),
|
||
"storage": h.checkStorage(),
|
||
"queue": h.checkQueue(),
|
||
}
|
||
|
||
healthy := true
|
||
for _, status := range checks {
|
||
if status != "ok" {
|
||
healthy = false
|
||
break
|
||
}
|
||
}
|
||
|
||
status := "healthy"
|
||
httpStatus := 200
|
||
if !healthy {
|
||
status = "unhealthy"
|
||
httpStatus = 503
|
||
}
|
||
|
||
c.JSON(httpStatus, gin.H{
|
||
"status": status,
|
||
"timestamp": time.Now().UTC(),
|
||
"checks": checks,
|
||
})
|
||
}
|
||
```
|
||
|
||
#### 8.1.2 性能指标
|
||
```go
|
||
// internal/api/middleware/metrics.go
|
||
func MetricsMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
start := time.Now()
|
||
|
||
c.Next()
|
||
|
||
duration := time.Since(start)
|
||
|
||
// 记录响应时间
|
||
metrics.Histogram("http_request_duration_seconds",
|
||
duration.Seconds(),
|
||
"method", c.Request.Method,
|
||
"path", c.FullPath(),
|
||
"status", strconv.Itoa(c.Writer.Status()),
|
||
)
|
||
|
||
// 记录请求计数
|
||
metrics.Counter("http_requests_total",
|
||
"method", c.Request.Method,
|
||
"path", c.FullPath(),
|
||
"status", strconv.Itoa(c.Writer.Status()),
|
||
)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 日志管理
|
||
|
||
#### 8.2.1 结构化日志
|
||
```go
|
||
// pkg/logger/logger.go
|
||
func InitLogger(config *config.Config) {
|
||
logrus.SetFormatter(&logrus.JSONFormatter{
|
||
TimestampFormat: time.RFC3339,
|
||
})
|
||
|
||
logrus.SetLevel(logrus.InfoLevel)
|
||
if config.Debug {
|
||
logrus.SetLevel(logrus.DebugLevel)
|
||
}
|
||
|
||
// 设置日志输出
|
||
file, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||
if err != nil {
|
||
logrus.Warn("Failed to open log file, using stdout")
|
||
} else {
|
||
logrus.SetOutput(file)
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
func LogUserActivity(userID uint, action string, details map[string]interface{}) {
|
||
logrus.WithFields(logrus.Fields{
|
||
"user_id": userID,
|
||
"action": action,
|
||
"details": details,
|
||
"type": "user_activity",
|
||
}).Info("User activity logged")
|
||
}
|
||
```
|
||
|
||
## 9. 开发规范
|
||
|
||
### 9.1 代码规范
|
||
|
||
#### 9.1.1 Go代码规范
|
||
```go
|
||
// 包注释
|
||
// Package handlers 提供HTTP处理器实现
|
||
package handlers
|
||
|
||
// 结构体注释
|
||
// PhotoHandler 处理照片相关的HTTP请求
|
||
type PhotoHandler struct {
|
||
service service.PhotoService
|
||
logger *logrus.Logger
|
||
}
|
||
|
||
// 方法注释
|
||
// GetPhotos 获取照片列表
|
||
// @Summary 获取照片列表
|
||
// @Description 支持分页、筛选、搜索等功能
|
||
// @Tags 照片管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param page query int false "页码"
|
||
// @Param limit query int false "每页数量"
|
||
// @Success 200 {object} PhotoListResponse
|
||
// @Failure 400 {object} ErrorResponse
|
||
// @Router /api/photos [get]
|
||
func (h *PhotoHandler) GetPhotos(c *gin.Context) {
|
||
// 参数验证
|
||
var req PhotoListRequest
|
||
if err := c.ShouldBindQuery(&req); err != nil {
|
||
h.logger.WithError(err).Error("Invalid request parameters")
|
||
c.JSON(400, gin.H{"error": "Invalid parameters"})
|
||
return
|
||
}
|
||
|
||
// 业务逻辑
|
||
photos, total, err := h.service.GetPhotos(req)
|
||
if err != nil {
|
||
h.logger.WithError(err).Error("Failed to get photos")
|
||
c.JSON(500, gin.H{"error": "Internal server error"})
|
||
return
|
||
}
|
||
|
||
// 返回响应
|
||
c.JSON(200, PhotoListResponse{
|
||
Photos: photos,
|
||
Pagination: PaginationInfo{
|
||
Page: req.Page,
|
||
Limit: req.Limit,
|
||
Total: total,
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
### 9.2 API文档
|
||
|
||
#### 9.2.1 Swagger文档
|
||
```yaml
|
||
# docs/swagger.yaml
|
||
openapi: 3.0.0
|
||
info:
|
||
title: Photography Admin API
|
||
version: 1.0.0
|
||
description: 摄影作品集管理后台API
|
||
|
||
paths:
|
||
/api/photos:
|
||
get:
|
||
summary: 获取照片列表
|
||
tags:
|
||
- 照片管理
|
||
parameters:
|
||
- name: page
|
||
in: query
|
||
schema:
|
||
type: integer
|
||
default: 1
|
||
- name: limit
|
||
in: query
|
||
schema:
|
||
type: integer
|
||
default: 20
|
||
- name: category
|
||
in: query
|
||
schema:
|
||
type: string
|
||
- name: status
|
||
in: query
|
||
schema:
|
||
type: string
|
||
enum: [published, draft, archived]
|
||
responses:
|
||
200:
|
||
description: 成功
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PhotoListResponse'
|
||
400:
|
||
description: 请求参数错误
|
||
500:
|
||
description: 服务器错误
|
||
|
||
components:
|
||
schemas:
|
||
Photo:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
title:
|
||
type: string
|
||
description:
|
||
type: string
|
||
status:
|
||
type: string
|
||
enum: [published, draft, archived]
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
categories:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Category'
|
||
tags:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Tag'
|
||
formats:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/PhotoFormat'
|
||
```
|
||
|
||
## 10. 测试策略
|
||
|
||
### 10.1 单元测试
|
||
|
||
#### 10.1.1 服务层测试
|
||
```go
|
||
// internal/service/photo_service_test.go
|
||
func TestPhotoService_GetPhotos(t *testing.T) {
|
||
// 准备测试数据
|
||
mockRepo := &MockPhotoRepository{}
|
||
service := NewPhotoService(mockRepo)
|
||
|
||
expectedPhotos := []Photo{
|
||
{ID: 1, Title: "Test Photo 1"},
|
||
{ID: 2, Title: "Test Photo 2"},
|
||
}
|
||
|
||
mockRepo.On("GetPhotosWithPagination", mock.Anything).
|
||
Return(expectedPhotos, int64(2), nil)
|
||
|
||
// 执行测试
|
||
req := PhotoListRequest{Page: 1, Limit: 10}
|
||
photos, total, err := service.GetPhotos(req)
|
||
|
||
// 断言
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, int64(2), total)
|
||
assert.Len(t, photos, 2)
|
||
assert.Equal(t, "Test Photo 1", photos[0].Title)
|
||
|
||
mockRepo.AssertExpectations(t)
|
||
}
|
||
```
|
||
|
||
### 10.2 集成测试
|
||
|
||
#### 10.2.1 API测试
|
||
```go
|
||
// test/integration/photo_api_test.go
|
||
func TestPhotoAPI_GetPhotos(t *testing.T) {
|
||
// 设置测试环境
|
||
app := setupTestApp()
|
||
|
||
// 创建测试数据
|
||
createTestPhotos(t)
|
||
|
||
// 发起请求
|
||
req := httptest.NewRequest("GET", "/api/photos?page=1&limit=10", nil)
|
||
resp := httptest.NewRecorder()
|
||
|
||
app.ServeHTTP(resp, req)
|
||
|
||
// 检查响应
|
||
assert.Equal(t, 200, resp.Code)
|
||
|
||
var response PhotoListResponse
|
||
err := json.Unmarshal(resp.Body.Bytes(), &response)
|
||
assert.NoError(t, err)
|
||
assert.NotEmpty(t, response.Photos)
|
||
}
|
||
```
|
||
|
||
## 11. 部署清单
|
||
|
||
### 11.1 开发环境部署
|
||
```bash
|
||
# 1. 克隆代码
|
||
git clone <repository>
|
||
cd photography-admin
|
||
|
||
# 2. 启动数据库
|
||
docker-compose up -d postgres redis minio
|
||
|
||
# 3. 运行迁移
|
||
make migrate
|
||
|
||
# 4. 启动后端
|
||
make run-backend
|
||
|
||
# 5. 启动前端
|
||
cd admin && npm install && npm run dev
|
||
```
|
||
|
||
### 11.2 生产环境部署
|
||
```bash
|
||
# 1. 构建镜像
|
||
make build-docker
|
||
|
||
# 2. 部署到服务器
|
||
make deploy-prod
|
||
|
||
# 3. 检查服务状态
|
||
make health-check
|
||
|
||
# 4. 查看日志
|
||
make logs
|
||
```
|
||
|
||
这个详细的管理后台设计文档涵盖了从功能设计到技术实现的各个方面,为Golang后端开发提供了完整的指导。 |