73 KiB
73 KiB
摄影作品集网站 - 管理后台开发文档
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 数据接口
// 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 核心依赖
{
"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 数据模型定义
// 照片模型
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 核心表结构
-- 照片表
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 角色权限定义
// 角色定义
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 图片处理
// 图片处理服务
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 前端优化
// 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 后端优化
// 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
# 前端构建
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
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 健康检查
// 健康检查端点
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 日志配置
// 日志配置
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 组件测试
// 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测试
// 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 单元测试
// 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 集成测试
// 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规范
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 生产环境部署
#!/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 环境配置
# 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设计
这个管理后台为摄影作品集网站提供了强大的后台管理能力,能够满足专业摄影师和摄影工作室的各种需求。通过模块化的设计和现代化的技术栈,系统具有良好的可维护性和扩展性,能够随着业务需求的增长而持续演进。