# 摄影作品集网站 - 管理后台开发文档 ## 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 (
{isInView && ( {alt} setIsLoaded(true)} style={{ opacity: isLoaded ? 1 : 0 }} /> )}
); }; // 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(); 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(); fireEvent.click(screen.getByText('编辑')); expect(mockOnEdit).toHaveBeenCalledWith(mockPhoto); }); it('calls onDelete when delete button is clicked', () => { const mockOnDelete = jest.fn(); render(); 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设计 这个管理后台为摄影作品集网站提供了强大的后台管理能力,能够满足专业摄影师和摄影工作室的各种需求。通过模块化的设计和现代化的技术栈,系统具有良好的可维护性和扩展性,能够随着业务需求的增长而持续演进。