Files
photography/docs/development/saved-docs/management-backend.md
2025-07-09 14:32:52 +08:00

1823 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 摄影作品集网站 - 管理后台开发文档
## 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 │
├─────────────────────────────────────────────────────────┤
│ 🔍 [搜索框] 📂 [分类筛选] 🏷️ [标签筛选] 📅 [时间筛选] │
上传照片 📤 批量操作 ⚙️ 设置 │
├─────────────────────────────────────────────────────────┤
│ 📷 照片网格 (支持列表/网格视图切换) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │
│ │ 标题 │ │ 标题 │ │ 标题 │ │ 标题 │ │
│ │ 分类 │ │ 分类 │ │ 分类 │ │ 分类 │ │
│ │ 日期 │ │ 日期 │ │ 日期 │ │ 日期 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ [更多照片...] │
│ │
│ ⬅️ 上一页 [1] [2] [3] ... [10] 下一页 ➡️ │
└─────────────────────────────────────────────────────────┘
```
#### 2.2.2 照片详情/编辑页面
```
┌─────────────────────────────────────────────────────────┐
│ 照片详情 Photo Detail │
├─────────────────────────────────────────────────────────┤
│ ⬅️ 返回列表 📝 编辑模式 🗑️ 删除 💾 保存 │
├─────────────────────────────────────────────────────────┤
│ 📸 图片预览 │ 📋 基本信息 │
│ ┌─────────────────────────────┐ │ ┌─────────────────────┐ │
│ │ │ │ │ 标题: [输入框] │ │
│ │ [原图显示] │ │ │ 描述: [文本框] │ │
│ │ │ │ │ 分类: [下拉选择] │ │
│ │ │ │ │ 标签: [标签输入] │ │
│ │ │ │ │ 状态: [开关] │ │
│ │ │ │ │ 拍摄时间: [日期] │ │
│ │ │ │ │ 位置: [输入框] │ │
│ │ │ │ └─────────────────────┘ │
│ └─────────────────────────────┘ │ │
│ │ 📷 EXIF信息 │
│ 🎨 图片处理 │ ┌─────────────────────┐ │
│ ┌─────────────────────────────┐ │ │ 相机: Canon EOS R5 │ │
│ │ [裁剪] [旋转] [滤镜] [调色] │ │ │ 镜头: 24-70mm f/2.8 │ │
│ │ [缩放] [水印] [格式转换] │ │ │ 光圈: f/2.8 │ │
│ └─────────────────────────────┘ │ │ 快门: 1/60s │ │
│ │ │ ISO: 400 │ │
│ │ │ 焦距: 35mm │ │
│ │ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
#### 2.2.3 批量上传组件
```
┌─────────────────────────────────────────────────────────┐
│ 批量上传 Batch Upload │
├─────────────────────────────────────────────────────────┤
│ 📁 拖拽上传区域 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 📸 拖拽图片到这里 或 点击选择文件 │ │
│ │ 支持 JPG, PNG, RAW, TIFF │ │
│ │ 最大文件大小: 50MB │ │
│ │ 最多同时上传: 20个文件 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 📋 上传队列 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 📸 IMG_001.jpg [██████████] 100% ✅ 完成 │ │
│ │ 📸 IMG_002.jpg [█████████▒] 90% ⏳ 上传中 │ │
│ │ 📸 IMG_003.jpg [██▒▒▒▒▒▒▒▒] 20% ⏳ 上传中 │ │
│ │ 📸 IMG_004.jpg [▒▒▒▒▒▒▒▒▒▒] 0% ⏸️ 等待 │ │
│ │ 📸 IMG_005.jpg [▒▒▒▒▒▒▒▒▒▒] 0% ⏸️ 等待 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚙️ 批量设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 默认分类: [下拉选择] │ │
│ │ 默认标签: [标签输入] │ │
│ │ 图片质量: [滑块] 85% │ │
│ │ 生成缩略图: [✅] 生成WebP格式: [✅] │ │
│ │ 提取EXIF: [✅] 自动分类: [✅] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 🎯 操作按钮 │
│ [⏸️ 暂停全部] [▶️ 继续全部] [🗑️ 清空队列] [⚙️ 高级设置] │
└─────────────────────────────────────────────────────────┘
```
### 2.3 分类管理模块 (Category Management)
#### 2.3.1 分类树形结构
```
┌─────────────────────────────────────────────────────────┐
│ 分类管理 Category Management │
├─────────────────────────────────────────────────────────┤
新建分类 📊 分类统计 🔄 重新排序 ⚙️ 批量操作 │
├─────────────────────────────────────────────────────────┤
│ 📂 分类树 (拖拽排序) │ 📋 分类详情 │
│ ┌─────────────────────────────────┐ │ ┌─────────────────┐ │
│ │ 📁 风景摄影 (125) │ │ │ 分类名: 风景摄影 │ │
│ │ ├─ 🏔️ 山川 (45) │ │ │ 父分类: 无 │ │
│ │ ├─ 🌊 海岸 (32) │ │ │ 描述: [文本框] │ │
│ │ ├─ 🌲 森林 (28) │ │ │ 颜色: [🔴] │ │
│ │ └─ 🌅 日出日落 (20) │ │ │ 封面: [选择图片] │ │
│ │ │ │ │ 状态: [✅] 启用 │ │
│ │ 📁 人像摄影 (89) │ │ │ 排序: 1 │ │
│ │ ├─ 👤 肖像 (34) │ │ │ 创建时间: 2024-01│ │
│ │ ├─ 👥 群像 (28) │ │ │ 照片数量: 125 │ │
│ │ ├─ 💃 艺术 (15) │ │ └─────────────────┘ │
│ │ └─ 📸 街头 (12) │ │ │
│ │ │ │ 🎨 操作 │
│ │ 📁 建筑摄影 (67) │ │ ┌─────────────────┐ │
│ │ ├─ 🏢 现代建筑 (23) │ │ │ [📝 编辑] │ │
│ │ ├─ 🏛️ 古建筑 (22) │ │ │ [👁️ 查看照片] │ │
│ │ ├─ 🌃 夜景 (12) │ │ │ [📊 统计] │ │
│ │ └─ 🏘️ 城市风光 (10) │ │ │ [🗑️ 删除] │ │
│ │ │ │ └─────────────────┘ │
│ └─────────────────────────────────┘ │ │
└─────────────────────────────────────────────────────────┘
```
#### 2.3.2 分类编辑表单
```
┌─────────────────────────────────────────────────────────┐
│ 编辑分类 Edit Category │
├─────────────────────────────────────────────────────────┤
│ 📝 基本信息 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 分类名称: [输入框] *必填 │ │
│ │ 父分类: [下拉选择] - 选择父分类 │ │
│ │ 分类描述: [文本框] - 详细描述 │ │
│ │ 分类颜色: [🔴🟡🟢🔵🟣] - 选择标识色 │ │
│ │ 排序权重: [数字输入] - 数字越小排序越前 │ │
│ │ 状态: [开关] 启用/禁用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 🖼️ 封面设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 封面图片: [选择图片] [上传新图片] │ │
│ │ ┌─────────────────┐ │ │
│ │ │ [预览图片] │ │ │
│ │ │ │ │ │
│ │ └─────────────────┘ │ │
│ │ 图片尺寸: 建议 400x300 像素 │ │
│ │ 文件大小: 不超过 2MB │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚙️ 高级设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SEO设置: │ │
│ │ URL别名: [输入框] - 用于URL路径 │ │
│ │ Meta描述: [文本框] - 搜索引擎描述 │ │
│ │ 关键词: [标签输入] - SEO关键词 │ │
│ │ │ │
│ │ 权限设置: │ │
│ │ 可见性: [公开/私有/仅登录用户] │ │
│ │ 管理权限: [仅管理员/编辑者/所有用户] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 💾 操作按钮 │
│ [💾 保存] [🔄 重置] [👁️ 预览] [❌ 取消] │
└─────────────────────────────────────────────────────────┘
```
### 2.4 标签管理模块 (Tag Management)
#### 2.4.1 标签云展示
```
┌─────────────────────────────────────────────────────────┐
│ 标签管理 Tag Management │
├─────────────────────────────────────────────────────────┤
新建标签 📊 使用统计 🔄 批量操作 🔍 搜索标签 │
├─────────────────────────────────────────────────────────┤
│ ☁️ 标签云 (按使用频率显示) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 自然 (120) 日落 (89) 肖像 (76) │ │
│ │ 城市 (145) 风景 (156) 黑白 (45) 建筑 (67) │ │
│ │ 艺术 (34) 旅行 (123) 海岸 (32) │ │
│ │ 山川 (45) 夜景 (28) 街头 (23) 现代 (56) │ │
│ │ 森林 (28) 抽象 (19) 光影 (67) │ │
│ │ 情感 (23) 色彩 (89) 构图 (45) 纹理 (34) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 📊 使用统计 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔥 热门标签 │ │
│ │ 1. 风景 (156张) ████████████████████████████████ │ │
│ │ 2. 城市 (145张) ████████████████████████████████ │ │
│ │ 3. 旅行 (123张) ████████████████████████████ │ │
│ │ 4. 自然 (120张) ████████████████████████████ │ │
│ │ 5. 日落 (89张) ████████████████████████ │ │
│ │ │ │
│ │ 📈 增长趋势 │ │
│ │ 本周新增: 12个标签 │ │
│ │ 本月活跃: 45个标签 │ │
│ │ 平均每张照片: 3.2个标签 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
#### 2.4.2 标签列表管理
```
┌─────────────────────────────────────────────────────────┐
│ 标签列表 Tag List │
├─────────────────────────────────────────────────────────┤
│ 🔍 [搜索标签] 📊 [按使用量排序] 🏷️ [按颜色筛选] [批量] │
├─────────────────────────────────────────────────────────┤
│ 标签名 │ 颜色 │ 使用数量 │ 创建时间 │ 状态 │ 操作 │
│ ─────────┼──────┼─────────┼──────────┼─────┼────── │
│ 风景 │ 🟢 │ 156张 │ 2024-01-01│ 启用 │ 编辑 │
│ 城市 │ 🔵 │ 145张 │ 2024-01-01│ 启用 │ 编辑 │
│ 旅行 │ 🟡 │ 123张 │ 2024-01-02│ 启用 │ 编辑 │
│ 自然 │ 🟢 │ 120张 │ 2024-01-01│ 启用 │ 编辑 │
│ 日落 │ 🟠 │ 89张 │ 2024-01-03│ 启用 │ 编辑 │
│ 色彩 │ 🎨 │ 89张 │ 2024-01-05│ 启用 │ 编辑 │
│ 肖像 │ 🟣 │ 76张 │ 2024-01-02│ 启用 │ 编辑 │
│ 光影 │ ⚪ │ 67张 │ 2024-01-04│ 启用 │ 编辑 │
│ 建筑 │ 🔴 │ 67张 │ 2024-01-03│ 启用 │ 编辑 │
│ 现代 │ 🔘 │ 56张 │ 2024-01-06│ 启用 │ 编辑 │
├─────────────────────────────────────────────────────────┤
│ ⬅️ 上一页 [1] [2] [3] ... [10] 下一页 ➡️ │
└─────────────────────────────────────────────────────────┘
```
### 2.5 用户管理模块 (User Management)
#### 2.5.1 用户列表页面
```
┌─────────────────────────────────────────────────────────┐
│ 用户管理 User Management │
├─────────────────────────────────────────────────────────┤
新建用户 👥 角色管理 🔍 搜索用户 📊 用户统计 │
├─────────────────────────────────────────────────────────┤
│ 用户名 │ 邮箱 │ 角色 │ 状态 │ 最后登录 │ 操作 │
│ ─────────┼──────────┼───────┼─────┼─────────┼────── │
│ admin │ admin@.. │ 管理员 │ 🟢活跃│ 2小时前 │ 编辑 │
│ editor │ editor@.. │ 编辑者 │ 🟢活跃│ 1天前 │ 编辑 │
│ user001 │ user001@..│ 用户 │ 🟡离线│ 3天前 │ 编辑 │
│ user002 │ user002@..│ 用户 │ 🔴禁用│ 1周前 │ 编辑 │
│ guest │ guest@.. │ 访客 │ 🟢活跃│ 5分钟前 │ 编辑 │
└─────────────────────────────────────────────────────────┘
```
#### 2.5.2 用户编辑表单
```
┌─────────────────────────────────────────────────────────┐
│ 编辑用户 Edit User │
├─────────────────────────────────────────────────────────┤
│ 👤 基本信息 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户名: [输入框] *必填 │ │
│ │ 邮箱: [输入框] *必填 │ │
│ │ 姓名: [输入框] 显示名称 │ │
│ │ 头像: [选择图片] [上传新头像] │ │
│ │ 角色: [下拉选择] 管理员/编辑者/用户/访客 │ │
│ │ 状态: [开关] 启用/禁用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 🔐 权限设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 照片管理: [✅] 查看 [✅] 创建 [✅] 编辑 [❌] 删除 │ │
│ │ 分类管理: [✅] 查看 [✅] 创建 [❌] 编辑 [❌] 删除 │ │
│ │ 标签管理: [✅] 查看 [✅] 创建 [❌] 编辑 [❌] 删除 │ │
│ │ 用户管理: [❌] 查看 [❌] 创建 [❌] 编辑 [❌] 删除 │ │
│ │ 系统设置: [❌] 查看 [❌] 编辑 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 📊 用户统计 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 注册时间: 2024-01-15 │ │
│ │ 最后登录: 2024-01-20 14:30 │ │
│ │ 登录次数: 25次 │ │
│ │ 上传照片: 45张 │ │
│ │ 创建分类: 3个 │ │
│ │ 创建标签: 12个 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 💾 操作按钮 │
│ [💾 保存] [🔄 重置密码] [🚫 禁用用户] [❌ 取消] │
└─────────────────────────────────────────────────────────┘
```
### 2.6 系统设置模块 (System Settings)
#### 2.6.1 网站配置
```
┌─────────────────────────────────────────────────────────┐
│ 系统设置 System Settings │
├─────────────────────────────────────────────────────────┤
│ 🌐 网站配置 📁 文件设置 🎨 主题设置 💾 缓存设置 │
├─────────────────────────────────────────────────────────┤
│ 🌐 网站基本信息 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 网站名称: [输入框] 摄影作品集 │ │
│ │ 网站描述: [文本框] 专业摄影作品展示平台 │ │
│ │ 网站关键词: [标签输入] 摄影,作品集,艺术 │ │
│ │ 网站Logo: [选择图片] [上传新Logo] │ │
│ │ 网站图标: [选择图片] [上传Favicon] │ │
│ │ 联系邮箱: [输入框] admin@photography.com │ │
│ │ 版权信息: [输入框] © 2024 Photography Portfolio │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 📁 文件上传设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 允许格式: [多选] JPG PNG GIF WEBP RAW TIFF │ │
│ │ 最大大小: [数字输入] 50 MB │ │
│ │ 图片质量: [滑块] 85% │ │
│ │ 缩略图尺寸: [输入框] 300x300 │ │
│ │ 水印设置: [开关] 启用 [文本/图片] [位置选择] │ │
│ │ 自动压缩: [开关] 启用 │ │
│ │ 存储方式: [单选] 本地存储/MinIO/AWS S3 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 🎨 主题设置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 默认主题: [下拉选择] 浅色主题 │ │
│ │ 主色调: [颜色选择器] #3b82f6 │ │
│ │ 辅助色: [颜色选择器] #6b7280 │ │
│ │ 背景色: [颜色选择器] #ffffff │ │
│ │ 字体设置: [下拉选择] 系统默认 │ │
│ │ 布局样式: [单选] 网格布局/列表布局/瀑布流 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 2.7 日志管理模块 (Log Management)
#### 2.7.1 日志查看器
```
┌─────────────────────────────────────────────────────────┐
│ 日志管理 Log Management │
├─────────────────────────────────────────────────────────┤
│ 🔍 [搜索] 📊 [级别筛选] 📅 [时间范围] 🔄 [自动刷新] │
├─────────────────────────────────────────────────────────┤
│ 时间 │ 级别 │ 模块 │ 消息 │
│ ───────────┼─────┼────────┼─────────────────────── │
│ 14:30:25 │ INFO │ Upload │ 文件上传成功: IMG_001.jpg │
│ 14:30:20 │ WARN │ Cache │ Redis连接超时使用备用缓存 │
│ 14:30:15 │ ERROR│ DB │ 数据库查询超时 │
│ 14:30:10 │ DEBUG│ API │ GET /api/photos 200 │
│ 14:30:05 │ INFO │ Auth │ 用户admin登录成功 │
│ 14:30:00 │ INFO │ System │ 系统启动完成 │
├─────────────────────────────────────────────────────────┤
│ 📊 统计信息 │
│ 总计: 1,234条 | 错误: 12条 | 警告: 45条 | 信息: 1,177条 │
└─────────────────────────────────────────────────────────┘
```
## 3. 技术实现方案
### 3.1 前端架构
#### 3.1.1 项目结构
```
admin/
├── src/
│ ├── components/ # 通用组件
│ │ ├── Layout/ # 布局组件
│ │ ├── Form/ # 表单组件
│ │ ├── Table/ # 表格组件
│ │ ├── Upload/ # 上传组件
│ │ └── Chart/ # 图表组件
│ ├── pages/ # 页面组件
│ │ ├── Dashboard/ # 仪表板
│ │ ├── Photos/ # 照片管理
│ │ ├── Categories/ # 分类管理
│ │ ├── Tags/ # 标签管理
│ │ ├── Users/ # 用户管理
│ │ ├── Settings/ # 系统设置
│ │ └── Logs/ # 日志管理
│ ├── services/ # API服务
│ ├── stores/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript类型
│ └── styles/ # 样式文件
├── public/ # 静态资源
├── package.json
├── tsconfig.json
└── tailwind.config.js
```
#### 3.1.2 核心依赖
```json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"typescript": "^4.9.0",
"tailwindcss": "^3.2.0",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-select": "^1.2.0",
"lucide-react": "^0.263.0",
"zustand": "^4.3.0",
"axios": "^1.3.0",
"react-query": "^3.39.0",
"react-hook-form": "^7.43.0",
"zod": "^3.20.0",
"recharts": "^2.5.0",
"react-dropzone": "^14.2.0"
}
}
```
### 3.2 后端API设计
#### 3.2.1 RESTful API 结构
```
/api/admin/
├── auth/ # 认证相关
│ ├── POST /login # 登录
│ ├── POST /logout # 登出
│ ├── POST /refresh # 刷新token
│ └── GET /profile # 用户信息
├── dashboard/ # 仪表板
│ ├── GET /stats # 统计数据
│ └── GET /activities # 近期活动
├── photos/ # 照片管理
│ ├── GET / # 照片列表
│ ├── POST / # 创建照片
│ ├── GET /:id # 照片详情
│ ├── PUT /:id # 更新照片
│ ├── DELETE /:id # 删除照片
│ ├── POST /upload # 上传文件
│ └── POST /batch # 批量操作
├── categories/ # 分类管理
│ ├── GET / # 分类列表
│ ├── POST / # 创建分类
│ ├── GET /:id # 分类详情
│ ├── PUT /:id # 更新分类
│ ├── DELETE /:id # 删除分类
│ └── PUT /reorder # 重新排序
├── tags/ # 标签管理
│ ├── GET / # 标签列表
│ ├── POST / # 创建标签
│ ├── GET /:id # 标签详情
│ ├── PUT /:id # 更新标签
│ ├── DELETE /:id # 删除标签
│ └── GET /suggestions # 标签建议
├── users/ # 用户管理
│ ├── GET / # 用户列表
│ ├── POST / # 创建用户
│ ├── GET /:id # 用户详情
│ ├── PUT /:id # 更新用户
│ ├── DELETE /:id # 删除用户
│ └── PUT /:id/password # 修改密码
├── settings/ # 系统设置
│ ├── GET / # 获取设置
│ ├── PUT / # 更新设置
│ └── POST /test # 测试配置
└── logs/ # 日志管理
├── GET / # 日志列表
└── GET /stats # 日志统计
```
#### 3.2.2 数据模型定义
```go
// 照片模型
type Photo struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Filename string `json:"filename" gorm:"size:255;not null"`
FilePath string `json:"file_path" gorm:"size:500;not null"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type" gorm:"size:100"`
Width int `json:"width"`
Height int `json:"height"`
CategoryID uint `json:"category_id"`
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
Tags []Tag `json:"tags" gorm:"many2many:photo_tags;"`
EXIF string `json:"exif" gorm:"type:jsonb"`
TakenAt time.Time `json:"taken_at"`
Location string `json:"location" gorm:"size:255"`
IsPublic bool `json:"is_public" gorm:"default:true"`
Status string `json:"status" gorm:"size:20;default:'draft'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 分类模型
type Category struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:100;not null"`
Description string `json:"description" gorm:"type:text"`
ParentID *uint `json:"parent_id"`
Parent *Category `json:"parent" gorm:"foreignKey:ParentID"`
Children []Category `json:"children" gorm:"foreignKey:ParentID"`
Color string `json:"color" gorm:"size:7;default:'#3b82f6'"`
CoverImage string `json:"cover_image" gorm:"size:500"`
Sort int `json:"sort" gorm:"default:0"`
IsActive bool `json:"is_active" gorm:"default:true"`
PhotoCount int `json:"photo_count" gorm:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 标签模型
type Tag struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:50;not null;unique"`
Color string `json:"color" gorm:"size:7;default:'#6b7280'"`
UseCount int `json:"use_count" gorm:"default:0"`
IsActive bool `json:"is_active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 用户模型
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"size:50;not null;unique"`
Email string `json:"email" gorm:"size:100;not null;unique"`
Password string `json:"-" gorm:"size:255;not null"`
Name string `json:"name" gorm:"size:100"`
Avatar string `json:"avatar" gorm:"size:500"`
Role string `json:"role" gorm:"size:20;default:'user'"`
IsActive bool `json:"is_active" gorm:"default:true"`
LastLogin time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
### 3.3 数据库设计
#### 3.3.1 核心表结构
```sql
-- 照片表
CREATE TABLE photos (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
filename VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
mime_type VARCHAR(100),
width INTEGER,
height INTEGER,
category_id INTEGER REFERENCES categories(id),
exif JSONB,
taken_at TIMESTAMP,
location VARCHAR(255),
is_public BOOLEAN DEFAULT true,
status VARCHAR(20) DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 分类表
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
parent_id INTEGER REFERENCES categories(id),
color VARCHAR(7) DEFAULT '#3b82f6',
cover_image VARCHAR(500),
sort INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 标签表
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
color VARCHAR(7) DEFAULT '#6b7280',
use_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 照片标签关联表
CREATE TABLE photo_tags (
photo_id INTEGER REFERENCES photos(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (photo_id, tag_id)
);
-- 用户表
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
name VARCHAR(100),
avatar VARCHAR(500),
role VARCHAR(20) DEFAULT 'user',
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 系统设置表
CREATE TABLE settings (
id SERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
value TEXT,
type VARCHAR(20) DEFAULT 'string',
group_name VARCHAR(50),
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 操作日志表
CREATE TABLE activity_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(50),
resource_id INTEGER,
details JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### 3.4 认证与权限
#### 3.4.1 JWT认证流程
```
1. 用户登录 → 验证账号密码
2. 生成JWT Token → 包含用户信息和权限
3. 前端存储Token → localStorage/sessionStorage
4. 请求携带Token → Authorization Header
5. 后端验证Token → 中间件验证
6. 权限检查 → 基于角色的访问控制
```
#### 3.4.2 角色权限定义
```go
// 角色定义
const (
RoleAdmin = "admin" // 管理员 - 所有权限
RoleEditor = "editor" // 编辑者 - 内容管理权限
RoleUser = "user" // 用户 - 基本权限
RoleGuest = "guest" // 访客 - 只读权限
)
// 权限定义
type Permission struct {
Resource string // 资源类型
Actions []string // 允许的操作
}
// 角色权限映射
var RolePermissions = map[string][]Permission{
RoleAdmin: {
{Resource: "photos", Actions: []string{"create", "read", "update", "delete"}},
{Resource: "categories", Actions: []string{"create", "read", "update", "delete"}},
{Resource: "tags", Actions: []string{"create", "read", "update", "delete"}},
{Resource: "users", Actions: []string{"create", "read", "update", "delete"}},
{Resource: "settings", Actions: []string{"read", "update"}},
{Resource: "logs", Actions: []string{"read"}},
},
RoleEditor: {
{Resource: "photos", Actions: []string{"create", "read", "update", "delete"}},
{Resource: "categories", Actions: []string{"create", "read", "update"}},
{Resource: "tags", Actions: []string{"create", "read", "update"}},
},
RoleUser: {
{Resource: "photos", Actions: []string{"read"}},
{Resource: "categories", Actions: []string{"read"}},
{Resource: "tags", Actions: []string{"read"}},
},
RoleGuest: {
{Resource: "photos", Actions: []string{"read"}},
{Resource: "categories", Actions: []string{"read"}},
},
}
```
### 3.5 文件上传与处理
#### 3.5.1 上传流程
```
1. 前端选择文件 → 文件验证 (格式、大小)
2. 创建上传任务 → 生成临时文件路径
3. 分块上传 → 支持断点续传
4. 文件合并 → 合并所有分块
5. 图片处理 → 生成缩略图、提取EXIF
6. 存储文件 → 本地存储或云存储
7. 更新数据库 → 保存文件信息
8. 返回结果 → 上传成功确认
```
#### 3.5.2 图片处理
```go
// 图片处理服务
type ImageProcessor struct {
storage StorageService
thumbnails []ThumbnailConfig
watermark WatermarkConfig
quality int
}
// 缩略图配置
type ThumbnailConfig struct {
Name string
Width int
Height int
Crop bool
}
// 处理上传的图片
func (p *ImageProcessor) ProcessImage(file *multipart.FileHeader) (*ImageResult, error) {
// 1. 验证文件格式
if !p.isValidImage(file) {
return nil, ErrInvalidImageFormat
}
// 2. 打开图片
img, err := bimg.NewFromFile(file.Filename)
if err != nil {
return nil, err
}
// 3. 提取EXIF信息
exif, err := p.extractEXIF(img)
if err != nil {
logger.Warn("Failed to extract EXIF", "error", err)
}
// 4. 生成缩略图
thumbnails := make(map[string]string)
for _, config := range p.thumbnails {
thumbnail, err := p.createThumbnail(img, config)
if err != nil {
logger.Error("Failed to create thumbnail", "config", config, "error", err)
continue
}
thumbnails[config.Name] = thumbnail
}
// 5. 添加水印 (可选)
if p.watermark.Enabled {
img, err = p.addWatermark(img, p.watermark)
if err != nil {
logger.Warn("Failed to add watermark", "error", err)
}
}
// 6. 保存原图
processed, err := img.Process(bimg.Options{
Quality: p.quality,
Type: bimg.JPEG,
})
if err != nil {
return nil, err
}
// 7. 上传到存储
originalPath, err := p.storage.Save(processed, "originals")
if err != nil {
return nil, err
}
return &ImageResult{
OriginalPath: originalPath,
Thumbnails: thumbnails,
EXIF: exif,
Width: img.Size().Width,
Height: img.Size().Height,
}, nil
}
```
### 3.6 性能优化
#### 3.6.1 前端优化
```javascript
// 1. 代码分割
const LazyPhotoManagement = lazy(() => import('./pages/Photos'));
const LazyDashboard = lazy(() => import('./pages/Dashboard'));
// 2. 虚拟滚动 (大量照片列表)
import { FixedSizeGrid } from 'react-window';
// 3. 图片懒加载
const LazyImage = ({ src, alt, ...props }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} {...props}>
{isInView && (
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
)}
</div>
);
};
// 4. 搜索防抖
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
```
#### 3.6.2 后端优化
```go
// 1. 数据库查询优化
func (r *PhotoRepository) GetPhotosWithPagination(page, limit int, filters PhotoFilters) ([]Photo, int, error) {
var photos []Photo
var total int64
query := r.db.Model(&Photo{}).
Preload("Category").
Preload("Tags")
// 添加筛选条件
if filters.CategoryID > 0 {
query = query.Where("category_id = ?", filters.CategoryID)
}
if len(filters.Tags) > 0 {
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
Where("photo_tags.tag_id IN (?)", filters.Tags)
}
if filters.Search != "" {
query = query.Where("title ILIKE ? OR description ILIKE ?",
"%"+filters.Search+"%", "%"+filters.Search+"%")
}
// 计算总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).
Order("created_at DESC").
Find(&photos).Error
return photos, int(total), err
}
// 2. Redis缓存
func (s *PhotoService) GetPhotosByCategory(categoryID uint) ([]Photo, error) {
cacheKey := fmt.Sprintf("photos:category:%d", categoryID)
// 尝试从缓存获取
cached, err := s.cache.Get(cacheKey)
if err == nil {
var photos []Photo
if err := json.Unmarshal(cached, &photos); err == nil {
return photos, nil
}
}
// 从数据库获取
photos, err := s.repo.GetPhotosByCategory(categoryID)
if err != nil {
return nil, err
}
// 缓存结果
if data, err := json.Marshal(photos); err == nil {
s.cache.Set(cacheKey, data, 10*time.Minute)
}
return photos, nil
}
// 3. 并发处理
func (s *PhotoService) ProcessBatchUpload(files []*multipart.FileHeader) error {
const maxWorkers = 5
jobs := make(chan *multipart.FileHeader, len(files))
results := make(chan error, len(files))
// 启动工作协程
for i := 0; i < maxWorkers; i++ {
go func() {
for file := range jobs {
err := s.ProcessSingleUpload(file)
results <- err
}
}()
}
// 发送任务
for _, file := range files {
jobs <- file
}
close(jobs)
// 收集结果
var errors []error
for i := 0; i < len(files); i++ {
if err := <-results; err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return fmt.Errorf("批量上传失败: %v", errors)
}
return nil
}
```
## 4. 部署与监控
### 4.1 Docker部署
#### 4.1.1 Dockerfile
```dockerfile
# 前端构建
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY admin/package*.json ./
RUN npm ci
COPY admin/ .
RUN npm run build
# 后端构建
FROM golang:1.21-alpine AS backend-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main cmd/server/main.go
# 生产镜像
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
COPY --from=backend-builder /app/main .
COPY --from=frontend-builder /app/dist ./web
COPY config/ ./config/
COPY migrations/ ./migrations/
EXPOSE 8080
CMD ["./main"]
```
#### 4.1.2 docker-compose.yml
```yaml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- postgres
- redis
environment:
- DATABASE_URL=postgres://user:password@postgres:5432/photography
- REDIS_URL=redis://redis:6379
volumes:
- uploads:/app/uploads
- logs:/app/logs
postgres:
image: postgres:15
environment:
POSTGRES_DB: photography
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
- uploads:/var/www/uploads
volumes:
postgres_data:
redis_data:
uploads:
logs:
```
### 4.2 监控与日志
#### 4.2.1 健康检查
```go
// 健康检查端点
func (h *HealthHandler) CheckHealth(c *gin.Context) {
health := map[string]interface{}{
"status": "ok",
"timestamp": time.Now(),
"services": map[string]interface{}{},
}
// 检查数据库连接
if err := h.db.Raw("SELECT 1").Error; err != nil {
health["services"]["database"] = map[string]interface{}{
"status": "error",
"error": err.Error(),
}
health["status"] = "error"
} else {
health["services"]["database"] = map[string]interface{}{
"status": "ok",
}
}
// 检查Redis连接
if err := h.redis.Ping().Err(); err != nil {
health["services"]["redis"] = map[string]interface{}{
"status": "error",
"error": err.Error(),
}
health["status"] = "error"
} else {
health["services"]["redis"] = map[string]interface{}{
"status": "ok",
}
}
// 检查存储服务
if err := h.storage.HealthCheck(); err != nil {
health["services"]["storage"] = map[string]interface{}{
"status": "error",
"error": err.Error(),
}
health["status"] = "error"
} else {
health["services"]["storage"] = map[string]interface{}{
"status": "ok",
}
}
if health["status"] == "ok" {
c.JSON(http.StatusOK, health)
} else {
c.JSON(http.StatusServiceUnavailable, health)
}
}
```
#### 4.2.2 日志配置
```go
// 日志配置
func InitLogger(config *Config) *logrus.Logger {
logger := logrus.New()
// 设置日志级别
level, err := logrus.ParseLevel(config.Logger.Level)
if err != nil {
level = logrus.InfoLevel
}
logger.SetLevel(level)
// 设置日志格式
if config.Logger.Format == "json" {
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "timestamp",
logrus.FieldKeyLevel: "level",
logrus.FieldKeyMsg: "message",
},
})
} else {
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
}
// 设置日志输出
if config.Logger.Output == "file" {
logFile := &lumberjack.Logger{
Filename: config.Logger.Filename,
MaxSize: config.Logger.MaxSize,
MaxAge: config.Logger.MaxAge,
MaxBackups: config.Logger.MaxBackups,
LocalTime: true,
Compress: config.Logger.Compress,
}
logger.SetOutput(logFile)
}
return logger
}
```
## 5. 测试策略
### 5.1 前端测试
#### 5.1.1 组件测试
```typescript
// PhotoCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { PhotoCard } from './PhotoCard';
import { Photo } from '../types';
const mockPhoto: Photo = {
id: 1,
title: 'Test Photo',
description: 'Test description',
filename: 'test.jpg',
thumbnailUrl: '/thumbnails/test.jpg',
category: { id: 1, name: 'Test Category' },
tags: [{ id: 1, name: 'test' }],
createdAt: '2024-01-01T00:00:00Z',
};
describe('PhotoCard', () => {
it('renders photo information correctly', () => {
render(<PhotoCard photo={mockPhoto} />);
expect(screen.getByText('Test Photo')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
expect(screen.getByText('Test Category')).toBeInTheDocument();
expect(screen.getByText('test')).toBeInTheDocument();
});
it('calls onEdit when edit button is clicked', () => {
const mockOnEdit = jest.fn();
render(<PhotoCard photo={mockPhoto} onEdit={mockOnEdit} />);
fireEvent.click(screen.getByText('编辑'));
expect(mockOnEdit).toHaveBeenCalledWith(mockPhoto);
});
it('calls onDelete when delete button is clicked', () => {
const mockOnDelete = jest.fn();
render(<PhotoCard photo={mockPhoto} onDelete={mockOnDelete} />);
fireEvent.click(screen.getByText('删除'));
expect(mockOnDelete).toHaveBeenCalledWith(mockPhoto.id);
});
});
```
#### 5.1.2 API测试
```typescript
// photoService.test.ts
import { photoService } from './photoService';
import { mockApi } from '../test/mocks';
describe('PhotoService', () => {
beforeEach(() => {
mockApi.reset();
});
it('fetches photos successfully', async () => {
const mockPhotos = [
{ id: 1, title: 'Photo 1' },
{ id: 2, title: 'Photo 2' },
];
mockApi.onGet('/api/admin/photos').reply(200, {
code: 0,
data: { photos: mockPhotos, total: 2 },
});
const result = await photoService.getPhotos();
expect(result.photos).toEqual(mockPhotos);
expect(result.total).toBe(2);
});
it('handles upload with progress', async () => {
const mockFile = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
const mockProgressCallback = jest.fn();
mockApi.onPost('/api/admin/photos/upload').reply(200, {
code: 0,
data: { id: 1, filename: 'test.jpg' },
});
const result = await photoService.uploadPhoto(mockFile, mockProgressCallback);
expect(result.id).toBe(1);
expect(mockProgressCallback).toHaveBeenCalled();
});
});
```
### 5.2 后端测试
#### 5.2.1 单元测试
```go
// photo_service_test.go
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"photography-backend/internal/domain"
"photography-backend/internal/repository/mocks"
)
func TestPhotoService_CreatePhoto(t *testing.T) {
// 准备测试数据
mockRepo := new(mocks.PhotoRepository)
mockStorage := new(mocks.StorageService)
service := NewPhotoService(mockRepo, mockStorage)
photo := &domain.Photo{
Title: "Test Photo",
Description: "Test description",
CategoryID: 1,
}
// 设置mock期望
mockRepo.On("Create", mock.AnythingOfType("*domain.Photo")).Return(nil).Run(func(args mock.Arguments) {
p := args.Get(0).(*domain.Photo)
p.ID = 1
})
// 执行测试
result, err := service.CreatePhoto(photo)
// 验证结果
assert.NoError(t, err)
assert.Equal(t, uint(1), result.ID)
assert.Equal(t, "Test Photo", result.Title)
mockRepo.AssertExpectations(t)
}
func TestPhotoService_GetPhotosByCategory(t *testing.T) {
mockRepo := new(mocks.PhotoRepository)
mockStorage := new(mocks.StorageService)
service := NewPhotoService(mockRepo, mockStorage)
expectedPhotos := []domain.Photo{
{ID: 1, Title: "Photo 1", CategoryID: 1},
{ID: 2, Title: "Photo 2", CategoryID: 1},
}
mockRepo.On("GetByCategory", uint(1)).Return(expectedPhotos, nil)
result, err := service.GetPhotosByCategory(1)
assert.NoError(t, err)
assert.Len(t, result, 2)
assert.Equal(t, expectedPhotos[0].Title, result[0].Title)
mockRepo.AssertExpectations(t)
}
```
#### 5.2.2 集成测试
```go
// photo_api_test.go
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"photography-backend/internal/domain"
"photography-backend/test/testutils"
)
func TestPhotoAPI_GetPhotos(t *testing.T) {
// 设置测试数据库
db := testutils.SetupTestDB()
defer testutils.CleanupTestDB(db)
// 插入测试数据
testutils.SeedPhotos(db)
// 创建测试路由
router := gin.New()
photoHandler := NewPhotoHandler(db)
router.GET("/api/admin/photos", photoHandler.GetPhotos)
// 创建测试请求
req, _ := http.NewRequest("GET", "/api/admin/photos?page=1&limit=10", nil)
req.Header.Set("Authorization", "Bearer "+testutils.GetTestToken())
// 执行请求
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// 验证响应
assert.Equal(t, http.StatusOK, w.Code)
var response struct {
Code int `json:"code"`
Data struct {
Photos []domain.Photo `json:"photos"`
Total int `json:"total"`
} `json:"data"`
}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, 0, response.Code)
assert.Greater(t, response.Data.Total, 0)
assert.Greater(t, len(response.Data.Photos), 0)
}
func TestPhotoAPI_CreatePhoto(t *testing.T) {
db := testutils.SetupTestDB()
defer testutils.CleanupTestDB(db)
router := gin.New()
photoHandler := NewPhotoHandler(db)
router.POST("/api/admin/photos", photoHandler.CreatePhoto)
photoData := map[string]interface{}{
"title": "Test Photo",
"description": "Test description",
"category_id": 1,
}
jsonData, _ := json.Marshal(photoData)
req, _ := http.NewRequest("POST", "/api/admin/photos", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+testutils.GetTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response struct {
Code int `json:"code"`
Data domain.Photo `json:"data"`
}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, 0, response.Code)
assert.Equal(t, "Test Photo", response.Data.Title)
}
```
## 6. 文档与维护
### 6.1 API文档
#### 6.1.1 OpenAPI规范
```yaml
openapi: 3.0.0
info:
title: 摄影作品集管理后台API
version: 1.0.0
description: 摄影作品集网站管理后台的RESTful API
paths:
/api/admin/photos:
get:
summary: 获取照片列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
- name: category_id
in: query
schema:
type: integer
- name: tags
in: query
schema:
type: array
items:
type: integer
- name: search
in: query
schema:
type: string
responses:
200:
description: 成功获取照片列表
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 0
message:
type: string
example: success
data:
type: object
properties:
photos:
type: array
items:
$ref: '#/components/schemas/Photo'
total:
type: integer
example: 100
page:
type: integer
example: 1
limit:
type: integer
example: 20
post:
summary: 创建照片
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PhotoCreate'
responses:
201:
description: 成功创建照片
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 0
message:
type: string
example: success
data:
$ref: '#/components/schemas/Photo'
components:
schemas:
Photo:
type: object
properties:
id:
type: integer
example: 1
title:
type: string
example: "Beautiful Sunset"
description:
type: string
example: "A beautiful sunset over the ocean"
filename:
type: string
example: "sunset.jpg"
file_path:
type: string
example: "/uploads/2024/01/sunset.jpg"
file_size:
type: integer
example: 1024000
width:
type: integer
example: 1920
height:
type: integer
example: 1080
category:
$ref: '#/components/schemas/Category'
tags:
type: array
items:
$ref: '#/components/schemas/Tag'
created_at:
type: string
format: date-time
example: "2024-01-01T00:00:00Z"
updated_at:
type: string
format: date-time
example: "2024-01-01T00:00:00Z"
PhotoCreate:
type: object
required:
- title
- category_id
properties:
title:
type: string
example: "Beautiful Sunset"
description:
type: string
example: "A beautiful sunset over the ocean"
category_id:
type: integer
example: 1
tag_ids:
type: array
items:
type: integer
example: [1, 2, 3]
Category:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: "Landscape"
description:
type: string
example: "Landscape photography"
parent_id:
type: integer
nullable: true
example: null
color:
type: string
example: "#3b82f6"
is_active:
type: boolean
example: true
Tag:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: "sunset"
color:
type: string
example: "#f59e0b"
use_count:
type: integer
example: 25
is_active:
type: boolean
example: true
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
```
### 6.2 部署文档
#### 6.2.1 生产环境部署
```bash
#!/bin/bash
# deploy.sh - 生产环境部署脚本
set -e
echo "🚀 开始部署摄影作品集管理后台..."
# 1. 拉取最新代码
echo "📥 拉取最新代码..."
git pull origin main
# 2. 构建前端
echo "🔨 构建前端..."
cd admin
npm install
npm run build
cd ..
# 3. 构建后端
echo "🔨 构建后端..."
go mod tidy
go build -o photography-backend cmd/server/main.go
# 4. 数据库迁移
echo "🗄️ 执行数据库迁移..."
./photography-backend migrate
# 5. 重启服务
echo "🔄 重启服务..."
sudo systemctl restart photography-backend
# 6. 检查服务状态
echo "🔍 检查服务状态..."
sleep 5
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
echo "✅ 部署成功!"
else
echo "❌ 部署失败!"
exit 1
fi
echo "🎉 部署完成!"
```
#### 6.2.2 环境配置
```yaml
# config/production.yaml
app:
name: "photography-backend"
version: "1.0.0"
environment: "production"
port: 8080
debug: false
database:
host: "localhost"
port: 5432
username: "photography"
password: "${DB_PASSWORD}"
database: "photography"
ssl_mode: "require"
max_open_conns: 100
max_idle_conns: 10
conn_max_lifetime: 300
redis:
host: "localhost"
port: 6379
password: "${REDIS_PASSWORD}"
database: 0
pool_size: 100
min_idle_conns: 10
storage:
type: "s3"
s3:
region: "us-west-2"
bucket: "photography-uploads"
access_key: "${S3_ACCESS_KEY}"
secret_key: "${S3_SECRET_KEY}"
endpoint: ""
use_ssl: true
logger:
level: "info"
format: "json"
output: "file"
filename: "logs/app.log"
max_size: 100
max_age: 30
compress: true
jwt:
secret: "${JWT_SECRET}"
expire_hours: 24
upload:
max_file_size: 52428800 # 50MB
allowed_types: ["image/jpeg", "image/png", "image/gif", "image/webp"]
thumbnail_sizes:
- { name: "small", width: 300, height: 300 }
- { name: "medium", width: 800, height: 600 }
- { name: "large", width: 1200, height: 900 }
```
## 7. 总结
摄影作品集管理后台是一个功能完善的内容管理系统涵盖了现代Web应用所需的各个方面
### 7.1 核心特性
- **直观的用户界面**: 基于React和shadcn/ui的现代化界面
- **强大的照片管理**: 支持批量上传、处理、分类和标签
- **灵活的权限系统**: 基于角色的访问控制
- **高性能优化**: 多级缓存、懒加载、虚拟滚动等
- **完善的监控**: 日志管理、健康检查、性能监控
### 7.2 技术优势
- **前后端分离**: 便于独立开发和部署
- **类型安全**: TypeScript确保代码质量
- **模块化设计**: 易于维护和扩展
- **云原生部署**: Docker容器化部署
- **自动化流程**: CI/CD集成部署
### 7.3 扩展性
- **微服务架构**: 支持后续拆分为微服务
- **多存储支持**: 本地存储、MinIO、AWS S3
- **插件系统**: 支持功能插件扩展
- **API标准化**: RESTful API设计
这个管理后台为摄影作品集网站提供了强大的后台管理能力,能够满足专业摄影师和摄影工作室的各种需求。通过模块化的设计和现代化的技术栈,系统具有良好的可维护性和扩展性,能够随着业务需求的增长而持续演进。