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