Files
photography/docs/technical/saved-docs/management-backend.md
iriver ff20f6f23a feat: 添加产品经理和全栈开发角色资源文件
初始化产品经理和全栈开发角色的相关资源文件,包括角色定义、知识库、思维模式和执行流程文档
2025-07-21 22:47:16 +08:00

73 KiB
Raw Permalink Blame History

摄影作品集网站 - 管理后台开发文档

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设计

这个管理后台为摄影作品集网站提供了强大的后台管理能力,能够满足专业摄影师和摄影工作室的各种需求。通过模块化的设计和现代化的技术栈,系统具有良好的可维护性和扩展性,能够随着业务需求的增长而持续演进。