From 21b1581bdb6f916b96f5a5bc01f3a41fe06a83cb Mon Sep 17 00:00:00 2001 From: xujiang Date: Wed, 9 Jul 2025 12:41:16 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=8D=E6=9E=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E6=8C=89=E7=89=88=E6=9C=AC=E5=88=92?= =?UTF-8?q?=E5=88=86=E7=BB=84=E7=BB=87=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要变更 - 创建版本化文档目录结构 (v1/, v2/) - 移动核心设计文档到对应版本目录 - 更新文档总览和版本说明 - 保留原有目录结构的兼容性 ## 新增文档 - docs/v1/README.md - v1.0版本开发指南 - docs/v2/README.md - v2.0版本规划文档 - docs/v1/admin/管理后台开发文档.md - docs/v1/backend/Golang项目架构文档.md - docs/v1/database/数据库设计文档.md - docs/v1/api/API接口设计文档.md ## 文档结构优化 - 清晰的版本划分,便于开发者快速定位 - 完整的开发进度跟踪 - 详细的技术栈说明和架构设计 - 未来版本功能规划和技术演进路径 ## 开发者体验提升 - 角色导向的文档导航 - 快速开始指南 - 详细的API和数据库设计文档 - 版本化管理便于迭代开发 --- docs/README.md | 236 +++- docs/v1/README.md | 289 ++++ docs/v1/admin/管理后台开发文档.md | 1460 ++++++++++++++++++++ docs/v1/api/API接口设计文档.md | 1569 +++++++++++++++++++++ docs/v1/backend/Golang项目架构文档.md | 1844 +++++++++++++++++++++++++ docs/v1/database/数据库设计文档.md | 1699 +++++++++++++++++++++++ docs/v2/README.md | 440 ++++++ 7 files changed, 7502 insertions(+), 35 deletions(-) create mode 100644 docs/v1/README.md create mode 100644 docs/v1/admin/管理后台开发文档.md create mode 100644 docs/v1/api/API接口设计文档.md create mode 100644 docs/v1/backend/Golang项目架构文档.md create mode 100644 docs/v1/database/数据库设计文档.md create mode 100644 docs/v2/README.md diff --git a/docs/README.md b/docs/README.md index 375d014..9403749 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,46 +1,212 @@ -# 产品文档 +# 摄影作品集网站 - 文档总览 -本目录包含摄影作品集项目的完整产品文档。 +## 📚 文档结构 -## 文档结构 +本项目采用版本化的文档管理策略,按照功能迭代分为不同版本,便于开发和维护。 -### 📐 Design(设计文档) -- UI/UX 设计规范 -- 组件设计系统 -- 交互设计文档 -- 品牌指南和视觉规范 +``` +docs/ +├── README.md # 文档总览(当前文件) +├── v1/ # v1.0 版本文档 +│ ├── admin/ # 管理后台相关 +│ ├── backend/ # 后端架构相关 +│ ├── database/ # 数据库设计相关 +│ ├── api/ # API接口相关 +│ └── README.md # v1版本说明 +├── v2/ # v2.0 版本文档(规划中) +│ ├── admin/ # 管理后台升级 +│ ├── backend/ # 后端微服务化 +│ ├── database/ # 数据库优化 +│ ├── api/ # GraphQL接口 +│ └── README.md # v2版本说明 +├── 原始 prd/ # 原始需求文档 +│ ├── UI设计需求文档.md +│ ├── 前端开发文档.md +│ ├── 后端开发文档.md +│ └── 测试需求文档.md +└── [传统目录结构] # 保留现有结构 + ├── design/ # 设计文档 + ├── api/ # API文档 + ├── user-guide/ # 用户指南 + ├── development/ # 开发文档 + └── deployment/ # 部署文档 +``` -### 🔌 API(API 文档) -- API 接口文档 -- 数据结构定义 -- 接口调用示例 -- 错误码说明 +## 🚀 版本规划 -### 👥 User Guide(用户指南) -- 用户操作手册 -- 功能使用说明 -- 常见问题解答 -- 最佳实践指南 +### v1.0 - 核心功能版本 (当前开发) -### 🛠️ Development(开发文档) -- 开发环境搭建 -- 代码规范和约定 -- 开发工作流程 -- 测试指南 +**目标**: 实现完整的摄影作品集网站核心功能 -### 🚀 Deployment(部署文档) -- 部署配置说明 -- 环境变量配置 -- 服务器配置要求 -- 部署流程和脚本 +#### 📋 功能范围 +- ✅ **前端展示**: Next.js 15 + React 19 静态网站 +- 🔄 **管理后台**: React + TypeScript 管理界面 +- 🔄 **后端API**: Golang + Gin + PostgreSQL +- 🔄 **图片处理**: 多格式转换和优化 +- 🔄 **用户管理**: JWT认证和权限控制 +- 🔄 **文件存储**: MinIO/S3 对象存储 -## 文档维护 +#### 📖 v1.0 文档 +- [管理后台开发文档](./v1/admin/管理后台开发文档.md) +- [Golang项目架构文档](./v1/backend/Golang项目架构文档.md) +- [数据库设计文档](./v1/database/数据库设计文档.md) +- [API接口设计文档](./v1/api/API接口设计文档.md) -请在相应的子目录中维护各类文档,确保文档的及时更新和准确性。 +#### 🛠️ 技术栈 +```yaml +前端: + - Next.js 15 + React 19 + - TypeScript + Tailwind CSS + - TanStack Query + Zustand -## 贡献指南 +后端: + - Golang + Gin Framework + - GORM + PostgreSQL + - Redis + MinIO/S3 -1. 在相应目录下创建或更新文档 -2. 使用 Markdown 格式编写 -3. 确保文档结构清晰、内容准确 -4. 添加适当的图片和示例代码 \ No newline at end of file +部署: + - Docker + Docker Compose + - Caddy Web Server + - Gitea Actions CI/CD +``` + +### v2.0 - 高级功能版本 (规划中) + +**目标**: 扩展高级功能,优化性能和用户体验 + +#### 🎯 规划功能 +- 🔮 **AI增强**: 自动标签、智能分类、内容推荐 +- 🔮 **社交功能**: 评论系统、点赞收藏、用户互动 +- 🔮 **高级搜索**: ElasticSearch全文搜索、地理位置搜索 +- 🔮 **性能优化**: CDN加速、图片懒加载、缓存优化 +- 🔮 **移动端**: PWA支持、移动端优化 +- 🔮 **多语言**: 国际化支持、多语言切换 + +#### 🏗️ 架构升级 +- **微服务化**: 服务拆分,独立部署 +- **GraphQL**: 统一数据查询接口 +- **消息队列**: RabbitMQ/Redis Streams +- **监控体系**: Prometheus + Grafana +- **日志中心**: ELK Stack + +## 📝 文档使用指南 + +### 开发者快速开始 + +1. **前端开发者** + ```bash + # 阅读前端相关文档 + cd docs/原始\ prd/ + # 查看 前端开发文档.md 和 UI设计需求文档.md + ``` + +2. **后端开发者** + ```bash + # 阅读v1版本后端文档 + cd docs/v1/ + # 依次阅读: + # - backend/Golang项目架构文档.md + # - database/数据库设计文档.md + # - api/API接口设计文档.md + ``` + +3. **全栈开发者** + ```bash + # 完整了解项目 + # 1. 先看原始需求: docs/原始\ prd/ + # 2. 再看具体实现: docs/v1/ + # 3. 了解未来规划: docs/v2/README.md + ``` + +### 管理员和产品经理 + +1. **项目概览**: 从 `docs/README.md` (本文件) 开始 +2. **功能规划**: 查看各版本的 README.md 了解功能范围 +3. **进度跟踪**: 根据文档中的状态标识了解开发进度 + +## 🎯 当前开发状态 + +### v1.0 开发进度 + +| 模块 | 状态 | 完成度 | 负责人 | 备注 | +|------|------|---------|--------|------| +| 前端展示 | ✅ 已完成 | 100% | - | Next.js静态网站 | +| 管理后台 | 📋 设计中 | 20% | - | React管理界面 | +| 后端API | 📋 设计中 | 15% | - | Golang架构设计 | +| 数据库 | 📋 设计中 | 30% | - | PostgreSQL表设计 | +| 图片处理 | ⏳ 待开发 | 0% | - | 多格式转换 | +| 用户认证 | ⏳ 待开发 | 0% | - | JWT + 权限 | +| 文件存储 | ⏳ 待开发 | 0% | - | MinIO/S3集成 | +| 部署配置 | ⏳ 待开发 | 0% | - | Docker + CI/CD | + +### 下一步计划 + +#### 近期计划 (1-2周) +1. **完成管理后台前端开发** + - React + TypeScript 管理界面 + - 照片上传和管理功能 + - 分类标签管理 + +2. **开始后端核心开发** + - Golang项目框架搭建 + - 数据库表创建和迁移 + - 基础API接口实现 + +#### 中期计划 (1个月) +1. **完善后端功能** + - 用户认证和权限系统 + - 图片处理和存储 + - 完整的CRUD操作 + +2. **前后端联调** + - API接口对接 + - 数据流测试 + - 功能完整性验证 + +#### 长期计划 (2-3个月) +1. **系统优化** + - 性能优化和缓存 + - 安全性加固 + - 错误处理完善 + +2. **部署上线** + - 生产环境配置 + - CI/CD流程 + - 监控和日志 + +## 📞 联系方式 + +### 项目维护者 +- **项目负责人**: [待填写] +- **技术负责人**: [待填写] +- **文档维护**: Claude Code Assistant + +### 沟通渠道 +- **技术讨论**: [GitHub Issues/Discussions] +- **进度同步**: [项目管理工具链接] +- **紧急联系**: [联系方式] + +## 📋 文档维护 + +### 更新频率 +- **设计文档**: 功能变更时及时更新 +- **API文档**: 接口变更时同步更新 +- **架构文档**: 重大架构调整时更新 +- **状态跟踪**: 每周更新开发进度 + +### 贡献指南 +1. 所有文档使用 Markdown 格式 +2. 图片和图表存放在对应的 `assets/` 目录 +3. 重大更新需要更新对应的 README.md +4. 保持文档结构清晰,便于检索 + +### 版本控制 +- 文档跟随代码版本管理 +- 重大版本升级创建新的版本目录 +- 保留历史版本文档,便于回溯 + +--- + +📅 **最后更新**: 2024-01-15 +📝 **文档版本**: v1.0 +👨‍💻 **维护者**: Claude Code Assistant \ No newline at end of file diff --git a/docs/v1/README.md b/docs/v1/README.md new file mode 100644 index 0000000..00b5ff1 --- /dev/null +++ b/docs/v1/README.md @@ -0,0 +1,289 @@ +# 摄影作品集网站 v1.0 - 开发文档 + +## 📋 v1.0 版本概述 + +v1.0 是摄影作品集网站的核心功能版本,实现了完整的摄影作品展示、管理和用户交互功能。 + +### 🎯 版本目标 +- 构建稳定可靠的摄影作品集展示平台 +- 实现高效的照片管理和处理系统 +- 提供完善的用户认证和权限控制 +- 支持多种图片格式和优化策略 + +### 📅 开发周期 +- **开始时间**: 2024-01-15 +- **预计完成**: 2024-04-15 +- **当前状态**: 设计阶段 + +## 🏗️ 架构设计 + +### 整体架构 +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户界面层 │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 前端展示网站 │ │ 管理后台界面 │ │ +│ │ (Next.js) │ │ (React) │ │ +│ └─────────────────┘ └─────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ API 接口层 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ RESTful API │ │ +│ │ (Gin Framework) │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 业务逻辑层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 照片管理 │ │ 用户认证 │ │ 文件处理 │ │ +│ │ 服务 │ │ 服务 │ │ 服务 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 数据访问层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ MinIO/S3 │ │ +│ │ 数据库 │ │ 缓存 │ │ 对象存储 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 技术选型 +```yaml +前端技术栈: + - Framework: Next.js 15 + React 19 + - Language: TypeScript + - Styling: Tailwind CSS + - State: TanStack Query + Zustand + - UI Components: shadcn/ui + Radix UI + +后端技术栈: + - Framework: Golang + Gin + - Database: PostgreSQL 15 + - Cache: Redis 7 + - ORM: GORM + - Authentication: JWT + - Image Processing: libvips/bimg + +存储和部署: + - Object Storage: MinIO/AWS S3 + - Web Server: Caddy + - Container: Docker + Docker Compose + - CI/CD: Gitea Actions +``` + +## 📚 文档导航 + +### 核心设计文档 +1. **[管理后台开发文档](./admin/管理后台开发文档.md)** + - 功能模块详细设计 + - 界面交互规范 + - 组件架构设计 + +2. **[Golang项目架构文档](./backend/Golang项目架构文档.md)** + - 项目结构设计 + - 分层架构实现 + - 依赖注入配置 + +3. **[数据库设计文档](./database/数据库设计文档.md)** + - 表结构设计 + - 索引优化策略 + - 数据迁移方案 + +4. **[API接口设计文档](./api/API接口设计文档.md)** + - RESTful API规范 + - 接口定义和示例 + - 错误处理机制 + +### 开发指南 +- **前端开发**: 参考 [前端开发文档](../原始\ prd/前端开发文档.md) +- **后端开发**: 参考 [后端开发文档](../原始\ prd/后端开发文档.md) +- **UI设计**: 参考 [UI设计需求文档](../原始\ prd/UI设计需求文档.md) + +## 🚀 功能特性 + +### 核心功能 +- ✅ **前端展示** + - 响应式照片网格展示 + - 时间线视图 + - 照片详情模态框 + - 分类和标签筛选 + +- 🔄 **管理后台** + - 照片批量上传和管理 + - 分类和标签管理 + - 用户权限控制 + - 系统设置配置 + +- 🔄 **后端API** + - 完整的CRUD操作 + - 文件上传处理 + - 图片多格式转换 + - 用户认证授权 + +- 🔄 **数据管理** + - PostgreSQL关系数据库 + - Redis缓存加速 + - MinIO对象存储 + - 数据备份恢复 + +### 性能特性 +- **图片优化**: 多格式转换 (JPG, WebP, AVIF) +- **懒加载**: 视口外图片延迟加载 +- **缓存策略**: 多级缓存提升响应速度 +- **CDN加速**: 静态资源分发优化 + +### 安全特性 +- **用户认证**: JWT令牌机制 +- **权限控制**: 基于角色的访问控制 +- **数据加密**: 敏感数据加密存储 +- **API限流**: 防止接口滥用 + +## 📈 开发进度 + +### 当前状态 + +| 模块 | 设计 | 开发 | 测试 | 部署 | 完成度 | +|------|------|------|------|------|--------| +| 前端展示 | ✅ | ✅ | ✅ | ✅ | 100% | +| 管理后台 | ✅ | ⏳ | ⏳ | ⏳ | 20% | +| 后端API | ✅ | ⏳ | ⏳ | ⏳ | 15% | +| 数据库 | ✅ | ⏳ | ⏳ | ⏳ | 30% | +| 图片处理 | ✅ | ⏳ | ⏳ | ⏳ | 10% | +| 用户认证 | ✅ | ⏳ | ⏳ | ⏳ | 5% | +| 文件存储 | ✅ | ⏳ | ⏳ | ⏳ | 5% | +| 部署配置 | ✅ | ⏳ | ⏳ | ⏳ | 10% | + +### 里程碑计划 + +#### 第一阶段 (2024-01-15 ~ 2024-02-15) +- [x] 完成技术选型和架构设计 +- [x] 完成详细设计文档编写 +- [ ] 搭建开发环境和基础框架 +- [ ] 实现管理后台核心界面 + +#### 第二阶段 (2024-02-15 ~ 2024-03-15) +- [ ] 完成后端API核心功能 +- [ ] 实现数据库表结构和迁移 +- [ ] 完成用户认证和权限系统 +- [ ] 实现图片上传和处理功能 + +#### 第三阶段 (2024-03-15 ~ 2024-04-15) +- [ ] 完成前后端功能对接 +- [ ] 实现文件存储和CDN配置 +- [ ] 完成系统测试和性能优化 +- [ ] 部署到生产环境 + +## 🔧 开发环境 + +### 环境要求 +```yaml +开发环境: + - Node.js: 18+ + - Golang: 1.21+ + - PostgreSQL: 15+ + - Redis: 7+ + - Docker: 24+ + +推荐配置: + - 内存: 8GB+ + - 硬盘: 50GB+ + - 操作系统: macOS/Linux +``` + +### 快速开始 +```bash +# 1. 克隆项目 +git clone +cd photography + +# 2. 前端开发 +cd frontend +make setup +make dev + +# 3. 后端开发 (开发中) +cd backend +make setup +make dev + +# 4. 数据库初始化 +make migrate-up +``` + +## 🧪 测试策略 + +### 测试类型 +- **单元测试**: 核心业务逻辑测试 +- **集成测试**: API接口和数据库交互测试 +- **端到端测试**: 完整用户流程测试 +- **性能测试**: 系统负载和响应时间测试 + +### 测试覆盖率目标 +- **后端代码**: 80%+ +- **前端组件**: 70%+ +- **API接口**: 90%+ + +## 📊 质量保证 + +### 代码质量 +- **代码规范**: ESLint + Prettier (前端), golangci-lint (后端) +- **类型检查**: TypeScript 严格模式 +- **代码审查**: Pull Request必须通过审查 +- **自动化测试**: CI/CD流水线集成 + +### 性能指标 +- **页面加载时间**: < 3s +- **图片处理时间**: < 10s +- **API响应时间**: < 500ms +- **数据库查询**: < 100ms + +## 🔍 监控和日志 + +### 应用监控 +- **错误监控**: 应用错误和异常追踪 +- **性能监控**: 响应时间和资源使用 +- **用户行为**: 用户操作和页面访问统计 + +### 日志管理 +- **结构化日志**: JSON格式日志记录 +- **日志级别**: Debug, Info, Warn, Error +- **日志轮转**: 按大小和时间自动轮转 + +## 📋 发布计划 + +### 发布流程 +1. **功能开发**: 在feature分支开发 +2. **代码审查**: 创建Pull Request +3. **测试验证**: 自动化测试通过 +4. **合并主分支**: 合并到main分支 +5. **部署发布**: 自动部署到生产环境 + +### 版本管理 +- **版本号**: 遵循语义化版本规范 +- **发布说明**: 详细的变更日志 +- **回滚策略**: 快速回滚机制 + +## 💡 未来规划 + +### v1.1 增强功能 +- 移动端PWA支持 +- 图片水印功能 +- 批量操作优化 +- 搜索功能增强 + +### v1.2 扩展功能 +- 评论系统 +- 社交分享 +- 数据导出 +- 多语言支持 + +### v2.0 升级计划 +- 微服务架构 +- AI智能标签 +- 实时通知 +- 高级分析 + +--- + +📅 **最后更新**: 2024-01-15 +📝 **文档版本**: v1.0 +👨‍💻 **维护者**: Claude Code Assistant \ No newline at end of file diff --git a/docs/v1/admin/管理后台开发文档.md b/docs/v1/admin/管理后台开发文档.md new file mode 100644 index 0000000..07cc1a4 --- /dev/null +++ b/docs/v1/admin/管理后台开发文档.md @@ -0,0 +1,1460 @@ +# 摄影作品集网站 - 管理后台开发文档 + +## 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 │ +├─────────────────────────────────────────────────────────┤ +│ 🔍 [搜索框] 📂 [分类筛选] 🏷️ [标签筛选] 📅 [时间筛选] │ +│ ➕ 上传照片 📤 批量操作 ⚙️ 设置 │ +├─────────────────────────────────────────────────────────┤ +│ 📷 照片网格 (支持列表/网格视图切换) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ +│ │ 标题 │ │ 标题 │ │ 标题 │ │ 标题 │ │ +│ │ 分类 │ │ 分类 │ │ 分类 │ │ 分类 │ │ +│ │ 2024/01 │ │ 2024/01 │ │ 2024/01 │ │ 2024/01 │ │ +│ │ [编辑] │ │ [编辑] │ │ [编辑] │ │ [编辑] │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ 📄 分页: [← 上一页] [1] [2] [3] [下一页 →] │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.2.2 照片详情编辑页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 编辑照片 Edit Photo │ +├─────────────────────────────────────────────────────────┤ +│ ← 返回列表 │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ │ │ 📝 基本信息 │ │ +│ │ [大图预览] │ │ 标题: [输入框] │ │ +│ │ │ │ 描述: [文本域] │ │ +│ │ [图片信息] │ │ 状态: [下拉选择] │ │ +│ │ - 尺寸: 1920x1080 │ │ │ │ +│ │ - 大小: 2.5MB │ │ 🏷️ 分类管理 │ │ +│ │ - 格式: JPG │ │ [分类选择器] │ │ +│ │ │ │ │ │ +│ │ [EXIF信息] │ │ 🔖 标签管理 │ │ +│ │ - 相机: Canon │ │ [标签输入] │ │ +│ │ - 镜头: 24-70mm │ │ │ │ +│ │ - ISO: 100 │ │ 📅 时间设置 │ │ +│ │ - 光圈: f/2.8 │ │ 拍摄时间: [日期选择器] │ │ +│ │ │ │ │ │ +│ └─────────────────┘ │ 💾 [保存] [取消] [删除] │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.2.3 批量上传功能 +``` +┌─────────────────────────────────────────────────────────┐ +│ 批量上传 Batch Upload │ +├─────────────────────────────────────────────────────────┤ +│ 📂 拖拽上传区域 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 拖拽文件到此处 │ │ +│ │ 或点击选择文件 │ │ +│ │ │ │ +│ │ 支持: JPG, PNG, RAW, HEIC │ │ +│ │ 最大: 50MB/文件, 100文件/次 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📋 上传队列 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 📷 photo1.jpg [████████████] 100% ✅ 完成 │ │ +│ │ 📷 photo2.raw [██████░░░░░░] 60% ⏳ 处理中 │ │ +│ │ 📷 photo3.jpg [░░░░░░░░░░░░] 0% ⏸️ 等待 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ⚙️ 批量设置 │ +│ 分类: [选择分类] 标签: [输入标签] 状态: [发布状态] │ +│ │ +│ 🚀 [开始上传] ⏸️ [暂停] 🗑️ [清空队列] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.3 分类管理模块 (Category Management) + +#### 2.3.1 分类列表页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 分类管理 Category Management │ +├─────────────────────────────────────────────────────────┤ +│ ➕ 新建分类 📊 分类统计 ⚙️ 批量操作 │ +├─────────────────────────────────────────────────────────┤ +│ 🌳 分类树形结构 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 📂 全部作品 (234张) [编辑] [删除] │ │ +│ │ ├─ 📂 城市风光 (89张) [编辑] [删除] │ │ +│ │ │ ├─ 📂 夜景 (34张) [编辑] [删除] │ │ +│ │ │ └─ 📂 建筑 (55张) [编辑] [删除] │ │ +│ │ ├─ 📂 自然风景 (78张) [编辑] [删除] │ │ +│ │ │ ├─ 📂 山景 (45张) [编辑] [删除] │ │ +│ │ │ └─ 📂 海景 (33张) [编辑] [删除] │ │ +│ │ └─ 📂 人像摄影 (67张) [编辑] [删除] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📊 分类统计 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 总分类: 8个 | 最热门: 城市风光 | 最新: 建筑摄影 │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.3.2 分类编辑页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 编辑分类 Edit Category │ +├─────────────────────────────────────────────────────────┤ +│ ← 返回列表 │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ [封面预览] │ │ 📝 基本信息 │ │ +│ │ │ │ 名称: [输入框] │ │ +│ │ [选择封面] │ │ 别名: [输入框] │ │ +│ │ │ │ 描述: [文本域] │ │ +│ │ [颜色选择] │ │ │ │ +│ │ 🎨 #d4af37 │ │ 🏗️ 结构设置 │ │ +│ │ │ │ 父分类: [选择器] │ │ +│ │ 统计信息 │ │ 排序: [数字输入] │ │ +│ │ - 照片数: 89 │ │ 状态: [开关] │ │ +│ │ - 子分类: 2 │ │ │ │ +│ │ - 创建: 2024/01 │ │ 💾 [保存] [取消] [删除] │ │ +│ └─────────────────┘ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.4 标签管理模块 (Tag Management) + +#### 2.4.1 标签列表页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 标签管理 Tag Management │ +├─────────────────────────────────────────────────────────┤ +│ ➕ 新建标签 🔍 [搜索框] 📊 使用统计 │ +├─────────────────────────────────────────────────────────┤ +│ 🏷️ 标签云 (按使用频率大小显示) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 城市风光 自然风景 人像摄影 │ │ +│ │ 夜景 建筑摄影 山景 海景 │ │ +│ │ 街头摄影 微距摄影 黑白 彩色 日出 日落 │ │ +│ │ 雨天 晴天 多云 雪景 春天 夏天 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📊 标签列表 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 标签名 颜色 使用次数 创建时间 操作 │ │ +│ │ 城市风光 🔵 89次 2024/01/01 [编辑] │ │ +│ │ 自然风景 🟢 78次 2024/01/02 [编辑] │ │ +│ │ 人像摄影 🔴 67次 2024/01/03 [编辑] │ │ +│ │ 夜景 🟡 45次 2024/01/04 [编辑] │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.5 时间线管理模块 (Timeline Management) + +#### 2.5.1 时间线编辑页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 时间线管理 Timeline Management │ +├─────────────────────────────────────────────────────────┤ +│ 📅 [年份选择] 📊 统计信息 ⚙️ 批量操作 │ +├─────────────────────────────────────────────────────────┤ +│ 🗓️ 时间线编辑器 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 2024年 总计: 234张 │ │ +│ │ ├─ 📅 1月 (45张) [编辑] │ │ +│ │ │ ├─ 📷 城市夜景系列 (12张) [调整] │ │ +│ │ │ ├─ 📷 雪景作品 (8张) [调整] │ │ +│ │ │ └─ 📷 春节街拍 (25张) [调整] │ │ +│ │ ├─ 📅 2月 (38张) [编辑] │ │ +│ │ │ ├─ 📷 梅花摄影 (18张) [调整] │ │ +│ │ │ └─ 📷 古建筑 (20张) [调整] │ │ +│ │ └─ 📅 3月 (51张) [编辑] │ │ +│ │ ├─ 📷 春天花卉 (25张) [调整] │ │ +│ │ └─ 📷 风景写真 (26张) [调整] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🎯 里程碑事件 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ➕ 添加里程碑 │ │ +│ │ 📍 2024/01/15 - 首次拍摄城市夜景 │ │ +│ │ 📍 2024/02/20 - 获得摄影比赛奖项 │ │ +│ │ 📍 2024/03/10 - 新镜头首次使用 │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.6 系统设置模块 (System Settings) + +#### 2.6.1 基本设置页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 系统设置 System Settings │ +├─────────────────────────────────────────────────────────┤ +│ 🔧 基本设置 📷 上传设置 🎨 主题配置 🗂️ 缓存管理 │ +├─────────────────────────────────────────────────────────┤ +│ 📝 网站基本信息 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 网站标题: [摄影作品集] │ │ +│ │ 网站描述: [专业摄影师作品展示] │ │ +│ │ 关键词: [摄影,作品集,艺术] │ │ +│ │ 联系邮箱: [contact@example.com] │ │ +│ │ 版权信息: [© 2024 摄影师姓名] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📷 上传配置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 最大文件大小: [50] MB │ │ +│ │ 支持格式: [JPG, PNG, RAW, HEIC] │ │ +│ │ 图片质量: [85] % │ │ +│ │ 缩略图尺寸: 小[150px] 中[300px] 大[600px] │ │ +│ │ 自动发布: [开启] 水印添加: [关闭] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🎨 主题配置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 主色调: [🎨 #d4af37] 辅助色: [🎨 #2d2d2d] │ │ +│ │ 字体: [Inter] 布局: [网格] 动画: [开启] │ │ +│ │ 深色模式: [自动] 响应式: [开启] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 💾 [保存设置] 🔄 [重置默认] 📥 [导出配置] │ +└─────────────────────────────────────────────────────────┘ +``` + +## 3. 技术架构设计 + +### 3.1 后端架构 (Golang) + +#### 3.1.1 项目结构 +``` +backend/ +├── cmd/ +│ └── server/ +│ └── main.go # 程序入口 +├── internal/ +│ ├── api/ # API层 +│ │ ├── handlers/ # 处理器 +│ │ │ ├── auth.go # 认证相关 +│ │ │ ├── photo.go # 照片管理 +│ │ │ ├── category.go # 分类管理 +│ │ │ ├── tag.go # 标签管理 +│ │ │ ├── timeline.go # 时间线管理 +│ │ │ ├── settings.go # 系统设置 +│ │ │ └── upload.go # 文件上传 +│ │ ├── middleware/ # 中间件 +│ │ │ ├── auth.go # 认证中间件 +│ │ │ ├── cors.go # CORS中间件 +│ │ │ ├── logger.go # 日志中间件 +│ │ │ └── ratelimit.go # 限流中间件 +│ │ └── routes/ # 路由 +│ │ ├── api.go # API路由 +│ │ └── admin.go # 管理路由 +│ ├── service/ # 业务逻辑层 +│ │ ├── auth_service.go # 认证服务 +│ │ ├── photo_service.go # 照片服务 +│ │ ├── category_service.go # 分类服务 +│ │ ├── tag_service.go # 标签服务 +│ │ ├── timeline_service.go # 时间线服务 +│ │ ├── upload_service.go # 上传服务 +│ │ └── settings_service.go # 设置服务 +│ ├── repository/ # 数据访问层 +│ │ ├── photo_repo.go # 照片数据访问 +│ │ ├── category_repo.go # 分类数据访问 +│ │ ├── tag_repo.go # 标签数据访问 +│ │ └── user_repo.go # 用户数据访问 +│ ├── models/ # 数据模型 +│ │ ├── photo.go # 照片模型 +│ │ ├── category.go # 分类模型 +│ │ ├── tag.go # 标签模型 +│ │ └── user.go # 用户模型 +│ └── utils/ # 工具函数 +│ ├── response.go # 响应工具 +│ ├── validator.go # 验证工具 +│ └── image.go # 图片工具 +├── pkg/ # 公共包 +│ ├── config/ # 配置管理 +│ │ └── config.go +│ ├── database/ # 数据库 +│ │ └── postgres.go +│ ├── cache/ # 缓存 +│ │ └── redis.go +│ ├── storage/ # 存储 +│ │ ├── local.go +│ │ └── s3.go +│ ├── logger/ # 日志 +│ │ └── logger.go +│ └── queue/ # 队列 +│ └── redis_queue.go +├── migrations/ # 数据库迁移 +│ ├── 001_create_photos_table.sql +│ ├── 002_create_categories_table.sql +│ └── 003_create_tags_table.sql +├── scripts/ # 脚本 +│ ├── build.sh +│ └── deploy.sh +├── docker/ # Docker配置 +│ ├── Dockerfile +│ └── docker-compose.yml +└── docs/ # 文档 + ├── api.md + └── deployment.md +``` + +#### 3.1.2 依赖管理 +```go +// go.mod +module photography-backend + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/spf13/viper v1.16.0 + github.com/sirupsen/logrus v1.9.3 + github.com/go-playground/validator/v10 v10.14.0 + gorm.io/gorm v1.25.4 + gorm.io/driver/postgres v1.5.2 + github.com/redis/go-redis/v9 v9.1.0 + github.com/minio/minio-go/v7 v7.0.63 + github.com/h2non/bimg v1.1.9 + github.com/gorilla/sessions v1.2.1 + github.com/google/uuid v1.3.0 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.1 +) +``` + +### 3.2 前端架构 (React) + +#### 3.2.1 项目结构 +``` +admin/ +├── src/ +│ ├── components/ # 组件 +│ │ ├── ui/ # 基础UI组件 +│ │ │ ├── button.tsx +│ │ │ ├── input.tsx +│ │ │ ├── modal.tsx +│ │ │ └── table.tsx +│ │ ├── layout/ # 布局组件 +│ │ │ ├── header.tsx +│ │ │ ├── sidebar.tsx +│ │ │ └── main-layout.tsx +│ │ ├── photo/ # 照片管理组件 +│ │ │ ├── photo-list.tsx +│ │ │ ├── photo-form.tsx +│ │ │ ├── photo-upload.tsx +│ │ │ └── photo-detail.tsx +│ │ ├── category/ # 分类管理组件 +│ │ │ ├── category-tree.tsx +│ │ │ ├── category-form.tsx +│ │ │ └── category-stats.tsx +│ │ └── common/ # 通用组件 +│ │ ├── loading.tsx +│ │ ├── error-boundary.tsx +│ │ └── confirmation.tsx +│ ├── pages/ # 页面 +│ │ ├── dashboard/ +│ │ │ └── index.tsx +│ │ ├── photos/ +│ │ │ ├── index.tsx +│ │ │ ├── edit.tsx +│ │ │ └── upload.tsx +│ │ ├── categories/ +│ │ │ └── index.tsx +│ │ ├── tags/ +│ │ │ └── index.tsx +│ │ └── settings/ +│ │ └── index.tsx +│ ├── hooks/ # 自定义Hooks +│ │ ├── useAuth.ts +│ │ ├── usePhotos.ts +│ │ ├── useCategories.ts +│ │ └── useUpload.ts +│ ├── services/ # API服务 +│ │ ├── api.ts +│ │ ├── auth.ts +│ │ ├── photo.ts +│ │ ├── category.ts +│ │ └── upload.ts +│ ├── store/ # 状态管理 +│ │ ├── auth.ts +│ │ ├── photo.ts +│ │ └── ui.ts +│ ├── utils/ # 工具函数 +│ │ ├── format.ts +│ │ ├── validation.ts +│ │ └── constants.ts +│ └── types/ # 类型定义 +│ ├── api.ts +│ ├── photo.ts +│ └── user.ts +├── public/ # 静态资源 +├── package.json +├── tsconfig.json +├── tailwind.config.js +└── vite.config.ts +``` + +## 4. 核心功能实现 + +### 4.1 图片上传与处理 + +#### 4.1.1 上传流程 +```go +// internal/service/upload_service.go +type UploadService struct { + storage storage.Storage + imageQueue queue.Queue + repo repository.PhotoRepository +} + +func (s *UploadService) UploadPhoto(file *multipart.FileHeader, metadata PhotoMetadata) (*Photo, error) { + // 1. 验证文件 + if err := s.validateFile(file); err != nil { + return nil, err + } + + // 2. 生成唯一文件名 + filename := s.generateFilename(file.Filename) + + // 3. 保存原始文件 + originalPath, err := s.storage.SaveOriginal(file, filename) + if err != nil { + return nil, err + } + + // 4. 创建照片记录 + photo := &Photo{ + Title: metadata.Title, + Description: metadata.Description, + OriginalFilename: file.Filename, + FileSize: file.Size, + Status: "processing", + Categories: metadata.Categories, + Tags: metadata.Tags, + } + + if err := s.repo.Create(photo); err != nil { + return nil, err + } + + // 5. 添加到处理队列 + s.imageQueue.Push(ProcessImageJob{ + PhotoID: photo.ID, + OriginalPath: originalPath, + }) + + return photo, nil +} +``` + +#### 4.1.2 图片处理队列 +```go +// internal/service/image_processor.go +type ImageProcessor struct { + storage storage.Storage + repo repository.PhotoRepository +} + +func (p *ImageProcessor) ProcessImage(job ProcessImageJob) error { + // 1. 加载原始图片 + img, err := bimg.NewFromFile(job.OriginalPath) + if err != nil { + return err + } + + // 2. 提取EXIF信息 + exif, err := p.extractEXIF(img) + if err != nil { + logrus.Warn("Failed to extract EXIF:", err) + } + + // 3. 生成多种格式和尺寸 + formats := []FormatConfig{ + {Type: "thumb_small", Width: 150, Height: 150, Quality: 80}, + {Type: "thumb_medium", Width: 300, Height: 300, Quality: 85}, + {Type: "thumb_large", Width: 600, Height: 600, Quality: 90}, + {Type: "display", Width: 1200, Height: 0, Quality: 90}, + {Type: "webp", Width: 1200, Height: 0, Quality: 85, Format: "webp"}, + } + + var photoFormats []PhotoFormat + for _, config := range formats { + processedImg, err := p.processFormat(img, config) + if err != nil { + continue + } + + path, err := p.storage.Save(processedImg, config.Type) + if err != nil { + continue + } + + photoFormats = append(photoFormats, PhotoFormat{ + PhotoID: job.PhotoID, + FormatType: config.Type, + FilePath: path, + FileSize: int64(len(processedImg)), + Width: config.Width, + Height: config.Height, + }) + } + + // 4. 更新照片信息 + updates := map[string]interface{}{ + "status": "published", + "camera": exif.Camera, + "lens": exif.Lens, + "iso": exif.ISO, + "aperture": exif.Aperture, + "shutter_speed": exif.ShutterSpeed, + "focal_length": exif.FocalLength, + "taken_at": exif.DateTime, + "formats": photoFormats, + } + + return p.repo.Update(job.PhotoID, updates) +} +``` + +### 4.2 分类管理系统 + +#### 4.2.1 树形分类结构 +```go +// internal/service/category_service.go +type CategoryService struct { + repo repository.CategoryRepository +} + +func (s *CategoryService) GetCategoryTree() ([]CategoryTree, error) { + categories, err := s.repo.GetAll() + if err != nil { + return nil, err + } + + return s.buildTree(categories, nil), nil +} + +func (s *CategoryService) buildTree(categories []Category, parentID *uint) []CategoryTree { + var tree []CategoryTree + + for _, category := range categories { + if category.ParentID == parentID { + node := CategoryTree{ + Category: category, + Children: s.buildTree(categories, &category.ID), + } + tree = append(tree, node) + } + } + + return tree +} +``` + +### 4.3 标签自动建议 + +#### 4.3.1 智能标签推荐 +```go +// internal/service/tag_service.go +type TagService struct { + repo repository.TagRepository + cache cache.Cache +} + +func (s *TagService) GetTagSuggestions(query string, limit int) ([]Tag, error) { + cacheKey := fmt.Sprintf("tag_suggestions:%s:%d", query, limit) + + // 检查缓存 + if cached, err := s.cache.Get(cacheKey); err == nil { + var tags []Tag + if err := json.Unmarshal(cached, &tags); err == nil { + return tags, nil + } + } + + // 数据库查询 + tags, err := s.repo.SearchByName(query, limit) + if err != nil { + return nil, err + } + + // 缓存结果 + if data, err := json.Marshal(tags); err == nil { + s.cache.Set(cacheKey, data, 5*time.Minute) + } + + return tags, nil +} +``` + +### 4.4 系统设置管理 + +#### 4.4.1 配置管理 +```go +// internal/service/settings_service.go +type SettingsService struct { + repo repository.SettingsRepository + cache cache.Cache +} + +func (s *SettingsService) GetSettings() (map[string]interface{}, error) { + cacheKey := "system_settings" + + // 检查缓存 + if cached, err := s.cache.Get(cacheKey); err == nil { + var settings map[string]interface{} + if err := json.Unmarshal(cached, &settings); err == nil { + return settings, nil + } + } + + // 数据库查询 + rawSettings, err := s.repo.GetAll() + if err != nil { + return nil, err + } + + // 类型转换 + settings := make(map[string]interface{}) + for _, setting := range rawSettings { + switch setting.Type { + case "number": + if val, err := strconv.Atoi(setting.Value); err == nil { + settings[setting.Key] = val + } + case "boolean": + settings[setting.Key] = setting.Value == "true" + case "json": + var jsonVal interface{} + if err := json.Unmarshal([]byte(setting.Value), &jsonVal); err == nil { + settings[setting.Key] = jsonVal + } + default: + settings[setting.Key] = setting.Value + } + } + + // 缓存结果 + if data, err := json.Marshal(settings); err == nil { + s.cache.Set(cacheKey, data, 10*time.Minute) + } + + return settings, nil +} +``` + +## 5. 安全性设计 + +### 5.1 用户认证与授权 + +#### 5.1.1 JWT认证 +```go +// internal/service/auth_service.go +type AuthService struct { + userRepo repository.UserRepository + jwtKey []byte +} + +func (s *AuthService) Login(username, password string) (*LoginResponse, error) { + user, err := s.userRepo.GetByUsername(username) + if err != nil { + return nil, ErrInvalidCredentials + } + + if !s.verifyPassword(user.PasswordHash, password) { + return nil, ErrInvalidCredentials + } + + token, err := s.generateToken(user) + if err != nil { + return nil, err + } + + // 记录登录日志 + s.logUserActivity(user.ID, "login", nil) + + return &LoginResponse{ + Token: token, + User: user, + }, nil +} + +func (s *AuthService) generateToken(user *User) (string, error) { + claims := jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "role": user.Role, + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(s.jwtKey) +} +``` + +#### 5.1.2 权限中间件 +```go +// internal/api/middleware/auth.go +func AuthMiddleware(jwtKey []byte) gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.JSON(401, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + tokenString = strings.TrimPrefix(tokenString, "Bearer ") + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil || !token.Valid { + c.JSON(401, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.JSON(401, gin.H{"error": "Invalid token claims"}) + c.Abort() + return + } + + c.Set("user_id", claims["user_id"]) + c.Set("username", claims["username"]) + c.Set("role", claims["role"]) + + c.Next() + } +} +``` + +### 5.2 文件上传安全 + +#### 5.2.1 文件验证 +```go +// internal/utils/validator.go +func ValidateImageFile(file *multipart.FileHeader) error { + // 检查文件大小 + maxSize := int64(50 * 1024 * 1024) // 50MB + if file.Size > maxSize { + return ErrFileTooLarge + } + + // 检查文件扩展名 + allowedExts := []string{".jpg", ".jpeg", ".png", ".raw", ".heic"} + ext := strings.ToLower(filepath.Ext(file.Filename)) + if !contains(allowedExts, ext) { + return ErrInvalidFileType + } + + // 检查MIME类型 + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + buffer := make([]byte, 512) + if _, err := src.Read(buffer); err != nil { + return err + } + + mimeType := http.DetectContentType(buffer) + allowedMimes := []string{"image/jpeg", "image/png", "image/x-canon-cr2"} + if !contains(allowedMimes, mimeType) { + return ErrInvalidMimeType + } + + return nil +} +``` + +## 6. 性能优化 + +### 6.1 数据库优化 + +#### 6.1.1 索引设计 +```sql +-- 照片表索引 +CREATE INDEX idx_photos_status ON photos(status); +CREATE INDEX idx_photos_created_at ON photos(created_at); +CREATE INDEX idx_photos_taken_at ON photos(taken_at); +CREATE INDEX idx_photos_status_created_at ON photos(status, created_at); + +-- 分类表索引 +CREATE INDEX idx_categories_parent_id ON categories(parent_id); +CREATE INDEX idx_categories_slug ON categories(slug); +CREATE INDEX idx_categories_sort_order ON categories(sort_order); + +-- 标签表索引 +CREATE INDEX idx_tags_name ON tags(name); +CREATE INDEX idx_tags_usage_count ON tags(usage_count); + +-- 关联表索引 +CREATE INDEX idx_photo_categories_photo_id ON photo_categories(photo_id); +CREATE INDEX idx_photo_categories_category_id ON photo_categories(category_id); +CREATE INDEX idx_photo_tags_photo_id ON photo_tags(photo_id); +CREATE INDEX idx_photo_tags_tag_id ON photo_tags(tag_id); +``` + +#### 6.1.2 查询优化 +```go +// internal/repository/photo_repo.go +func (r *PhotoRepository) GetPhotosWithPagination(req PhotoListRequest) ([]Photo, int64, error) { + var photos []Photo + var total int64 + + query := r.db.Model(&Photo{}). + Preload("Categories"). + Preload("Tags"). + Preload("Formats") + + // 条件过滤 + if req.Status != "" { + query = query.Where("status = ?", req.Status) + } + + if req.CategoryID != 0 { + query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). + Where("photo_categories.category_id = ?", req.CategoryID) + } + + if req.TagID != 0 { + query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). + Where("photo_tags.tag_id = ?", req.TagID) + } + + if req.Search != "" { + query = query.Where("title ILIKE ? OR description ILIKE ?", + "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 统计总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Limit + if err := query.Offset(offset).Limit(req.Limit). + Order(fmt.Sprintf("%s %s", req.SortBy, req.SortOrder)). + Find(&photos).Error; err != nil { + return nil, 0, err + } + + return photos, total, nil +} +``` + +### 6.2 缓存策略 + +#### 6.2.1 多级缓存 +```go +// internal/service/cache_service.go +type CacheService struct { + redis *redis.Client + memCache *cache.Cache +} + +func (s *CacheService) Get(key string) ([]byte, error) { + // L1: 内存缓存 + if data, found := s.memCache.Get(key); found { + return data.([]byte), nil + } + + // L2: Redis缓存 + data, err := s.redis.Get(context.Background(), key).Bytes() + if err != nil { + return nil, err + } + + // 回写到内存缓存 + s.memCache.Set(key, data, 5*time.Minute) + + return data, nil +} + +func (s *CacheService) Set(key string, value []byte, ttl time.Duration) error { + // 设置内存缓存 + s.memCache.Set(key, value, ttl) + + // 设置Redis缓存 + return s.redis.Set(context.Background(), key, value, ttl).Err() +} +``` + +## 7. 部署方案 + +### 7.1 Docker部署 + +#### 7.1.1 Dockerfile +```dockerfile +# 后端Dockerfile +FROM golang:1.21-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/server/main.go + +FROM alpine:latest +RUN apk --no-cache add ca-certificates vips-dev +WORKDIR /root/ + +COPY --from=builder /app/main . +COPY --from=builder /app/migrations ./migrations + +EXPOSE 8080 +CMD ["./main"] +``` + +#### 7.1.2 docker-compose.yml +```yaml +version: '3.8' + +services: + backend: + build: . + ports: + - "8080:8080" + depends_on: + - postgres + - redis + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=postgres + - DB_PASSWORD=password + - DB_NAME=photography + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - ./uploads:/app/uploads + + postgres: + image: postgres:15 + environment: + - POSTGRES_DB=photography + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + + minio: + image: minio/minio + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=admin + - MINIO_ROOT_PASSWORD=password123 + command: server /data --console-address ":9001" + volumes: + - minio_data:/data + +volumes: + postgres_data: + redis_data: + minio_data: +``` + +### 7.2 生产环境配置 + +#### 7.2.1 Caddy配置 +``` +# /etc/caddy/Caddyfile +admin.photography.iriver.top { + reverse_proxy localhost:3000 + + # 上传限制 + request_body { + max_size 100MB + } + + # 安全头 + header { + X-Frame-Options "DENY" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Referrer-Policy "strict-origin-when-cross-origin" + } + + # 日志 + log { + output file /var/log/caddy/admin.log + format json + } +} + +api.photography.iriver.top { + reverse_proxy localhost:8080 + + # CORS设置 + header { + Access-Control-Allow-Origin "https://admin.photography.iriver.top" + Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + Access-Control-Allow-Headers "Content-Type, Authorization" + } + + # API限流 + rate_limit { + zone api_zone + key remote_addr + events 100 + window 1m + } +} +``` + +## 8. 监控与日志 + +### 8.1 应用监控 + +#### 8.1.1 健康检查 +```go +// internal/api/handlers/health.go +func (h *HealthHandler) CheckHealth(c *gin.Context) { + checks := map[string]string{ + "database": h.checkDatabase(), + "redis": h.checkRedis(), + "storage": h.checkStorage(), + "queue": h.checkQueue(), + } + + healthy := true + for _, status := range checks { + if status != "ok" { + healthy = false + break + } + } + + status := "healthy" + httpStatus := 200 + if !healthy { + status = "unhealthy" + httpStatus = 503 + } + + c.JSON(httpStatus, gin.H{ + "status": status, + "timestamp": time.Now().UTC(), + "checks": checks, + }) +} +``` + +#### 8.1.2 性能指标 +```go +// internal/api/middleware/metrics.go +func MetricsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + c.Next() + + duration := time.Since(start) + + // 记录响应时间 + metrics.Histogram("http_request_duration_seconds", + duration.Seconds(), + "method", c.Request.Method, + "path", c.FullPath(), + "status", strconv.Itoa(c.Writer.Status()), + ) + + // 记录请求计数 + metrics.Counter("http_requests_total", + "method", c.Request.Method, + "path", c.FullPath(), + "status", strconv.Itoa(c.Writer.Status()), + ) + } +} +``` + +### 8.2 日志管理 + +#### 8.2.1 结构化日志 +```go +// pkg/logger/logger.go +func InitLogger(config *config.Config) { + logrus.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + }) + + logrus.SetLevel(logrus.InfoLevel) + if config.Debug { + logrus.SetLevel(logrus.DebugLevel) + } + + // 设置日志输出 + file, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + logrus.Warn("Failed to open log file, using stdout") + } else { + logrus.SetOutput(file) + } +} + +// 使用示例 +func LogUserActivity(userID uint, action string, details map[string]interface{}) { + logrus.WithFields(logrus.Fields{ + "user_id": userID, + "action": action, + "details": details, + "type": "user_activity", + }).Info("User activity logged") +} +``` + +## 9. 开发规范 + +### 9.1 代码规范 + +#### 9.1.1 Go代码规范 +```go +// 包注释 +// Package handlers 提供HTTP处理器实现 +package handlers + +// 结构体注释 +// PhotoHandler 处理照片相关的HTTP请求 +type PhotoHandler struct { + service service.PhotoService + logger *logrus.Logger +} + +// 方法注释 +// GetPhotos 获取照片列表 +// @Summary 获取照片列表 +// @Description 支持分页、筛选、搜索等功能 +// @Tags 照片管理 +// @Accept json +// @Produce json +// @Param page query int false "页码" +// @Param limit query int false "每页数量" +// @Success 200 {object} PhotoListResponse +// @Failure 400 {object} ErrorResponse +// @Router /api/photos [get] +func (h *PhotoHandler) GetPhotos(c *gin.Context) { + // 参数验证 + var req PhotoListRequest + if err := c.ShouldBindQuery(&req); err != nil { + h.logger.WithError(err).Error("Invalid request parameters") + c.JSON(400, gin.H{"error": "Invalid parameters"}) + return + } + + // 业务逻辑 + photos, total, err := h.service.GetPhotos(req) + if err != nil { + h.logger.WithError(err).Error("Failed to get photos") + c.JSON(500, gin.H{"error": "Internal server error"}) + return + } + + // 返回响应 + c.JSON(200, PhotoListResponse{ + Photos: photos, + Pagination: PaginationInfo{ + Page: req.Page, + Limit: req.Limit, + Total: total, + }, + }) +} +``` + +### 9.2 API文档 + +#### 9.2.1 Swagger文档 +```yaml +# docs/swagger.yaml +openapi: 3.0.0 +info: + title: Photography Admin API + version: 1.0.0 + description: 摄影作品集管理后台API + +paths: + /api/photos: + get: + summary: 获取照片列表 + tags: + - 照片管理 + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: limit + in: query + schema: + type: integer + default: 20 + - name: category + in: query + schema: + type: string + - name: status + in: query + schema: + type: string + enum: [published, draft, archived] + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PhotoListResponse' + 400: + description: 请求参数错误 + 500: + description: 服务器错误 + +components: + schemas: + Photo: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + status: + type: string + enum: [published, draft, archived] + created_at: + type: string + format: date-time + categories: + type: array + items: + $ref: '#/components/schemas/Category' + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + formats: + type: array + items: + $ref: '#/components/schemas/PhotoFormat' +``` + +## 10. 测试策略 + +### 10.1 单元测试 + +#### 10.1.1 服务层测试 +```go +// internal/service/photo_service_test.go +func TestPhotoService_GetPhotos(t *testing.T) { + // 准备测试数据 + mockRepo := &MockPhotoRepository{} + service := NewPhotoService(mockRepo) + + expectedPhotos := []Photo{ + {ID: 1, Title: "Test Photo 1"}, + {ID: 2, Title: "Test Photo 2"}, + } + + mockRepo.On("GetPhotosWithPagination", mock.Anything). + Return(expectedPhotos, int64(2), nil) + + // 执行测试 + req := PhotoListRequest{Page: 1, Limit: 10} + photos, total, err := service.GetPhotos(req) + + // 断言 + assert.NoError(t, err) + assert.Equal(t, int64(2), total) + assert.Len(t, photos, 2) + assert.Equal(t, "Test Photo 1", photos[0].Title) + + mockRepo.AssertExpectations(t) +} +``` + +### 10.2 集成测试 + +#### 10.2.1 API测试 +```go +// test/integration/photo_api_test.go +func TestPhotoAPI_GetPhotos(t *testing.T) { + // 设置测试环境 + app := setupTestApp() + + // 创建测试数据 + createTestPhotos(t) + + // 发起请求 + req := httptest.NewRequest("GET", "/api/photos?page=1&limit=10", nil) + resp := httptest.NewRecorder() + + app.ServeHTTP(resp, req) + + // 检查响应 + assert.Equal(t, 200, resp.Code) + + var response PhotoListResponse + err := json.Unmarshal(resp.Body.Bytes(), &response) + assert.NoError(t, err) + assert.NotEmpty(t, response.Photos) +} +``` + +## 11. 部署清单 + +### 11.1 开发环境部署 +```bash +# 1. 克隆代码 +git clone +cd photography-admin + +# 2. 启动数据库 +docker-compose up -d postgres redis minio + +# 3. 运行迁移 +make migrate + +# 4. 启动后端 +make run-backend + +# 5. 启动前端 +cd admin && npm install && npm run dev +``` + +### 11.2 生产环境部署 +```bash +# 1. 构建镜像 +make build-docker + +# 2. 部署到服务器 +make deploy-prod + +# 3. 检查服务状态 +make health-check + +# 4. 查看日志 +make logs +``` + +这个详细的管理后台设计文档涵盖了从功能设计到技术实现的各个方面,为Golang后端开发提供了完整的指导。 \ No newline at end of file diff --git a/docs/v1/api/API接口设计文档.md b/docs/v1/api/API接口设计文档.md new file mode 100644 index 0000000..01f3bfa --- /dev/null +++ b/docs/v1/api/API接口设计文档.md @@ -0,0 +1,1569 @@ +# 摄影作品集网站 - API接口设计文档 + +## 1. API 概述 + +### 1.1 设计原则 +- **RESTful 设计**: 遵循 REST 架构风格 +- **统一响应格式**: 标准化的 JSON 响应结构 +- **版本控制**: API 版本化管理 +- **安全认证**: JWT 令牌认证机制 +- **错误处理**: 详细的错误码和错误信息 +- **性能优化**: 支持分页、筛选、排序 + +### 1.2 基础信息 +```yaml +Base URL: https://api.photography.iriver.top +API Version: v1 +Content-Type: application/json +Authentication: Bearer Token (JWT) +``` + +### 1.3 通用响应格式 + +#### 1.3.1 成功响应 +```json +{ + "success": true, + "code": 200, + "message": "Success", + "data": { + // 具体数据内容 + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +#### 1.3.2 分页响应 +```json +{ + "success": true, + "code": 200, + "message": "Success", + "data": [ + // 数据列表 + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 150, + "total_pages": 8, + "has_next": true, + "has_prev": false + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +#### 1.3.3 错误响应 +```json +{ + "success": false, + "code": 400, + "message": "Bad Request", + "error": { + "type": "VALIDATION_ERROR", + "details": [ + { + "field": "title", + "message": "Title is required" + } + ] + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +### 1.4 HTTP 状态码规范 + +| 状态码 | 说明 | 使用场景 | +|--------|------|----------| +| 200 | OK | 请求成功 | +| 201 | Created | 资源创建成功 | +| 204 | No Content | 删除成功 | +| 400 | Bad Request | 请求参数错误 | +| 401 | Unauthorized | 未认证 | +| 403 | Forbidden | 权限不足 | +| 404 | Not Found | 资源不存在 | +| 409 | Conflict | 资源冲突 | +| 422 | Unprocessable Entity | 数据验证失败 | +| 429 | Too Many Requests | 请求过于频繁 | +| 500 | Internal Server Error | 服务器内部错误 | + +## 2. 认证与授权 + +### 2.1 认证机制 + +#### 2.1.1 登录认证 +```http +POST /v1/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "password123" +} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "message": "Login successful", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 86400, + "user": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "admin", + "display_name": "管理员", + "avatar_url": "https://example.com/avatar.jpg" + } + } +} +``` + +#### 2.1.2 令牌刷新 +```http +POST /v1/auth/refresh +Content-Type: application/json + +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +#### 2.1.3 登出 +```http +POST /v1/auth/logout +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +#### 2.1.4 用户信息 +```http +GET /v1/auth/profile +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 2.2 权限控制 + +#### 2.2.1 权限级别 +```json +{ + "roles": { + "super_admin": { + "name": "超级管理员", + "permissions": ["*"] + }, + "admin": { + "name": "管理员", + "permissions": [ + "photo.*", + "category.*", + "tag.*", + "user.read", + "user.update", + "settings.*" + ] + }, + "editor": { + "name": "编辑者", + "permissions": [ + "photo.read", + "photo.create", + "photo.update", + "category.read", + "tag.read", + "tag.create" + ] + }, + "user": { + "name": "普通用户", + "permissions": [ + "photo.read", + "category.read", + "tag.read" + ] + } + } +} +``` + +## 3. 照片管理 API + +### 3.1 照片列表 + +#### 3.1.1 获取照片列表 +```http +GET /v1/photos?page=1&limit=20&status=published&category=1&tag=nature&search=sunset&sort_by=created_at&sort_order=desc +``` + +**查询参数:** +| 参数 | 类型 | 必填 | 说明 | 示例 | +|------|------|------|------|------| +| page | integer | 否 | 页码,默认1 | 1 | +| limit | integer | 否 | 每页数量,默认20 | 20 | +| status | string | 否 | 状态筛选 | published, draft, archived | +| category | integer | 否 | 分类ID | 1 | +| tag | string | 否 | 标签名称 | nature | +| search | string | 否 | 搜索关键词 | sunset | +| sort_by | string | 否 | 排序字段 | created_at, taken_at, title | +| sort_order | string | 否 | 排序方向 | asc, desc | +| year | integer | 否 | 年份筛选 | 2024 | +| month | integer | 否 | 月份筛选 | 1 | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "title": "城市夜景", + "description": "繁华都市的夜晚景色", + "slug": "city-night-view-001", + "status": "published", + "visibility": "public", + "taken_at": "2024-01-15T18:30:00Z", + "created_at": "2024-01-15T20:00:00Z", + "updated_at": "2024-01-15T20:00:00Z", + "view_count": 156, + "like_count": 23, + "formats": { + "thumb_small": "https://cdn.example.com/photos/1/thumb_small.jpg", + "thumb_medium": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "thumb_large": "https://cdn.example.com/photos/1/thumb_large.jpg", + "display": "https://cdn.example.com/photos/1/display.jpg", + "webp": "https://cdn.example.com/photos/1/display.webp" + }, + "exif": { + "camera": "Canon EOS R5", + "lens": "RF 24-70mm f/2.8L IS USM", + "iso": 800, + "aperture": "f/2.8", + "shutter_speed": "1/125", + "focal_length": "50mm" + }, + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370, + "country": "China", + "city": "Shanghai" + }, + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "color": "#3498db" + } + ], + "tags": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "color": "#2c3e50" + }, + { + "id": 2, + "name": "城市", + "slug": "city", + "color": "#e74c3c" + } + ] + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 150, + "total_pages": 8, + "has_next": true, + "has_prev": false + } +} +``` + +### 3.2 照片详情 + +#### 3.2.1 获取照片详情 +```http +GET /v1/photos/{id} +``` + +**路径参数:** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 照片ID | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "id": 1, + "title": "城市夜景", + "description": "繁华都市的夜晚景色,灯火通明的建筑群构成了美丽的天际线...", + "slug": "city-night-view-001", + "status": "published", + "visibility": "public", + "sort_order": 0, + "taken_at": "2024-01-15T18:30:00Z", + "created_at": "2024-01-15T20:00:00Z", + "updated_at": "2024-01-15T20:00:00Z", + "view_count": 156, + "like_count": 23, + "download_count": 5, + "file_info": { + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "mime_type": "image/jpeg" + }, + "formats": { + "original": "https://cdn.example.com/photos/1/original.jpg", + "jpg": "https://cdn.example.com/photos/1/display.jpg", + "webp": "https://cdn.example.com/photos/1/display.webp", + "thumb_small": "https://cdn.example.com/photos/1/thumb_small.jpg", + "thumb_medium": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "thumb_large": "https://cdn.example.com/photos/1/thumb_large.jpg" + }, + "exif": { + "camera": "Canon EOS R5", + "lens": "RF 24-70mm f/2.8L IS USM", + "iso": 800, + "aperture": "f/2.8", + "shutter_speed": "1/125", + "focal_length": "50mm" + }, + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370, + "country": "China", + "city": "Shanghai" + }, + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "description": "城市景观摄影作品", + "color": "#3498db", + "is_primary": true + } + ], + "tags": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "color": "#2c3e50", + "confidence": 1.0, + "source": "manual" + }, + { + "id": 2, + "name": "城市", + "slug": "city", + "color": "#e74c3c", + "confidence": 0.95, + "source": "ai" + } + ], + "metadata": { + "weather": "clear", + "temperature": "15°C", + "processing_notes": "调整了曝光和对比度" + } + } +} +``` + +### 3.3 照片操作 + +#### 3.3.1 创建照片 (用于上传后的元数据创建) +```http +POST /v1/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "城市夜景", + "description": "繁华都市的夜晚景色", + "file_id": "upload_123456789", + "status": "published", + "visibility": "public", + "taken_at": "2024-01-15T18:30:00Z", + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370 + }, + "categories": [1, 2], + "tags": ["夜景", "城市", "建筑"], + "metadata": { + "weather": "clear", + "temperature": "15°C" + } +} +``` + +#### 3.3.2 更新照片信息 +```http +PUT /v1/photos/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "上海外滩夜景", + "description": "更新后的描述", + "status": "published", + "categories": [1, 3], + "tags": ["夜景", "城市", "外滩"] +} +``` + +#### 3.3.3 删除照片 +```http +DELETE /v1/photos/{id} +Authorization: Bearer {token} +``` + +#### 3.3.4 批量操作 +```http +POST /v1/photos/batch +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3, 4, 5], + "action": "update_status", + "data": { + "status": "published" + } +} +``` + +**支持的批量操作:** +- `update_status`: 批量更新状态 +- `add_tags`: 批量添加标签 +- `remove_tags`: 批量移除标签 +- `add_categories`: 批量添加分类 +- `remove_categories`: 批量移除分类 +- `delete`: 批量删除 + +### 3.4 照片搜索 + +#### 3.4.1 全文搜索 +```http +GET /v1/photos/search?q=夜景&category=1&tags=城市,建筑&location=上海&date_from=2024-01-01&date_to=2024-12-31 +``` + +**查询参数:** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| q | string | 是 | 搜索关键词 | +| category | integer | 否 | 分类ID | +| tags | string | 否 | 标签名称,逗号分隔 | +| location | string | 否 | 地点名称 | +| date_from | string | 否 | 开始日期 (YYYY-MM-DD) | +| date_to | string | 否 | 结束日期 (YYYY-MM-DD) | +| camera | string | 否 | 相机型号 | +| lens | string | 否 | 镜头型号 | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "photos": [ + { + "id": 1, + "title": "城市夜景", + "score": 0.95, + "highlight": { + "title": "夜景", + "description": "繁华都市的夜晚景色" + } + } + ], + "facets": { + "categories": [ + {"name": "城市风光", "count": 15}, + {"name": "建筑摄影", "count": 8} + ], + "tags": [ + {"name": "夜景", "count": 12}, + {"name": "城市", "count": 20} + ], + "years": [ + {"year": 2024, "count": 25}, + {"year": 2023, "count": 18} + ] + } + }, + "pagination": { + "page": 1, + "limit": 20, + "total": 25 + } +} +``` + +## 4. 文件上传 API + +### 4.1 文件上传 + +#### 4.1.1 单文件上传 +```http +POST /v1/upload/single +Authorization: Bearer {token} +Content-Type: multipart/form-data + +file: (binary data) +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "file_id": "upload_123456789", + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "mime_type": "image/jpeg", + "upload_url": "https://temp.example.com/upload_123456789.jpg", + "status": "uploaded", + "exif_extracted": true, + "processing_status": "pending" + } +} +``` + +#### 4.1.2 多文件上传 +```http +POST /v1/upload/multiple +Authorization: Bearer {token} +Content-Type: multipart/form-data + +files[]: (binary data) +files[]: (binary data) +... +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "uploaded": [ + { + "file_id": "upload_123456789", + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "status": "uploaded" + } + ], + "failed": [ + { + "filename": "invalid_file.txt", + "error": "Invalid file type" + } + ], + "summary": { + "total": 5, + "success": 4, + "failed": 1 + } + } +} +``` + +#### 4.1.3 分块上传 +```http +POST /v1/upload/chunked/init +Authorization: Bearer {token} +Content-Type: application/json + +{ + "filename": "large_photo.raw", + "file_size": 52428800, + "mime_type": "image/raw", + "chunk_size": 1048576 +} +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "upload_id": "chunked_123456789", + "chunk_size": 1048576, + "total_chunks": 50, + "upload_urls": [ + "https://temp.example.com/chunked_123456789/chunk_0", + "https://temp.example.com/chunked_123456789/chunk_1" + ] + } +} +``` + +#### 4.1.4 上传状态查询 +```http +GET /v1/upload/status/{file_id} +Authorization: Bearer {token} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "file_id": "upload_123456789", + "status": "processing", + "progress": 75, + "current_step": "generating_thumbnails", + "steps": [ + {"name": "uploaded", "status": "completed"}, + {"name": "exif_extraction", "status": "completed"}, + {"name": "generating_thumbnails", "status": "processing"}, + {"name": "optimization", "status": "pending"} + ], + "estimated_time_remaining": 30 + } +} +``` + +### 4.2 文件处理 + +#### 4.2.1 重新处理文件 +```http +POST /v1/upload/{file_id}/reprocess +Authorization: Bearer {token} +Content-Type: application/json + +{ + "formats": ["thumb_small", "thumb_medium", "webp"], + "force": true +} +``` + +#### 4.2.2 删除上传文件 +```http +DELETE /v1/upload/{file_id} +Authorization: Bearer {token} +``` + +## 5. 分类管理 API + +### 5.1 分类操作 + +#### 5.1.1 获取分类列表 +```http +GET /v1/categories?include_stats=true&include_tree=true +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "description": "城市景观摄影作品", + "parent_id": null, + "level": 0, + "path": "1", + "cover_photo": { + "id": 15, + "title": "都市夜景", + "thumb_url": "https://cdn.example.com/photos/15/thumb_medium.jpg" + }, + "color": "#3498db", + "icon": "building", + "sort_order": 1, + "is_active": true, + "is_featured": true, + "photo_count": 45, + "direct_photo_count": 30, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-15T10:30:00Z" + } + ], + "tree": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "photo_count": 45, + "children": [ + { + "id": 2, + "name": "夜景摄影", + "slug": "night-photography", + "photo_count": 15, + "children": [] + }, + { + "id": 3, + "name": "建筑摄影", + "slug": "architecture", + "photo_count": 30, + "children": [] + } + ] + } + ], + "stats": { + "total_categories": 12, + "max_level": 3, + "featured_count": 5 + } + } +} +``` + +#### 5.1.2 获取分类详情 +```http +GET /v1/categories/{id} +``` + +#### 5.1.3 创建分类 +```http +POST /v1/categories +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "自然风景", + "slug": "nature-landscape", + "description": "自然风景摄影作品", + "parent_id": null, + "color": "#27ae60", + "icon": "tree", + "sort_order": 2, + "is_featured": true, + "seo_title": "自然风景摄影作品集", + "seo_description": "欣赏美丽的自然风景摄影作品" +} +``` + +#### 5.1.4 更新分类 +```http +PUT /v1/categories/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "自然风光", + "description": "更新后的描述", + "color": "#2ecc71" +} +``` + +#### 5.1.5 删除分类 +```http +DELETE /v1/categories/{id} +Authorization: Bearer {token} +``` + +#### 5.1.6 设置分类封面 +```http +PUT /v1/categories/{id}/cover +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_id": 15 +} +``` + +### 5.2 分类照片管理 + +#### 5.2.1 获取分类下的照片 +```http +GET /v1/categories/{id}/photos?page=1&limit=20&sort_by=created_at&sort_order=desc +``` + +#### 5.2.2 添加照片到分类 +```http +POST /v1/categories/{id}/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3], + "is_primary": true +} +``` + +#### 5.2.3 从分类移除照片 +```http +DELETE /v1/categories/{id}/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3] +} +``` + +## 6. 标签管理 API + +### 6.1 标签操作 + +#### 6.1.1 获取标签列表 +```http +GET /v1/tags?group=all&sort_by=usage_count&sort_order=desc&limit=50 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "description": "夜晚拍摄的景色", + "color": "#2c3e50", + "icon": "moon", + "tag_group": "style", + "usage_count": 45, + "trend_score": 8.5, + "is_active": true, + "is_featured": true, + "created_at": "2024-01-01T00:00:00Z", + "last_used_at": "2024-01-15T10:30:00Z" + } + ], + "groups": { + "style": {"name": "摄影风格", "count": 12}, + "subject": {"name": "拍摄主题", "count": 18}, + "technique": {"name": "拍摄技法", "count": 8}, + "location": {"name": "地理位置", "count": 25} + } +} +``` + +#### 6.1.2 标签搜索建议 +```http +GET /v1/tags/suggestions?q=夜&limit=10 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "usage_count": 45, + "match_score": 0.95 + }, + { + "id": 15, + "name": "夜市", + "slug": "night-market", + "usage_count": 12, + "match_score": 0.8 + } + ] +} +``` + +#### 6.1.3 创建标签 +```http +POST /v1/tags +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "极光", + "slug": "aurora", + "description": "极光摄影作品", + "color": "#9b59b6", + "tag_group": "subject", + "is_featured": true +} +``` + +#### 6.1.4 更新标签 +```http +PUT /v1/tags/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "北极光", + "description": "更新后的描述", + "color": "#8e44ad" +} +``` + +#### 6.1.5 删除标签 +```http +DELETE /v1/tags/{id} +Authorization: Bearer {token} +``` + +### 6.2 标签统计 + +#### 6.2.1 标签云数据 +```http +GET /v1/tags/cloud?min_usage=5&max_tags=50 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "usage_count": 45, + "relative_size": 100, + "color": "#2c3e50" + }, + { + "id": 2, + "name": "城市", + "usage_count": 38, + "relative_size": 84, + "color": "#e74c3c" + } + ] +} +``` + +#### 6.2.2 趋势标签 +```http +GET /v1/tags/trending?period=30d&limit=10 +``` + +## 7. 时间线 API + +### 7.1 时间线数据 + +#### 7.1.1 获取时间线 +```http +GET /v1/timeline?year=2024&include_photos=true&photos_limit=5 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "years": [ + { + "year": 2024, + "photo_count": 156, + "months": [ + { + "month": 1, + "month_name": "一月", + "photo_count": 25, + "photos": [ + { + "id": 1, + "title": "城市夜景", + "thumb_url": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "taken_at": "2024-01-15T18:30:00Z" + } + ], + "events": [ + { + "id": 1, + "title": "首次夜景拍摄", + "description": "第一次尝试城市夜景摄影", + "date": "2024-01-15", + "type": "milestone" + } + ] + } + ] + } + ], + "stats": { + "total_photos": 456, + "year_range": [2020, 2024], + "most_active_month": { + "year": 2024, + "month": 3, + "count": 45 + }, + "photos_by_year": [ + {"year": 2024, "count": 156}, + {"year": 2023, "count": 189}, + {"year": 2022, "count": 111} + ] + } + } +} +``` + +#### 7.1.2 获取指定月份详情 +```http +GET /v1/timeline/{year}/{month}?include_photos=true +``` + +### 7.2 时间线事件 + +#### 7.2.1 创建事件 +```http +POST /v1/timeline/events +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "获得摄影比赛奖项", + "description": "在城市摄影大赛中获得金奖", + "date": "2024-03-15", + "type": "achievement", + "related_photos": [15, 23, 31] +} +``` + +#### 7.2.2 更新事件 +```http +PUT /v1/timeline/events/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "更新后的标题", + "description": "更新后的描述" +} +``` + +#### 7.2.3 删除事件 +```http +DELETE /v1/timeline/events/{id} +Authorization: Bearer {token} +``` + +## 8. 系统设置 API + +### 8.1 设置管理 + +#### 8.1.1 获取系统设置 +```http +GET /v1/settings?category=all&include_public=true +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "general": { + "site_title": "摄影作品集", + "site_description": "专业摄影师作品展示平台", + "site_keywords": "摄影,作品集,艺术,创作", + "site_author": "摄影师姓名", + "site_email": "contact@example.com" + }, + "upload": { + "max_file_size": 52428800, + "allowed_types": ["image/jpeg", "image/png", "image/raw"], + "max_files_per_batch": 50, + "auto_publish": false, + "generate_thumbnails": true + }, + "image": { + "quality_jpg": 85, + "quality_webp": 80, + "max_width": 1920, + "max_height": 1080, + "watermark_enabled": false + }, + "display": { + "photos_per_page": 20, + "thumbnail_size": 300, + "theme_primary_color": "#d4af37", + "theme_secondary_color": "#2d2d2d", + "enable_dark_mode": true, + "enable_animations": true + } + } +} +``` + +#### 8.1.2 更新系统设置 +```http +PUT /v1/settings +Authorization: Bearer {token} +Content-Type: application/json + +{ + "site_title": "我的摄影作品集", + "upload_max_file_size": 104857600, + "display_photos_per_page": 24 +} +``` + +#### 8.1.3 重置设置 +```http +POST /v1/settings/reset +Authorization: Bearer {token} +Content-Type: application/json + +{ + "categories": ["upload", "image"], + "confirm": true +} +``` + +### 8.2 缓存管理 + +#### 8.2.1 清理缓存 +```http +POST /v1/settings/cache/clear +Authorization: Bearer {token} +Content-Type: application/json + +{ + "types": ["photos", "categories", "tags", "settings"], + "confirm": true +} +``` + +#### 8.2.2 预热缓存 +```http +POST /v1/settings/cache/warm +Authorization: Bearer {token} +Content-Type: application/json + +{ + "types": ["popular_photos", "category_tree", "tag_cloud"] +} +``` + +#### 8.2.3 缓存统计 +```http +GET /v1/settings/cache/stats +Authorization: Bearer {token} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "redis": { + "connected": true, + "used_memory": "15.2MB", + "total_keys": 1247, + "hit_rate": 0.89 + }, + "categories": { + "photos": {"keys": 456, "hit_rate": 0.92}, + "categories": {"keys": 12, "hit_rate": 0.95}, + "tags": {"keys": 89, "hit_rate": 0.87}, + "settings": {"keys": 1, "hit_rate": 0.99} + } + } +} +``` + +## 9. 统计分析 API + +### 9.1 仪表板统计 + +#### 9.1.1 仪表板数据 +```http +GET /v1/dashboard/stats?period=30d +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "summary": { + "total_photos": 456, + "total_categories": 12, + "total_tags": 89, + "total_views": 15420, + "storage_used": 2684354560, + "storage_limit": 10737418240 + }, + "recent": { + "new_photos": 15, + "new_views": 234, + "new_likes": 67, + "period": "7d" + }, + "trends": { + "uploads": [ + {"date": "2024-01-01", "count": 5}, + {"date": "2024-01-02", "count": 8}, + {"date": "2024-01-03", "count": 3} + ], + "views": [ + {"date": "2024-01-01", "count": 156}, + {"date": "2024-01-02", "count": 234}, + {"date": "2024-01-03", "count": 189} + ] + }, + "popular": { + "categories": [ + {"name": "城市风光", "count": 45, "percentage": 28.5}, + {"name": "自然风景", "count": 38, "percentage": 24.1} + ], + "tags": [ + {"name": "夜景", "count": 45}, + {"name": "城市", "count": 38} + ], + "photos": [ + { + "id": 15, + "title": "都市夜景", + "view_count": 1247, + "like_count": 89 + } + ] + }, + "system_status": { + "database": "healthy", + "redis": "healthy", + "storage": "healthy", + "queue": "healthy" + } + } +} +``` + +### 9.2 详细统计 + +#### 9.2.1 照片统计 +```http +GET /v1/stats/photos?group_by=month&year=2024 +``` + +#### 9.2.2 访问统计 +```http +GET /v1/stats/views?period=30d&group_by=day +``` + +#### 9.2.3 存储统计 +```http +GET /v1/stats/storage +``` + +## 10. 用户管理 API + +### 10.1 用户操作 + +#### 10.1.1 获取用户列表 +```http +GET /v1/users?page=1&limit=20&role=all&status=active&search=admin +``` + +#### 10.1.2 创建用户 +```http +POST /v1/users +Authorization: Bearer {token} +Content-Type: application/json + +{ + "username": "editor", + "email": "editor@example.com", + "password": "password123", + "role": "editor", + "display_name": "编辑者", + "is_active": true +} +``` + +#### 10.1.3 更新用户 +```http +PUT /v1/users/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "display_name": "高级编辑者", + "role": "admin", + "is_active": true +} +``` + +#### 10.1.4 删除用户 +```http +DELETE /v1/users/{id} +Authorization: Bearer {token} +``` + +### 10.2 用户会话管理 + +#### 10.2.1 获取用户会话 +```http +GET /v1/users/{id}/sessions +Authorization: Bearer {token} +``` + +#### 10.2.2 强制下线 +```http +DELETE /v1/users/{id}/sessions/{session_id} +Authorization: Bearer {token} +``` + +## 11. 错误处理 + +### 11.1 错误码定义 + +| 错误码 | HTTP状态码 | 说明 | +|--------|------------|------| +| VALIDATION_ERROR | 400 | 请求参数验证失败 | +| AUTHENTICATION_REQUIRED | 401 | 需要认证 | +| INVALID_TOKEN | 401 | 无效的令牌 | +| TOKEN_EXPIRED | 401 | 令牌已过期 | +| PERMISSION_DENIED | 403 | 权限不足 | +| RESOURCE_NOT_FOUND | 404 | 资源不存在 | +| RESOURCE_CONFLICT | 409 | 资源冲突 | +| UNPROCESSABLE_ENTITY | 422 | 数据处理失败 | +| RATE_LIMIT_EXCEEDED | 429 | 请求频率超限 | +| INTERNAL_SERVER_ERROR | 500 | 服务器内部错误 | +| SERVICE_UNAVAILABLE | 503 | 服务不可用 | + +### 11.2 错误响应示例 + +#### 11.2.1 验证错误 +```json +{ + "success": false, + "code": 400, + "message": "Validation failed", + "error": { + "type": "VALIDATION_ERROR", + "details": [ + { + "field": "title", + "message": "Title is required", + "code": "REQUIRED" + }, + { + "field": "email", + "message": "Invalid email format", + "code": "INVALID_FORMAT" + } + ] + } +} +``` + +#### 11.2.2 权限错误 +```json +{ + "success": false, + "code": 403, + "message": "Permission denied", + "error": { + "type": "PERMISSION_DENIED", + "details": { + "required_permission": "photo.delete", + "user_role": "editor" + } + } +} +``` + +#### 11.2.3 资源不存在 +```json +{ + "success": false, + "code": 404, + "message": "Resource not found", + "error": { + "type": "RESOURCE_NOT_FOUND", + "details": { + "resource_type": "photo", + "resource_id": 999 + } + } +} +``` + +## 12. API 限流 + +### 12.1 限流策略 + +| 端点类型 | 限制 | 时间窗口 | +|----------|------|----------| +| 认证端点 | 5次/IP | 1分钟 | +| 上传端点 | 10次/用户 | 1分钟 | +| 搜索端点 | 60次/用户 | 1分钟 | +| 一般端点 | 1000次/用户 | 1小时 | +| 管理端点 | 500次/用户 | 1小时 | + +### 12.2 限流响应头 + +```http +X-RateLimit-Limit: 1000 +X-RateLimit-Remaining: 999 +X-RateLimit-Reset: 1642636800 +X-RateLimit-Window: 3600 +``` + +### 12.3 限流超出响应 + +```json +{ + "success": false, + "code": 429, + "message": "Rate limit exceeded", + "error": { + "type": "RATE_LIMIT_EXCEEDED", + "details": { + "limit": 1000, + "window": 3600, + "reset_at": "2024-01-15T11:00:00Z" + } + } +} +``` + +## 13. WebHook API + +### 13.1 WebHook 配置 + +#### 13.1.1 创建 WebHook +```http +POST /v1/webhooks +Authorization: Bearer {token} +Content-Type: application/json + +{ + "url": "https://example.com/webhook", + "events": ["photo.created", "photo.updated", "photo.deleted"], + "secret": "webhook_secret_key", + "is_active": true +} +``` + +#### 13.1.2 WebHook 事件类型 + +| 事件类型 | 描述 | 数据载荷 | +|----------|------|----------| +| photo.created | 照片创建 | 完整照片数据 | +| photo.updated | 照片更新 | 更新后的照片数据 | +| photo.deleted | 照片删除 | 删除的照片ID | +| category.created | 分类创建 | 完整分类数据 | +| category.updated | 分类更新 | 更新后的分类数据 | +| category.deleted | 分类删除 | 删除的分类ID | +| user.login | 用户登录 | 用户基本信息 | +| system.backup | 系统备份 | 备份状态信息 | + +### 13.2 WebHook 载荷示例 + +```json +{ + "event": "photo.created", + "timestamp": "2024-01-15T10:30:00Z", + "data": { + "id": 1, + "title": "城市夜景", + "status": "published", + "created_at": "2024-01-15T10:30:00Z" + }, + "signature": "sha256=abcdef123456..." +} +``` + +## 14. API 版本控制 + +### 14.1 版本策略 + +- **URL 版本控制**: `/v1/`, `/v2/` +- **向后兼容**: 至少支持2个主版本 +- **废弃通知**: 通过响应头通知 + +### 14.2 版本响应头 + +```http +API-Version: 1.0 +API-Deprecated: false +API-Sunset: 2025-01-15T00:00:00Z +``` + +### 14.3 版本变更日志 + +#### 版本 1.0.0 (当前) +- 初始 API 版本 +- 支持照片、分类、标签管理 +- 用户认证和权限控制 + +#### 版本 1.1.0 (计划) +- 添加 AI 标签推荐 +- 支持视频文件 +- 增强搜索功能 + +## 15. 总结 + +这个API接口设计文档提供了摄影作品集网站的完整API规范,包括: + +### 🎯 设计特点 +- **RESTful 风格**: 符合REST架构原则 +- **统一响应格式**: 标准化的JSON响应 +- **完整的CRUD操作**: 支持所有资源的增删改查 +- **灵活的查询**: 丰富的筛选、排序、搜索功能 + +### 🔒 安全机制 +- **JWT认证**: 基于令牌的认证机制 +- **权限控制**: 细粒度的角色权限管理 +- **请求限流**: 防止API滥用 +- **数据验证**: 严格的输入验证 + +### 📊 功能丰富 +- **文件上传**: 支持单文件、多文件、分块上传 +- **图片处理**: 自动生成多种格式和尺寸 +- **全文搜索**: 强大的搜索和筛选功能 +- **统计分析**: 详细的数据统计和趋势分析 + +### 🛠️ 开发友好 +- **详细文档**: 完整的接口说明和示例 +- **错误处理**: 清晰的错误码和错误信息 +- **版本控制**: 科学的API版本管理 +- **WebHook支持**: 事件驱动的集成能力 + +这个API设计为Golang后端实现提供了完整的接口规范,可以支持前端和管理后台的所有功能需求。 \ No newline at end of file diff --git a/docs/v1/backend/Golang项目架构文档.md b/docs/v1/backend/Golang项目架构文档.md new file mode 100644 index 0000000..92b66af --- /dev/null +++ b/docs/v1/backend/Golang项目架构文档.md @@ -0,0 +1,1844 @@ +# 摄影作品集网站 - Golang项目架构文档 + +## 1. 项目概述 + +### 1.1 架构设计理念 +- **Clean Architecture**: 采用整洁架构,分离业务逻辑和技术实现 +- **Domain-Driven Design**: 以领域为核心的设计方法 +- **微服务友好**: 模块化设计,便于后续拆分为微服务 +- **高性能**: 充分利用Go语言的并发特性 +- **可测试**: 依赖注入和接口抽象,便于单元测试 + +### 1.2 技术栈选择 +```go +// 核心框架 +gin-gonic/gin // Web框架 +gorm.io/gorm // ORM框架 +redis/go-redis // Redis客户端 +golang-jwt/jwt // JWT认证 + +// 数据库 +gorm.io/driver/postgres // PostgreSQL驱动 +golang-migrate/migrate // 数据库迁移 + +// 图片处理 +h2non/bimg // 图片处理 (基于libvips) +disintegration/imaging // 图片处理备选方案 + +// 文件存储 +minio/minio-go // MinIO/S3客户端 +aws/aws-sdk-go // AWS SDK + +// 配置管理 +spf13/viper // 配置管理 +joho/godotenv // 环境变量 + +// 日志系统 +sirupsen/logrus // 结构化日志 +lumberjack.v2 // 日志轮转 + +// 验证和工具 +go-playground/validator // 数据验证 +google/uuid // UUID生成 +shopspring/decimal // 精确小数 + +// 测试和Mock +stretchr/testify // 测试框架 +golang/mock // Mock生成 +``` + +### 1.3 项目结构概览 +``` +photography-backend/ +├── cmd/ # 应用程序入口 +│ └── server/ +│ └── main.go +├── internal/ # 私有应用代码 +│ ├── api/ # API层 +│ ├── service/ # 业务逻辑层 +│ ├── repository/ # 数据访问层 +│ ├── domain/ # 领域模型 +│ ├── dto/ # 数据传输对象 +│ └── infrastructure/ # 基础设施层 +├── pkg/ # 公共库代码 +│ ├── config/ # 配置管理 +│ ├── database/ # 数据库连接 +│ ├── logger/ # 日志系统 +│ ├── middleware/ # 中间件 +│ ├── storage/ # 存储服务 +│ ├── cache/ # 缓存服务 +│ ├── queue/ # 队列服务 +│ └── utils/ # 工具函数 +├── migrations/ # 数据库迁移 +├── scripts/ # 构建和部署脚本 +├── docker/ # Docker配置 +├── docs/ # 项目文档 +├── test/ # 集成测试 +├── go.mod # Go模块定义 +├── go.sum # 依赖版本锁定 +├── Makefile # 构建任务 +└── README.md # 项目说明 +``` + +## 2. 详细架构设计 + +### 2.1 分层架构 + +#### 2.1.1 架构图 +``` +┌─────────────────────────────────────────────────────────┐ +│ API Layer (Gin) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ REST API │ │ WebSocket │ │ GraphQL │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Service Layer (Business Logic) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PhotoService│ │CategorySvc │ │ AuthService│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Repository Layer (Data Access) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PhotoRepo │ │CategoryRepo │ │ UserRepo │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ MinIO/S3 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.1.2 依赖关系 +``` +API Layer ──→ Service Layer ──→ Repository Layer ──→ Infrastructure Layer + ↑ ↑ ↑ ↑ + │ │ │ │ + Handlers Business Logic Data Access External Services +``` + +### 2.2 领域模型设计 + +#### 2.2.1 核心领域实体 +```go +// internal/domain/photo.go +package domain + +import ( + "time" + "github.com/google/uuid" +) + +// Photo 照片领域实体 +type Photo struct { + ID PhotoID `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Slug string `json:"slug"` + Status PhotoStatus `json:"status"` + Visibility Visibility `json:"visibility"` + + // 文件信息 + FileInfo FileInfo `json:"file_info"` + Formats []PhotoFormat `json:"formats"` + + // EXIF数据 + EXIF *EXIFData `json:"exif,omitempty"` + + // 位置信息 + Location *Location `json:"location,omitempty"` + + // 关联关系 + Categories []CategoryID `json:"categories"` + Tags []TagID `json:"tags"` + + // 统计信息 + Stats PhotoStats `json:"stats"` + + // 时间信息 + TakenAt *time.Time `json:"taken_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // 元数据 + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// PhotoID 照片ID值对象 +type PhotoID struct { + value uint +} + +func NewPhotoID(id uint) PhotoID { + return PhotoID{value: id} +} + +func (p PhotoID) Value() uint { + return p.value +} + +func (p PhotoID) String() string { + return fmt.Sprintf("photo-%d", p.value) +} + +// PhotoStatus 照片状态枚举 +type PhotoStatus string + +const ( + PhotoStatusDraft PhotoStatus = "draft" + PhotoStatusPublished PhotoStatus = "published" + PhotoStatusArchived PhotoStatus = "archived" + PhotoStatusProcessing PhotoStatus = "processing" +) + +func (s PhotoStatus) IsValid() bool { + switch s { + case PhotoStatusDraft, PhotoStatusPublished, PhotoStatusArchived, PhotoStatusProcessing: + return true + default: + return false + } +} + +// Visibility 可见性枚举 +type Visibility string + +const ( + VisibilityPublic Visibility = "public" + VisibilityPrivate Visibility = "private" + VisibilityPassword Visibility = "password" +) + +// FileInfo 文件信息值对象 +type FileInfo struct { + OriginalFilename string `json:"original_filename"` + FileSize int64 `json:"file_size"` + MimeType string `json:"mime_type"` + FileHash string `json:"file_hash"` +} + +// PhotoFormat 照片格式值对象 +type PhotoFormat struct { + Type FormatType `json:"type"` + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + Width int `json:"width"` + Height int `json:"height"` + Quality int `json:"quality"` +} + +type FormatType string + +const ( + FormatOriginal FormatType = "original" + FormatJPG FormatType = "jpg" + FormatWebP FormatType = "webp" + FormatThumbSmall FormatType = "thumb_small" + FormatThumbMedium FormatType = "thumb_medium" + FormatThumbLarge FormatType = "thumb_large" + FormatDisplay FormatType = "display" +) + +// EXIFData EXIF数据值对象 +type EXIFData struct { + Camera string `json:"camera,omitempty"` + Lens string `json:"lens,omitempty"` + ISO int `json:"iso,omitempty"` + Aperture string `json:"aperture,omitempty"` + ShutterSpeed string `json:"shutter_speed,omitempty"` + FocalLength string `json:"focal_length,omitempty"` +} + +// Location 位置信息值对象 +type Location struct { + Name string `json:"name"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` +} + +// PhotoStats 照片统计值对象 +type PhotoStats struct { + ViewCount int `json:"view_count"` + LikeCount int `json:"like_count"` + DownloadCount int `json:"download_count"` +} +``` + +#### 2.2.2 领域服务 +```go +// internal/domain/service/photo_domain_service.go +package service + +import ( + "context" + "fmt" + "strings" + "photography-backend/internal/domain" +) + +// PhotoDomainService 照片领域服务 +type PhotoDomainService struct { + slugGenerator SlugGenerator + exifExtractor EXIFExtractor +} + +// SlugGenerator 别名生成器接口 +type SlugGenerator interface { + Generate(title string) string +} + +// EXIFExtractor EXIF提取器接口 +type EXIFExtractor interface { + Extract(filePath string) (*domain.EXIFData, error) +} + +func NewPhotoDomainService(slugGen SlugGenerator, exifExt EXIFExtractor) *PhotoDomainService { + return &PhotoDomainService{ + slugGenerator: slugGen, + exifExtractor: exifExt, + } +} + +// CreatePhoto 创建照片聚合根 +func (s *PhotoDomainService) CreatePhoto(cmd CreatePhotoCommand) (*domain.Photo, error) { + // 生成唯一别名 + slug := s.slugGenerator.Generate(cmd.Title) + + // 提取EXIF数据 + exif, err := s.exifExtractor.Extract(cmd.FilePath) + if err != nil { + // EXIF提取失败不影响照片创建 + exif = nil + } + + // 创建照片实体 + photo := &domain.Photo{ + ID: domain.NewPhotoID(0), // 由数据库生成 + Title: cmd.Title, + Description: cmd.Description, + Slug: slug, + Status: domain.PhotoStatusProcessing, + Visibility: domain.VisibilityPublic, + FileInfo: domain.FileInfo{ + OriginalFilename: cmd.OriginalFilename, + FileSize: cmd.FileSize, + MimeType: cmd.MimeType, + }, + EXIF: exif, + Location: cmd.Location, + Categories: cmd.Categories, + Tags: cmd.Tags, + TakenAt: cmd.TakenAt, + Metadata: cmd.Metadata, + } + + // 验证照片数据 + if err := s.validatePhoto(photo); err != nil { + return nil, fmt.Errorf("photo validation failed: %w", err) + } + + return photo, nil +} + +// validatePhoto 验证照片数据 +func (s *PhotoDomainService) validatePhoto(photo *domain.Photo) error { + if strings.TrimSpace(photo.Title) == "" { + return fmt.Errorf("title cannot be empty") + } + + if !photo.Status.IsValid() { + return fmt.Errorf("invalid photo status: %s", photo.Status) + } + + // 验证文件信息 + if photo.FileInfo.FileSize <= 0 { + return fmt.Errorf("invalid file size") + } + + return nil +} + +// UpdatePhotoStatus 更新照片状态 +func (s *PhotoDomainService) UpdatePhotoStatus(photo *domain.Photo, newStatus domain.PhotoStatus) error { + if !newStatus.IsValid() { + return fmt.Errorf("invalid status: %s", newStatus) + } + + // 业务规则:已归档的照片不能直接发布 + if photo.Status == domain.PhotoStatusArchived && newStatus == domain.PhotoStatusPublished { + return fmt.Errorf("archived photo cannot be published directly") + } + + photo.Status = newStatus + return nil +} + +// CreatePhotoCommand 创建照片命令 +type CreatePhotoCommand struct { + Title string + Description string + FilePath string + OriginalFilename string + FileSize int64 + MimeType string + Location *domain.Location + Categories []domain.CategoryID + Tags []domain.TagID + TakenAt *time.Time + Metadata map[string]interface{} +} +``` + +### 2.3 服务层设计 + +#### 2.3.1 应用服务接口 +```go +// internal/service/photo_service.go +package service + +import ( + "context" + "photography-backend/internal/domain" + "photography-backend/internal/dto" +) + +// PhotoService 照片应用服务接口 +type PhotoService interface { + // 查询操作 + GetPhotos(ctx context.Context, req dto.PhotoListRequest) (*dto.PhotoListResponse, error) + GetPhotoByID(ctx context.Context, id uint) (*dto.PhotoResponse, error) + GetPhotoBySlug(ctx context.Context, slug string) (*dto.PhotoResponse, error) + SearchPhotos(ctx context.Context, req dto.PhotoSearchRequest) (*dto.PhotoSearchResponse, error) + + // 命令操作 + CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error) + UpdatePhoto(ctx context.Context, id uint, req dto.UpdatePhotoRequest) (*dto.PhotoResponse, error) + DeletePhoto(ctx context.Context, id uint) error + UpdatePhotoStatus(ctx context.Context, id uint, status string) error + + // 批量操作 + BatchUpdatePhotos(ctx context.Context, req dto.BatchUpdatePhotosRequest) error + BatchDeletePhotos(ctx context.Context, photoIDs []uint) error + + // 统计操作 + GetPhotoStats(ctx context.Context) (*dto.PhotoStatsResponse, error) +} + +// photoServiceImpl 照片应用服务实现 +type photoServiceImpl struct { + photoRepo repository.PhotoRepository + categoryRepo repository.CategoryRepository + tagRepo repository.TagRepository + photoDomainSvc *service.PhotoDomainService + imageProcessor ImageProcessor + cacheService cache.Service + eventPublisher event.Publisher + logger *logrus.Logger +} + +func NewPhotoService( + photoRepo repository.PhotoRepository, + categoryRepo repository.CategoryRepository, + tagRepo repository.TagRepository, + photoDomainSvc *service.PhotoDomainService, + imageProcessor ImageProcessor, + cacheService cache.Service, + eventPublisher event.Publisher, + logger *logrus.Logger, +) PhotoService { + return &photoServiceImpl{ + photoRepo: photoRepo, + categoryRepo: categoryRepo, + tagRepo: tagRepo, + photoDomainSvc: photoDomainSvc, + imageProcessor: imageProcessor, + cacheService: cacheService, + eventPublisher: eventPublisher, + logger: logger, + } +} + +// GetPhotos 获取照片列表 +func (s *photoServiceImpl) GetPhotos(ctx context.Context, req dto.PhotoListRequest) (*dto.PhotoListResponse, error) { + // 参数验证 + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + // 构建查询条件 + filter := s.buildPhotoFilter(req) + + // 尝试从缓存获取 + cacheKey := s.buildCacheKey("photos:list", filter) + if cached, err := s.cacheService.Get(ctx, cacheKey); err == nil { + var response dto.PhotoListResponse + if err := json.Unmarshal(cached, &response); err == nil { + return &response, nil + } + } + + // 从数据库查询 + photos, total, err := s.photoRepo.FindWithPagination(ctx, filter) + if err != nil { + s.logger.WithError(err).Error("Failed to get photos from repository") + return nil, fmt.Errorf("failed to get photos: %w", err) + } + + // 转换为DTO + photoResponses := make([]dto.PhotoSummary, 0, len(photos)) + for _, photo := range photos { + photoResponses = append(photoResponses, dto.NewPhotoSummary(photo)) + } + + response := &dto.PhotoListResponse{ + Photos: photoResponses, + Pagination: dto.PaginationResponse{ + Page: req.Page, + Limit: req.Limit, + Total: total, + TotalPages: (total + int64(req.Limit) - 1) / int64(req.Limit), + HasNext: req.Page < int((total+int64(req.Limit)-1)/int64(req.Limit)), + HasPrev: req.Page > 1, + }, + } + + // 缓存结果 + if data, err := json.Marshal(response); err == nil { + s.cacheService.Set(ctx, cacheKey, data, 10*time.Minute) + } + + return response, nil +} + +// CreatePhoto 创建照片 +func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error) { + // 参数验证 + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + // 构建领域命令 + cmd := service.CreatePhotoCommand{ + Title: req.Title, + Description: req.Description, + FilePath: req.FilePath, + OriginalFilename: req.OriginalFilename, + FileSize: req.FileSize, + MimeType: req.MimeType, + Location: req.Location, + Categories: req.Categories, + Tags: req.Tags, + TakenAt: req.TakenAt, + Metadata: req.Metadata, + } + + // 使用领域服务创建照片 + photo, err := s.photoDomainSvc.CreatePhoto(cmd) + if err != nil { + return nil, fmt.Errorf("failed to create photo: %w", err) + } + + // 开始数据库事务 + tx, err := s.photoRepo.BeginTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // 保存照片到数据库 + if err := s.photoRepo.Create(ctx, tx, photo); err != nil { + s.logger.WithError(err).Error("Failed to save photo to database") + return nil, fmt.Errorf("failed to save photo: %w", err) + } + + // 处理分类关联 + if len(req.Categories) > 0 { + if err := s.photoRepo.UpdateCategories(ctx, tx, photo.ID, req.Categories); err != nil { + return nil, fmt.Errorf("failed to update categories: %w", err) + } + } + + // 处理标签关联 + if len(req.Tags) > 0 { + if err := s.photoRepo.UpdateTags(ctx, tx, photo.ID, req.Tags); err != nil { + return nil, fmt.Errorf("failed to update tags: %w", err) + } + } + + // 提交事务 + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + // 异步处理图片 + go func() { + if err := s.imageProcessor.ProcessPhoto(photo); err != nil { + s.logger.WithError(err).WithField("photo_id", photo.ID).Error("Failed to process photo") + } + }() + + // 发布领域事件 + event := event.PhotoCreated{ + PhotoID: photo.ID.Value(), + Title: photo.Title, + CreatedAt: photo.CreatedAt, + } + s.eventPublisher.Publish(ctx, event) + + // 清除相关缓存 + s.invalidateCache(ctx, "photos:*", "categories:*", "stats:*") + + return dto.NewPhotoResponse(photo), nil +} +``` + +### 2.4 数据访问层设计 + +#### 2.4.1 Repository接口 +```go +// internal/repository/photo_repository.go +package repository + +import ( + "context" + "database/sql" + "photography-backend/internal/domain" +) + +// PhotoRepository 照片数据访问接口 +type PhotoRepository interface { + // 基础CRUD + Create(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error + Update(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error + Delete(ctx context.Context, tx *sql.Tx, id domain.PhotoID) error + + // 查询操作 + FindByID(ctx context.Context, id domain.PhotoID) (*domain.Photo, error) + FindBySlug(ctx context.Context, slug string) (*domain.Photo, error) + FindWithPagination(ctx context.Context, filter PhotoFilter) ([]*domain.Photo, int64, error) + Search(ctx context.Context, query string, filter PhotoFilter) ([]*domain.Photo, int64, error) + + // 关联操作 + UpdateCategories(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, categoryIDs []domain.CategoryID) error + UpdateTags(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, tagIDs []domain.TagID) error + UpdateFormats(ctx context.Context, tx *sql.Tx, photoID domain.PhotoID, formats []domain.PhotoFormat) error + + // 批量操作 + BatchUpdate(ctx context.Context, tx *sql.Tx, photoIDs []domain.PhotoID, updates map[string]interface{}) error + BatchDelete(ctx context.Context, tx *sql.Tx, photoIDs []domain.PhotoID) error + + // 统计操作 + Count(ctx context.Context, filter PhotoFilter) (int64, error) + GetStats(ctx context.Context) (*PhotoStats, error) + + // 事务管理 + BeginTx(ctx context.Context) (*sql.Tx, error) +} + +// PhotoFilter 照片查询过滤器 +type PhotoFilter struct { + Status *domain.PhotoStatus + Visibility *domain.Visibility + CategoryIDs []domain.CategoryID + TagIDs []domain.TagID + Search string + DateFrom *time.Time + DateTo *time.Time + Page int + Limit int + SortBy string + SortOrder string +} + +// PhotoStats 照片统计数据 +type PhotoStats struct { + TotalPhotos int64 + PublishedPhotos int64 + DraftPhotos int64 + ArchivedPhotos int64 + TotalViews int64 + TotalLikes int64 + StorageUsed int64 +} +``` + +#### 2.4.2 GORM实现 +```go +// internal/infrastructure/persistence/photo_repository_impl.go +package persistence + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "gorm.io/gorm" + "photography-backend/internal/domain" + "photography-backend/internal/repository" + "photography-backend/internal/infrastructure/persistence/model" +) + +// photoRepositoryImpl GORM实现的照片仓库 +type photoRepositoryImpl struct { + db *gorm.DB +} + +func NewPhotoRepository(db *gorm.DB) repository.PhotoRepository { + return &photoRepositoryImpl{db: db} +} + +// Create 创建照片 +func (r *photoRepositoryImpl) Create(ctx context.Context, tx *sql.Tx, photo *domain.Photo) error { + // 转换为数据库模型 + photoModel := model.FromDomainPhoto(photo) + + // 使用事务或普通连接 + db := r.getDB(tx) + + // 创建照片记录 + if err := db.WithContext(ctx).Create(&photoModel).Error; err != nil { + return fmt.Errorf("failed to create photo: %w", err) + } + + // 更新领域对象ID + photo.ID = domain.NewPhotoID(photoModel.ID) + + return nil +} + +// FindWithPagination 分页查询照片 +func (r *photoRepositoryImpl) FindWithPagination(ctx context.Context, filter repository.PhotoFilter) ([]*domain.Photo, int64, error) { + var photoModels []model.Photo + var total int64 + + // 构建查询 + query := r.db.WithContext(ctx).Model(&model.Photo{}) + + // 应用过滤条件 + query = r.applyFilters(query, filter) + + // 预加载关联数据 + query = query.Preload("Categories").Preload("Tags").Preload("Formats") + + // 统计总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count photos: %w", err) + } + + // 应用分页和排序 + offset := (filter.Page - 1) * filter.Limit + query = query.Offset(offset).Limit(filter.Limit) + + if filter.SortBy != "" { + order := fmt.Sprintf("%s %s", filter.SortBy, filter.SortOrder) + query = query.Order(order) + } else { + query = query.Order("created_at DESC") + } + + // 执行查询 + if err := query.Find(&photoModels).Error; err != nil { + return nil, 0, fmt.Errorf("failed to find photos: %w", err) + } + + // 转换为领域对象 + photos := make([]*domain.Photo, 0, len(photoModels)) + for _, photoModel := range photoModels { + photo := model.ToDomainPhoto(&photoModel) + photos = append(photos, photo) + } + + return photos, total, nil +} + +// applyFilters 应用查询过滤条件 +func (r *photoRepositoryImpl) applyFilters(query *gorm.DB, filter repository.PhotoFilter) *gorm.DB { + // 状态过滤 + if filter.Status != nil { + query = query.Where("status = ?", *filter.Status) + } + + // 可见性过滤 + if filter.Visibility != nil { + query = query.Where("visibility = ?", *filter.Visibility) + } + + // 分类过滤 + if len(filter.CategoryIDs) > 0 { + categoryIDs := make([]uint, len(filter.CategoryIDs)) + for i, id := range filter.CategoryIDs { + categoryIDs[i] = id.Value() + } + query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). + Where("photo_categories.category_id IN ?", categoryIDs) + } + + // 标签过滤 + if len(filter.TagIDs) > 0 { + tagIDs := make([]uint, len(filter.TagIDs)) + for i, id := range filter.TagIDs { + tagIDs[i] = id.Value() + } + query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). + Where("photo_tags.tag_id IN ?", tagIDs) + } + + // 搜索过滤 + if filter.Search != "" { + searchTerm := "%" + strings.ToLower(filter.Search) + "%" + query = query.Where("LOWER(title) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm) + } + + // 日期范围过滤 + if filter.DateFrom != nil { + query = query.Where("taken_at >= ?", *filter.DateFrom) + } + if filter.DateTo != nil { + query = query.Where("taken_at <= ?", *filter.DateTo) + } + + return query +} + +// getDB 获取数据库连接(事务或普通连接) +func (r *photoRepositoryImpl) getDB(tx *sql.Tx) *gorm.DB { + if tx != nil { + return r.db.Set("gorm:tx", tx) + } + return r.db +} +``` + +### 2.5 数据模型映射 + +#### 2.5.1 数据库模型 +```go +// internal/infrastructure/persistence/model/photo.go +package model + +import ( + "time" + "gorm.io/gorm" + "photography-backend/internal/domain" +) + +// Photo 照片数据库模型 +type Photo struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `gorm:"size:255;not null" json:"title"` + Description string `gorm:"type:text" json:"description"` + Slug string `gorm:"size:255;uniqueIndex" json:"slug"` + Status string `gorm:"size:20;default:published" json:"status"` + Visibility string `gorm:"size:20;default:public" json:"visibility"` + SortOrder int `gorm:"default:0" json:"sort_order"` + + // 文件信息 + OriginalFilename string `gorm:"size:255" json:"original_filename"` + FileSize int64 `json:"file_size"` + MimeType string `gorm:"size:100" json:"mime_type"` + FileHash string `gorm:"size:64" json:"file_hash"` + + // EXIF数据 + Camera string `gorm:"size:100" json:"camera"` + Lens string `gorm:"size:100" json:"lens"` + ISO int `json:"iso"` + Aperture string `gorm:"size:10" json:"aperture"` + ShutterSpeed string `gorm:"size:20" json:"shutter_speed"` + FocalLength string `gorm:"size:20" json:"focal_length"` + + // 位置信息 + Latitude *float64 `gorm:"type:decimal(10,8)" json:"latitude"` + Longitude *float64 `gorm:"type:decimal(11,8)" json:"longitude"` + LocationName string `gorm:"size:255" json:"location_name"` + Country string `gorm:"size:100" json:"country"` + City string `gorm:"size:100" json:"city"` + + // 统计信息 + ViewCount int `gorm:"default:0" json:"view_count"` + LikeCount int `gorm:"default:0" json:"like_count"` + DownloadCount int `gorm:"default:0" json:"download_count"` + + // 时间信息 + TakenAt *time.Time `json:"taken_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 元数据 + Metadata JSON `gorm:"type:jsonb" json:"metadata"` + + // 关联关系 + Categories []Category `gorm:"many2many:photo_categories" json:"categories"` + Tags []Tag `gorm:"many2many:photo_tags" json:"tags"` + Formats []PhotoFormat `gorm:"foreignKey:PhotoID" json:"formats"` +} + +// PhotoFormat 照片格式数据库模型 +type PhotoFormat struct { + ID uint `gorm:"primaryKey" json:"id"` + PhotoID uint `gorm:"not null;index" json:"photo_id"` + FormatType string `gorm:"size:20;not null" json:"format_type"` + FilePath string `gorm:"size:500;not null" json:"file_path"` + FileSize int64 `json:"file_size"` + Width int `json:"width"` + Height int `json:"height"` + Quality int `json:"quality"` + CreatedAt time.Time `json:"created_at"` + + // 唯一约束 + Photo Photo `gorm:"foreignKey:PhotoID" json:"-"` +} + +// JSON 自定义JSON类型 +type JSON map[string]interface{} + +// 实现GORM的Valuer和Scanner接口 +func (j JSON) Value() (driver.Value, error) { + if len(j) == 0 { + return nil, nil + } + return json.Marshal(j) +} + +func (j *JSON) Scan(value interface{}) error { + if value == nil { + *j = nil + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("cannot scan %T into JSON", value) + } + + return json.Unmarshal(bytes, j) +} + +// FromDomainPhoto 从领域对象转换为数据库模型 +func FromDomainPhoto(photo *domain.Photo) *Photo { + model := &Photo{ + ID: photo.ID.Value(), + Title: photo.Title, + Description: photo.Description, + Slug: photo.Slug, + Status: string(photo.Status), + Visibility: string(photo.Visibility), + OriginalFilename: photo.FileInfo.OriginalFilename, + FileSize: photo.FileInfo.FileSize, + MimeType: photo.FileInfo.MimeType, + FileHash: photo.FileInfo.FileHash, + ViewCount: photo.Stats.ViewCount, + LikeCount: photo.Stats.LikeCount, + DownloadCount: photo.Stats.DownloadCount, + TakenAt: photo.TakenAt, + CreatedAt: photo.CreatedAt, + UpdatedAt: photo.UpdatedAt, + Metadata: JSON(photo.Metadata), + } + + // EXIF数据 + if photo.EXIF != nil { + model.Camera = photo.EXIF.Camera + model.Lens = photo.EXIF.Lens + model.ISO = photo.EXIF.ISO + model.Aperture = photo.EXIF.Aperture + model.ShutterSpeed = photo.EXIF.ShutterSpeed + model.FocalLength = photo.EXIF.FocalLength + } + + // 位置信息 + if photo.Location != nil { + model.Latitude = &photo.Location.Latitude + model.Longitude = &photo.Location.Longitude + model.LocationName = photo.Location.Name + model.Country = photo.Location.Country + model.City = photo.Location.City + } + + // 转换格式信息 + formats := make([]PhotoFormat, 0, len(photo.Formats)) + for _, format := range photo.Formats { + formats = append(formats, PhotoFormat{ + PhotoID: photo.ID.Value(), + FormatType: string(format.Type), + FilePath: format.FilePath, + FileSize: format.FileSize, + Width: format.Width, + Height: format.Height, + Quality: format.Quality, + }) + } + model.Formats = formats + + return model +} + +// ToDomainPhoto 从数据库模型转换为领域对象 +func ToDomainPhoto(model *Photo) *domain.Photo { + photo := &domain.Photo{ + ID: domain.NewPhotoID(model.ID), + Title: model.Title, + Description: model.Description, + Slug: model.Slug, + Status: domain.PhotoStatus(model.Status), + Visibility: domain.Visibility(model.Visibility), + FileInfo: domain.FileInfo{ + OriginalFilename: model.OriginalFilename, + FileSize: model.FileSize, + MimeType: model.MimeType, + FileHash: model.FileHash, + }, + Stats: domain.PhotoStats{ + ViewCount: model.ViewCount, + LikeCount: model.LikeCount, + DownloadCount: model.DownloadCount, + }, + TakenAt: model.TakenAt, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + Metadata: map[string]interface{}(model.Metadata), + } + + // EXIF数据 + if model.Camera != "" || model.Lens != "" || model.ISO != 0 { + photo.EXIF = &domain.EXIFData{ + Camera: model.Camera, + Lens: model.Lens, + ISO: model.ISO, + Aperture: model.Aperture, + ShutterSpeed: model.ShutterSpeed, + FocalLength: model.FocalLength, + } + } + + // 位置信息 + if model.Latitude != nil && model.Longitude != nil { + photo.Location = &domain.Location{ + Name: model.LocationName, + Latitude: *model.Latitude, + Longitude: *model.Longitude, + Country: model.Country, + City: model.City, + } + } + + // 转换格式信息 + formats := make([]domain.PhotoFormat, 0, len(model.Formats)) + for _, format := range model.Formats { + formats = append(formats, domain.PhotoFormat{ + Type: domain.FormatType(format.FormatType), + FilePath: format.FilePath, + FileSize: format.FileSize, + Width: format.Width, + Height: format.Height, + Quality: format.Quality, + }) + } + photo.Formats = formats + + // 转换分类ID + categoryIDs := make([]domain.CategoryID, 0, len(model.Categories)) + for _, category := range model.Categories { + categoryIDs = append(categoryIDs, domain.NewCategoryID(category.ID)) + } + photo.Categories = categoryIDs + + // 转换标签ID + tagIDs := make([]domain.TagID, 0, len(model.Tags)) + for _, tag := range model.Tags { + tagIDs = append(tagIDs, domain.NewTagID(tag.ID)) + } + photo.Tags = tagIDs + + return photo +} +``` + +### 2.6 依赖注入与容器 + +#### 2.6.1 依赖注入容器 +```go +// pkg/container/container.go +package container + +import ( + "log" + "photography-backend/internal/api/handlers" + "photography-backend/internal/service" + "photography-backend/internal/repository" + "photography-backend/internal/infrastructure/persistence" + "photography-backend/pkg/config" + "photography-backend/pkg/database" + "photography-backend/pkg/cache" + "photography-backend/pkg/storage" + "photography-backend/pkg/logger" +) + +// Container 依赖注入容器 +type Container struct { + config *config.Config + + // 基础设施 + db *gorm.DB + redisClient *redis.Client + logger *logrus.Logger + + // 服务 + cacheService cache.Service + storageService storage.Service + + // 仓库 + photoRepo repository.PhotoRepository + categoryRepo repository.CategoryRepository + tagRepo repository.TagRepository + userRepo repository.UserRepository + + // 应用服务 + photoService service.PhotoService + categoryService service.CategoryService + tagService service.TagService + authService service.AuthService + uploadService service.UploadService + + // 处理器 + photoHandler *handlers.PhotoHandler + categoryHandler *handlers.CategoryHandler + tagHandler *handlers.TagHandler + authHandler *handlers.AuthHandler + uploadHandler *handlers.UploadHandler +} + +// NewContainer 创建新的容器 +func NewContainer(cfg *config.Config) *Container { + container := &Container{ + config: cfg, + } + + container.initInfrastructure() + container.initRepositories() + container.initServices() + container.initHandlers() + + return container +} + +// initInfrastructure 初始化基础设施 +func (c *Container) initInfrastructure() { + // 初始化日志器 + c.logger = logger.NewLogger(c.config.Logger) + + // 初始化数据库 + db, err := database.NewPostgresDB(c.config.Database) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + c.db = db + + // 初始化Redis + redisClient, err := cache.NewRedisClient(c.config.Redis) + if err != nil { + log.Fatal("Failed to connect to Redis:", err) + } + c.redisClient = redisClient + + // 初始化缓存服务 + c.cacheService = cache.NewCacheService(c.redisClient, c.logger) + + // 初始化存储服务 + c.storageService = storage.NewStorageService(c.config.Storage, c.logger) +} + +// initRepositories 初始化仓库 +func (c *Container) initRepositories() { + c.photoRepo = persistence.NewPhotoRepository(c.db) + c.categoryRepo = persistence.NewCategoryRepository(c.db) + c.tagRepo = persistence.NewTagRepository(c.db) + c.userRepo = persistence.NewUserRepository(c.db) +} + +// initServices 初始化服务 +func (c *Container) initServices() { + // 领域服务 + slugGenerator := service.NewSlugGenerator() + exifExtractor := service.NewEXIFExtractor() + photoDomainService := service.NewPhotoDomainService(slugGenerator, exifExtractor) + + // 图片处理器 + imageProcessor := service.NewImageProcessor(c.storageService, c.logger) + + // 事件发布器 + eventPublisher := event.NewEventPublisher(c.redisClient, c.logger) + + // 应用服务 + c.photoService = service.NewPhotoService( + c.photoRepo, + c.categoryRepo, + c.tagRepo, + photoDomainService, + imageProcessor, + c.cacheService, + eventPublisher, + c.logger, + ) + + c.categoryService = service.NewCategoryService( + c.categoryRepo, + c.photoRepo, + c.cacheService, + c.logger, + ) + + c.tagService = service.NewTagService( + c.tagRepo, + c.photoRepo, + c.cacheService, + c.logger, + ) + + c.authService = service.NewAuthService( + c.userRepo, + c.config.JWT, + c.logger, + ) + + c.uploadService = service.NewUploadService( + c.storageService, + imageProcessor, + c.logger, + ) +} + +// initHandlers 初始化处理器 +func (c *Container) initHandlers() { + c.photoHandler = handlers.NewPhotoHandler(c.photoService, c.logger) + c.categoryHandler = handlers.NewCategoryHandler(c.categoryService, c.logger) + c.tagHandler = handlers.NewTagHandler(c.tagService, c.logger) + c.authHandler = handlers.NewAuthHandler(c.authService, c.logger) + c.uploadHandler = handlers.NewUploadHandler(c.uploadService, c.logger) +} + +// GetPhotoHandler 获取照片处理器 +func (c *Container) GetPhotoHandler() *handlers.PhotoHandler { + return c.photoHandler +} + +// ... 其他getter方法 + +// Close 关闭容器资源 +func (c *Container) Close() error { + if c.redisClient != nil { + c.redisClient.Close() + } + + if c.db != nil { + sqlDB, err := c.db.DB() + if err == nil { + sqlDB.Close() + } + } + + return nil +} +``` + +### 2.7 配置管理 + +#### 2.7.1 配置结构 +```go +// pkg/config/config.go +package config + +import ( + "fmt" + "time" + "github.com/spf13/viper" +) + +// Config 应用配置 +type Config struct { + App AppConfig `mapstructure:"app"` + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` + Storage StorageConfig `mapstructure:"storage"` + JWT JWTConfig `mapstructure:"jwt"` + Logger LoggerConfig `mapstructure:"logger"` + Upload UploadConfig `mapstructure:"upload"` + Image ImageConfig `mapstructure:"image"` +} + +// AppConfig 应用配置 +type AppConfig struct { + Name string `mapstructure:"name"` + Version string `mapstructure:"version"` + Environment string `mapstructure:"environment"` + Debug bool `mapstructure:"debug"` +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` + ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` + CORS CORSConfig `mapstructure:"cors"` +} + +// CORSConfig CORS配置 +type CORSConfig struct { + AllowOrigins []string `mapstructure:"allow_origins"` + AllowMethods []string `mapstructure:"allow_methods"` + AllowHeaders []string `mapstructure:"allow_headers"` + ExposeHeaders []string `mapstructure:"expose_headers"` + AllowCredentials bool `mapstructure:"allow_credentials"` + MaxAge int `mapstructure:"max_age"` +} + +// DatabaseConfig 数据库配置 +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Database string `mapstructure:"database"` + SSLMode string `mapstructure:"ssl_mode"` + MaxOpenConns int `mapstructure:"max_open_conns"` + MaxIdleConns int `mapstructure:"max_idle_conns"` + ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` + LogLevel string `mapstructure:"log_level"` +} + +// RedisConfig Redis配置 +type RedisConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Password string `mapstructure:"password"` + Database int `mapstructure:"database"` + MaxRetries int `mapstructure:"max_retries"` + PoolSize int `mapstructure:"pool_size"` + IdleTimeout time.Duration `mapstructure:"idle_timeout"` +} + +// StorageConfig 存储配置 +type StorageConfig struct { + Type string `mapstructure:"type"` // local, s3, minio + Local LocalConfig `mapstructure:"local"` + S3 S3Config `mapstructure:"s3"` + CDNBaseURL string `mapstructure:"cdn_base_url"` +} + +// LocalConfig 本地存储配置 +type LocalConfig struct { + UploadDir string `mapstructure:"upload_dir"` + BaseURL string `mapstructure:"base_url"` +} + +// S3Config S3/MinIO配置 +type S3Config struct { + Endpoint string `mapstructure:"endpoint"` + Region string `mapstructure:"region"` + AccessKeyID string `mapstructure:"access_key_id"` + SecretAccessKey string `mapstructure:"secret_access_key"` + Bucket string `mapstructure:"bucket"` + UseSSL bool `mapstructure:"use_ssl"` +} + +// JWTConfig JWT配置 +type JWTConfig struct { + SecretKey string `mapstructure:"secret_key"` + Issuer string `mapstructure:"issuer"` + AccessDuration time.Duration `mapstructure:"access_duration"` + RefreshDuration time.Duration `mapstructure:"refresh_duration"` +} + +// LoggerConfig 日志配置 +type LoggerConfig struct { + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` // json, text + Output string `mapstructure:"output"` // stdout, file + Filename string `mapstructure:"filename"` + MaxSize int `mapstructure:"max_size"` + MaxAge int `mapstructure:"max_age"` + Compress bool `mapstructure:"compress"` +} + +// UploadConfig 上传配置 +type UploadConfig struct { + MaxFileSize int64 `mapstructure:"max_file_size"` + AllowedTypes []string `mapstructure:"allowed_types"` + MaxFilesPerBatch int `mapstructure:"max_files_per_batch"` + TempDir string `mapstructure:"temp_dir"` + CleanupInterval time.Duration `mapstructure:"cleanup_interval"` +} + +// ImageConfig 图片处理配置 +type ImageConfig struct { + QualityJPG int `mapstructure:"quality_jpg"` + QualityWebP int `mapstructure:"quality_webp"` + MaxWidth int `mapstructure:"max_width"` + MaxHeight int `mapstructure:"max_height"` + + ThumbnailSizes map[string]ImageSize `mapstructure:"thumbnail_sizes"` + + WatermarkEnabled bool `mapstructure:"watermark_enabled"` + WatermarkText string `mapstructure:"watermark_text"` + WatermarkOpacity int `mapstructure:"watermark_opacity"` +} + +// ImageSize 图片尺寸配置 +type ImageSize struct { + Width int `mapstructure:"width"` + Height int `mapstructure:"height"` + Quality int `mapstructure:"quality"` +} + +// LoadConfig 加载配置 +func LoadConfig(configPath string) (*Config, error) { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(configPath) + viper.AddConfigPath(".") + viper.AddConfigPath("./config") + + // 设置环境变量 + viper.AutomaticEnv() + viper.SetEnvPrefix("PHOTOGRAPHY") + + // 设置默认值 + setDefaults() + + // 读取配置文件 + if err := viper.ReadInConfig(); err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + // 验证配置 + if err := validateConfig(&config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + return &config, nil +} + +// setDefaults 设置默认配置值 +func setDefaults() { + // 应用默认配置 + viper.SetDefault("app.name", "photography-backend") + viper.SetDefault("app.version", "1.0.0") + viper.SetDefault("app.environment", "development") + viper.SetDefault("app.debug", false) + + // 服务器默认配置 + viper.SetDefault("server.host", "0.0.0.0") + viper.SetDefault("server.port", 8080) + viper.SetDefault("server.read_timeout", "30s") + viper.SetDefault("server.write_timeout", "30s") + viper.SetDefault("server.shutdown_timeout", "10s") + + // CORS默认配置 + viper.SetDefault("server.cors.allow_origins", []string{"*"}) + viper.SetDefault("server.cors.allow_methods", []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}) + viper.SetDefault("server.cors.allow_headers", []string{"Content-Type", "Authorization"}) + viper.SetDefault("server.cors.max_age", 3600) + + // 数据库默认配置 + viper.SetDefault("database.host", "localhost") + viper.SetDefault("database.port", 5432) + viper.SetDefault("database.ssl_mode", "disable") + viper.SetDefault("database.max_open_conns", 25) + viper.SetDefault("database.max_idle_conns", 5) + viper.SetDefault("database.conn_max_lifetime", "3600s") + viper.SetDefault("database.log_level", "warn") + + // Redis默认配置 + viper.SetDefault("redis.host", "localhost") + viper.SetDefault("redis.port", 6379) + viper.SetDefault("redis.database", 0) + viper.SetDefault("redis.max_retries", 3) + viper.SetDefault("redis.pool_size", 10) + viper.SetDefault("redis.idle_timeout", "300s") + + // 存储默认配置 + viper.SetDefault("storage.type", "local") + viper.SetDefault("storage.local.upload_dir", "./uploads") + viper.SetDefault("storage.local.base_url", "http://localhost:8080") + + // JWT默认配置 + viper.SetDefault("jwt.issuer", "photography-backend") + viper.SetDefault("jwt.access_duration", "24h") + viper.SetDefault("jwt.refresh_duration", "168h") + + // 日志默认配置 + viper.SetDefault("logger.level", "info") + viper.SetDefault("logger.format", "json") + viper.SetDefault("logger.output", "stdout") + viper.SetDefault("logger.max_size", 100) + viper.SetDefault("logger.max_age", 30) + viper.SetDefault("logger.compress", true) + + // 上传默认配置 + viper.SetDefault("upload.max_file_size", 52428800) // 50MB + viper.SetDefault("upload.allowed_types", []string{"image/jpeg", "image/png", "image/raw"}) + viper.SetDefault("upload.max_files_per_batch", 50) + viper.SetDefault("upload.temp_dir", "./temp") + viper.SetDefault("upload.cleanup_interval", "1h") + + // 图片处理默认配置 + viper.SetDefault("image.quality_jpg", 85) + viper.SetDefault("image.quality_webp", 80) + viper.SetDefault("image.max_width", 1920) + viper.SetDefault("image.max_height", 1080) + viper.SetDefault("image.watermark_enabled", false) + viper.SetDefault("image.watermark_opacity", 50) + + // 缩略图尺寸默认配置 + viper.SetDefault("image.thumbnail_sizes.thumb_small.width", 150) + viper.SetDefault("image.thumbnail_sizes.thumb_small.height", 150) + viper.SetDefault("image.thumbnail_sizes.thumb_small.quality", 80) + + viper.SetDefault("image.thumbnail_sizes.thumb_medium.width", 300) + viper.SetDefault("image.thumbnail_sizes.thumb_medium.height", 300) + viper.SetDefault("image.thumbnail_sizes.thumb_medium.quality", 85) + + viper.SetDefault("image.thumbnail_sizes.thumb_large.width", 600) + viper.SetDefault("image.thumbnail_sizes.thumb_large.height", 600) + viper.SetDefault("image.thumbnail_sizes.thumb_large.quality", 90) +} + +// validateConfig 验证配置 +func validateConfig(config *Config) error { + if config.App.Name == "" { + return fmt.Errorf("app name cannot be empty") + } + + if config.Server.Port <= 0 || config.Server.Port > 65535 { + return fmt.Errorf("invalid server port: %d", config.Server.Port) + } + + if config.Database.Host == "" { + return fmt.Errorf("database host cannot be empty") + } + + if config.JWT.SecretKey == "" { + return fmt.Errorf("JWT secret key cannot be empty") + } + + return nil +} +``` + +### 2.8 构建和部署 + +#### 2.8.1 Makefile +```makefile +# Makefile +.PHONY: build test clean run docker-build docker-run migrate-up migrate-down + +# 变量定义 +APP_NAME = photography-backend +VERSION = $(shell git describe --tags --always --dirty) +BUILD_TIME = $(shell date +%Y-%m-%dT%H:%M:%S) +GO_VERSION = $(shell go version | awk '{print $$3}') +LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)" + +# 构建 +build: + @echo "Building $(APP_NAME)..." + @go build $(LDFLAGS) -o bin/$(APP_NAME) cmd/server/main.go + +# 运行 +run: + @echo "Running $(APP_NAME)..." + @go run cmd/server/main.go + +# 测试 +test: + @echo "Running tests..." + @go test -v -race -coverprofile=coverage.out ./... + +# 测试覆盖率 +test-coverage: test + @go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +# 清理 +clean: + @echo "Cleaning..." + @rm -rf bin/ + @rm -f coverage.out coverage.html + @go clean -cache + +# 代码检查 +lint: + @echo "Running linter..." + @golangci-lint run + +# 格式化代码 +fmt: + @echo "Formatting code..." + @go fmt ./... + @goimports -w . + +# 生成模拟代码 +generate: + @echo "Generating mocks..." + @go generate ./... + +# 数据库迁移 +migrate-up: + @echo "Running database migrations..." + @migrate -path migrations -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" up + +migrate-down: + @echo "Rolling back database migrations..." + @migrate -path migrations -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" down + +# 创建新的迁移文件 +migrate-create: + @echo "Creating new migration: $(NAME)" + @migrate create -ext sql -dir migrations $(NAME) + +# Docker构建 +docker-build: + @echo "Building Docker image..." + @docker build -t $(APP_NAME):$(VERSION) -t $(APP_NAME):latest . + +# Docker运行 +docker-run: + @echo "Running Docker container..." + @docker-compose up -d + +# Docker停止 +docker-stop: + @echo "Stopping Docker containers..." + @docker-compose down + +# 安装依赖 +deps: + @echo "Installing dependencies..." + @go mod download + @go mod tidy + +# 安装开发工具 +install-tools: + @echo "Installing development tools..." + @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @go install golang.org/x/tools/cmd/goimports@latest + @go install github.com/golang/mock/mockgen@latest + @go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest + +# 开发环境设置 +dev-setup: install-tools deps + @echo "Setting up development environment..." + @cp config/config.example.yaml config/config.yaml + @mkdir -p uploads temp logs + +# 生产构建 +build-prod: + @echo "Building for production..." + @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -a -installsuffix cgo -o bin/$(APP_NAME) cmd/server/main.go + +# 帮助信息 +help: + @echo "Available commands:" + @echo " build - Build the application" + @echo " run - Run the application" + @echo " test - Run tests" + @echo " test-coverage - Run tests with coverage report" + @echo " clean - Clean build artifacts" + @echo " lint - Run linter" + @echo " fmt - Format code" + @echo " generate - Generate mocks" + @echo " migrate-up - Run database migrations" + @echo " migrate-down - Rollback database migrations" + @echo " migrate-create- Create new migration file (NAME=migration_name)" + @echo " docker-build - Build Docker image" + @echo " docker-run - Run with Docker Compose" + @echo " docker-stop - Stop Docker containers" + @echo " deps - Install dependencies" + @echo " install-tools - Install development tools" + @echo " dev-setup - Setup development environment" + @echo " build-prod - Build for production" + @echo " help - Show this help message" +``` + +#### 2.8.2 Dockerfile +```dockerfile +# 多阶段构建 +FROM golang:1.21-alpine AS builder + +# 安装必要的包 +RUN apk add --no-cache git ca-certificates tzdata vips-dev gcc musl-dev + +# 设置工作目录 +WORKDIR /app + +# 复制go.mod和go.sum +COPY go.mod go.sum ./ + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go + +# 最终镜像 +FROM alpine:latest + +# 安装运行时依赖 +RUN apk --no-cache add ca-certificates vips-dev tzdata + +# 创建非root用户 +RUN addgroup -g 1000 appgroup && \ + adduser -u 1000 -G appgroup -s /bin/sh -D appuser + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/main . + +# 复制配置文件和迁移文件 +COPY --from=builder /app/config ./config +COPY --from=builder /app/migrations ./migrations + +# 创建必要的目录 +RUN mkdir -p uploads temp logs && \ + chown -R appuser:appgroup /app + +# 切换到非root用户 +USER appuser + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# 运行应用 +CMD ["./main"] +``` + +#### 2.8.3 docker-compose.yml +```yaml +version: '3.8' + +services: + # 应用服务 + app: + build: . + ports: + - "8080:8080" + environment: + - PHOTOGRAPHY_DATABASE_HOST=postgres + - PHOTOGRAPHY_DATABASE_PORT=5432 + - PHOTOGRAPHY_DATABASE_USER=postgres + - PHOTOGRAPHY_DATABASE_PASSWORD=password + - PHOTOGRAPHY_DATABASE_DATABASE=photography + - PHOTOGRAPHY_REDIS_HOST=redis + - PHOTOGRAPHY_REDIS_PORT=6379 + - PHOTOGRAPHY_STORAGE_TYPE=minio + - PHOTOGRAPHY_STORAGE_S3_ENDPOINT=minio:9000 + - PHOTOGRAPHY_STORAGE_S3_ACCESS_KEY_ID=minioadmin + - PHOTOGRAPHY_STORAGE_S3_SECRET_ACCESS_KEY=minioadmin + - PHOTOGRAPHY_STORAGE_S3_BUCKET=photography + - PHOTOGRAPHY_STORAGE_S3_USE_SSL=false + depends_on: + - postgres + - redis + - minio + volumes: + - ./uploads:/app/uploads + - ./logs:/app/logs + restart: unless-stopped + networks: + - photography-network + + # PostgreSQL数据库 + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=photography + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + restart: unless-stopped + networks: + - photography-network + + # Redis缓存 + redis: + image: redis:7-alpine + command: redis-server --appendonly yes + volumes: + - redis_data:/data + ports: + - "6379:6379" + restart: unless-stopped + networks: + - photography-network + + # MinIO对象存储 + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - minio_data:/data + ports: + - "9000:9000" + - "9001:9001" + restart: unless-stopped + networks: + - photography-network + + # Nginx反向代理(可选) + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - app + restart: unless-stopped + networks: + - photography-network + +volumes: + postgres_data: + redis_data: + minio_data: + +networks: + photography-network: + driver: bridge +``` + +### 2.9 总结 + + + +[{"id": "1", "content": "\u521b\u5efa\u8be6\u7ec6\u7684\u540e\u7aef\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "2", "content": "\u521b\u5efa\u7ba1\u7406\u540e\u53f0\u529f\u80fd\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "3", "content": "\u521b\u5efa\u6570\u636e\u5e93\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "4", "content": "\u521b\u5efaAPI\u63a5\u53e3\u8bbe\u8ba1\u6587\u6863", "status": "completed", "priority": "high"}, {"id": "5", "content": "\u521b\u5efaGolang\u9879\u76ee\u67b6\u6784\u6587\u6863", "status": "completed", "priority": "high"}] \ No newline at end of file diff --git a/docs/v1/database/数据库设计文档.md b/docs/v1/database/数据库设计文档.md new file mode 100644 index 0000000..e2c90f7 --- /dev/null +++ b/docs/v1/database/数据库设计文档.md @@ -0,0 +1,1699 @@ +# 摄影作品集网站 - 数据库设计文档 + +## 1. 数据库概述 + +### 1.1 设计目标 +- **高性能**: 支持大量图片数据的快速查询和检索 +- **可扩展**: 支持未来功能扩展和数据增长 +- **完整性**: 确保数据一致性和引用完整性 +- **安全性**: 支持用户权限管理和数据安全 + +### 1.2 技术选型 +- **主数据库**: PostgreSQL 15+ +- **缓存数据库**: Redis 7+ +- **搜索引擎**: PostgreSQL 全文搜索 (可选ElasticSearch) +- **文件存储**: MinIO/AWS S3 + 本地存储 + +### 1.3 数据库命名规范 +- **表名**: 使用复数形式,小写字母,下划线分隔 +- **字段名**: 小写字母,下划线分隔,避免保留字 +- **索引名**: `idx_表名_字段名` 格式 +- **外键名**: `fk_表名_字段名` 格式 + +## 2. 数据库架构设计 + +### 2.1 数据库分层架构 +``` +┌─────────────────────────────────────────────────────────┐ +│ 应用层 (Golang Backend) │ +├─────────────────────────────────────────────────────────┤ +│ 缓存层 (Redis) │ +│ ├─ 会话缓存 (Session Storage) │ +│ ├─ 数据缓存 (Data Cache) │ +│ ├─ 队列系统 (Task Queue) │ +│ └─ 计数器 (Counters) │ +├─────────────────────────────────────────────────────────┤ +│ 数据层 (PostgreSQL) │ +│ ├─ 核心业务表 (Photos, Categories, Tags) │ +│ ├─ 用户管理表 (Users, Roles) │ +│ ├─ 系统配置表 (Settings) │ +│ └─ 日志审计表 (Logs) │ +├─────────────────────────────────────────────────────────┤ +│ 存储层 (MinIO/S3) │ +│ ├─ 原始文件存储 │ +│ ├─ 处理后文件存储 │ +│ └─ 缓存文件存储 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 数据库连接配置 +```yaml +# database.yml +development: + host: localhost + port: 5432 + database: photography_dev + username: postgres + password: password + sslmode: disable + max_open_conns: 25 + max_idle_conns: 5 + conn_max_lifetime: 3600 + +production: + host: ${DB_HOST} + port: ${DB_PORT} + database: photography_prod + username: ${DB_USER} + password: ${DB_PASSWORD} + sslmode: require + max_open_conns: 100 + max_idle_conns: 10 + conn_max_lifetime: 3600 +``` + +## 3. 核心数据表设计 + +### 3.1 照片主表 (photos) + +#### 3.1.1 表结构 +```sql +CREATE TABLE photos ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + slug VARCHAR(255) UNIQUE, + + -- 文件信息 + original_filename VARCHAR(255), + file_size BIGINT, + mime_type VARCHAR(100), + + -- 状态管理 + status VARCHAR(20) DEFAULT 'published' CHECK (status IN ('published', 'draft', 'archived', 'processing')), + visibility VARCHAR(20) DEFAULT 'public' CHECK (visibility IN ('public', 'private', 'password')), + sort_order INTEGER DEFAULT 0, + + -- 时间信息 + taken_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- EXIF 元数据 + camera VARCHAR(100), + lens VARCHAR(100), + iso INTEGER, + aperture VARCHAR(10), + shutter_speed VARCHAR(20), + focal_length VARCHAR(20), + + -- 位置信息 + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + location_name VARCHAR(255), + country VARCHAR(100), + city VARCHAR(100), + + -- 统计信息 + view_count INTEGER DEFAULT 0, + like_count INTEGER DEFAULT 0, + download_count INTEGER DEFAULT 0, + + -- 全文搜索 + search_vector TSVECTOR, + + -- 元数据JSON (扩展信息) + metadata JSONB, + + -- 索引 + CONSTRAINT photos_title_not_empty CHECK (LENGTH(title) > 0), + CONSTRAINT photos_valid_coordinates CHECK ( + (latitude IS NULL AND longitude IS NULL) OR + (latitude IS NOT NULL AND longitude IS NOT NULL AND + latitude >= -90 AND latitude <= 90 AND + longitude >= -180 AND longitude <= 180) + ) +); + +-- 索引优化 +CREATE INDEX idx_photos_status ON photos(status); +CREATE INDEX idx_photos_visibility ON photos(visibility); +CREATE INDEX idx_photos_created_at ON photos(created_at); +CREATE INDEX idx_photos_taken_at ON photos(taken_at); +CREATE INDEX idx_photos_status_created_at ON photos(status, created_at); +CREATE INDEX idx_photos_location ON photos(latitude, longitude); +CREATE INDEX idx_photos_search_vector ON photos USING gin(search_vector); +CREATE INDEX idx_photos_metadata ON photos USING gin(metadata); + +-- 更新时间触发器 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_photos_updated_at + BEFORE UPDATE ON photos + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +#### 3.1.2 字段说明 +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| id | SERIAL | 主键,自增 | 1 | +| title | VARCHAR(255) | 照片标题 | "城市夜景" | +| description | TEXT | 详细描述 | "拍摄于上海外滩..." | +| slug | VARCHAR(255) | URL友好的唯一标识 | "city-night-view-001" | +| original_filename | VARCHAR(255) | 原始文件名 | "DSC_0001.jpg" | +| file_size | BIGINT | 文件大小(字节) | 2048576 | +| mime_type | VARCHAR(100) | MIME类型 | "image/jpeg" | +| status | VARCHAR(20) | 发布状态 | "published" | +| visibility | VARCHAR(20) | 可见性 | "public" | +| taken_at | TIMESTAMP | 拍摄时间 | "2024-01-15 18:30:00" | +| camera | VARCHAR(100) | 相机型号 | "Canon EOS R5" | +| lens | VARCHAR(100) | 镜头信息 | "RF 24-70mm f/2.8L" | +| iso | INTEGER | ISO值 | 800 | +| aperture | VARCHAR(10) | 光圈值 | "f/2.8" | +| shutter_speed | VARCHAR(20) | 快门速度 | "1/125" | +| focal_length | VARCHAR(20) | 焦距 | "50mm" | +| latitude | DECIMAL(10,8) | 纬度 | 31.23037000 | +| longitude | DECIMAL(11,8) | 经度 | 121.47370000 | +| location_name | VARCHAR(255) | 地点名称 | "上海外滩" | +| search_vector | TSVECTOR | 全文搜索向量 | 自动生成 | +| metadata | JSONB | 扩展元数据 | {"weather": "sunny"} | + +### 3.2 照片格式表 (photo_formats) + +#### 3.2.1 表结构 +```sql +CREATE TABLE photo_formats ( + id SERIAL PRIMARY KEY, + photo_id INTEGER NOT NULL REFERENCES photos(id) ON DELETE CASCADE, + format_type VARCHAR(20) NOT NULL CHECK (format_type IN ( + 'original', 'raw', 'jpg', 'webp', 'avif', + 'thumb_small', 'thumb_medium', 'thumb_large', + 'display', 'watermark' + )), + + -- 文件信息 + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + file_hash VARCHAR(64), -- SHA256哈希 + + -- 图片属性 + width INTEGER, + height INTEGER, + quality INTEGER, + + -- 处理信息 + processing_status VARCHAR(20) DEFAULT 'completed' CHECK (processing_status IN ( + 'pending', 'processing', 'completed', 'failed' + )), + processing_error TEXT, + + -- 存储信息 + storage_type VARCHAR(20) DEFAULT 'local' CHECK (storage_type IN ('local', 's3', 'minio')), + storage_bucket VARCHAR(100), + storage_path VARCHAR(500), + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 唯一约束 + UNIQUE(photo_id, format_type), + + -- 检查约束 + CONSTRAINT photo_formats_positive_dimensions CHECK ( + width > 0 AND height > 0 + ), + CONSTRAINT photo_formats_valid_quality CHECK ( + quality IS NULL OR (quality >= 1 AND quality <= 100) + ) +); + +-- 索引优化 +CREATE INDEX idx_photo_formats_photo_id ON photo_formats(photo_id); +CREATE INDEX idx_photo_formats_format_type ON photo_formats(format_type); +CREATE INDEX idx_photo_formats_processing_status ON photo_formats(processing_status); +CREATE INDEX idx_photo_formats_storage_type ON photo_formats(storage_type); +CREATE INDEX idx_photo_formats_hash ON photo_formats(file_hash); +``` + +#### 3.2.2 格式类型说明 +| 格式类型 | 说明 | 尺寸 | 用途 | +|----------|------|------|------| +| original | 原始上传文件 | 原始尺寸 | 备份和再处理 | +| raw | RAW格式文件 | 原始尺寸 | 专业编辑 | +| jpg | 高质量JPEG | 最大1920px | 主要展示 | +| webp | WebP格式 | 最大1920px | 现代浏览器优化 | +| avif | AVIF格式 | 最大1920px | 次世代格式 | +| thumb_small | 小缩略图 | 150x150 | 列表预览 | +| thumb_medium | 中缩略图 | 300x300 | 网格展示 | +| thumb_large | 大缩略图 | 600x600 | 详情预览 | +| display | 展示版本 | 1200px | 灯箱展示 | +| watermark | 水印版本 | 1920px | 带水印展示 | + +### 3.3 分类表 (categories) + +#### 3.3.1 表结构 +```sql +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + slug VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + + -- 层级结构 + parent_id INTEGER REFERENCES categories(id) ON DELETE SET NULL, + level INTEGER DEFAULT 0, + path VARCHAR(500), -- 存储完整路径,如 "1.2.3" + + -- 展示信息 + cover_photo_id INTEGER REFERENCES photos(id) ON DELETE SET NULL, + color VARCHAR(7), -- 十六进制颜色代码 + icon VARCHAR(50), -- 图标类名 + + -- 排序和状态 + sort_order INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + is_featured BOOLEAN DEFAULT FALSE, + + -- 统计信息 + photo_count INTEGER DEFAULT 0, + direct_photo_count INTEGER DEFAULT 0, -- 直接关联的照片数 + + -- SEO信息 + seo_title VARCHAR(255), + seo_description TEXT, + seo_keywords VARCHAR(500), + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 检查约束 + CONSTRAINT categories_name_not_empty CHECK (LENGTH(name) > 0), + CONSTRAINT categories_valid_color CHECK ( + color IS NULL OR color ~ '^#[0-9A-Fa-f]{6}$' + ), + CONSTRAINT categories_no_self_parent CHECK (id != parent_id) +); + +-- 索引优化 +CREATE INDEX idx_categories_parent_id ON categories(parent_id); +CREATE INDEX idx_categories_slug ON categories(slug); +CREATE INDEX idx_categories_sort_order ON categories(sort_order); +CREATE INDEX idx_categories_is_active ON categories(is_active); +CREATE INDEX idx_categories_is_featured ON categories(is_featured); +CREATE INDEX idx_categories_level ON categories(level); +CREATE INDEX idx_categories_path ON categories(path); +``` + +#### 3.3.2 层级结构管理 +```sql +-- 创建层级结构维护函数 +CREATE OR REPLACE FUNCTION update_category_path() +RETURNS TRIGGER AS $$ +DECLARE + parent_path VARCHAR(500); +BEGIN + IF NEW.parent_id IS NULL THEN + NEW.level = 0; + NEW.path = NEW.id::VARCHAR; + ELSE + SELECT level + 1, path || '.' || NEW.id::VARCHAR + INTO NEW.level, NEW.path + FROM categories + WHERE id = NEW.parent_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_category_path_trigger + BEFORE INSERT OR UPDATE ON categories + FOR EACH ROW + EXECUTE FUNCTION update_category_path(); +``` + +### 3.4 标签表 (tags) + +#### 3.4.1 表结构 +```sql +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + slug VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + + -- 展示信息 + color VARCHAR(7), -- 十六进制颜色代码 + icon VARCHAR(50), -- 图标类名 + + -- 分类信息 + tag_group VARCHAR(50), -- 标签分组,如 "技术", "风格", "地点" + + -- 统计信息 + usage_count INTEGER DEFAULT 0, + trend_score DECIMAL(5,2) DEFAULT 0, -- 趋势评分 + + -- 状态信息 + is_active BOOLEAN DEFAULT TRUE, + is_featured BOOLEAN DEFAULT FALSE, + + -- SEO信息 + seo_title VARCHAR(255), + seo_description TEXT, + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP, + + -- 检查约束 + CONSTRAINT tags_name_not_empty CHECK (LENGTH(name) > 0), + CONSTRAINT tags_valid_color CHECK ( + color IS NULL OR color ~ '^#[0-9A-Fa-f]{6}$' + ), + CONSTRAINT tags_usage_count_positive CHECK (usage_count >= 0) +); + +-- 索引优化 +CREATE INDEX idx_tags_name ON tags(name); +CREATE INDEX idx_tags_slug ON tags(slug); +CREATE INDEX idx_tags_usage_count ON tags(usage_count DESC); +CREATE INDEX idx_tags_tag_group ON tags(tag_group); +CREATE INDEX idx_tags_is_active ON tags(is_active); +CREATE INDEX idx_tags_is_featured ON tags(is_featured); +CREATE INDEX idx_tags_trend_score ON tags(trend_score DESC); +``` + +#### 3.4.2 标签分组说明 +| 分组名 | 说明 | 示例标签 | +|--------|------|----------| +| style | 摄影风格 | 黑白、彩色、复古、现代 | +| subject | 拍摄主题 | 人像、风景、建筑、街拍 | +| technique | 拍摄技法 | 长曝光、微距、HDR、全景 | +| location | 地理位置 | 北京、上海、户外、室内 | +| time | 时间相关 | 日出、日落、夜景、春天 | +| mood | 情绪氛围 | 宁静、热闹、忧郁、欢快 | +| equipment | 器材相关 | 广角、长焦、无人机、手机 | + +### 3.5 关联表设计 + +#### 3.5.1 照片分类关联表 (photo_categories) +```sql +CREATE TABLE photo_categories ( + photo_id INTEGER NOT NULL REFERENCES photos(id) ON DELETE CASCADE, + category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE, + + -- 关联信息 + sort_order INTEGER DEFAULT 0, + is_primary BOOLEAN DEFAULT FALSE, -- 是否为主分类 + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 主键 + PRIMARY KEY (photo_id, category_id), + + -- 检查约束 + CONSTRAINT photo_categories_unique_primary_per_photo + EXCLUDE (photo_id WITH =) WHERE (is_primary = TRUE) +); + +-- 索引优化 +CREATE INDEX idx_photo_categories_photo_id ON photo_categories(photo_id); +CREATE INDEX idx_photo_categories_category_id ON photo_categories(category_id); +CREATE INDEX idx_photo_categories_is_primary ON photo_categories(is_primary); +``` + +#### 3.5.2 照片标签关联表 (photo_tags) +```sql +CREATE TABLE photo_tags ( + photo_id INTEGER NOT NULL REFERENCES photos(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + + -- 关联信息 + confidence DECIMAL(3,2) DEFAULT 1.0, -- 标签置信度 (0.0-1.0) + source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'auto', 'ai')), + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 主键 + PRIMARY KEY (photo_id, tag_id), + + -- 检查约束 + CONSTRAINT photo_tags_valid_confidence CHECK ( + confidence >= 0.0 AND confidence <= 1.0 + ) +); + +-- 索引优化 +CREATE INDEX idx_photo_tags_photo_id ON photo_tags(photo_id); +CREATE INDEX idx_photo_tags_tag_id ON photo_tags(tag_id); +CREATE INDEX idx_photo_tags_confidence ON photo_tags(confidence DESC); +CREATE INDEX idx_photo_tags_source ON photo_tags(source); +``` + +## 4. 用户管理表设计 + +### 4.1 用户表 (users) + +#### 4.1.1 表结构 +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + + -- 个人信息 + first_name VARCHAR(50), + last_name VARCHAR(50), + display_name VARCHAR(100), + bio TEXT, + avatar_url VARCHAR(500), + + -- 角色权限 + role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('super_admin', 'admin', 'editor', 'user')), + permissions JSONB DEFAULT '[]', + + -- 状态信息 + is_active BOOLEAN DEFAULT TRUE, + is_verified BOOLEAN DEFAULT FALSE, + is_2fa_enabled BOOLEAN DEFAULT FALSE, + + -- 登录信息 + last_login_at TIMESTAMP, + last_login_ip INET, + login_count INTEGER DEFAULT 0, + failed_login_attempts INTEGER DEFAULT 0, + locked_until TIMESTAMP, + + -- 设置信息 + timezone VARCHAR(50) DEFAULT 'UTC', + language VARCHAR(10) DEFAULT 'en', + theme VARCHAR(20) DEFAULT 'auto', + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 检查约束 + CONSTRAINT users_username_not_empty CHECK (LENGTH(username) > 0), + CONSTRAINT users_email_format CHECK (email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + CONSTRAINT users_failed_attempts_positive CHECK (failed_login_attempts >= 0), + CONSTRAINT users_login_count_positive CHECK (login_count >= 0) +); + +-- 索引优化 +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_role ON users(role); +CREATE INDEX idx_users_is_active ON users(is_active); +CREATE INDEX idx_users_last_login_at ON users(last_login_at); +CREATE INDEX idx_users_permissions ON users USING gin(permissions); +``` + +#### 4.1.2 角色权限说明 +| 角色 | 权限描述 | 功能范围 | +|------|----------|----------| +| super_admin | 超级管理员 | 所有功能 + 系统管理 | +| admin | 管理员 | 内容管理 + 用户管理 | +| editor | 编辑者 | 内容编辑 + 发布 | +| user | 普通用户 | 基本浏览权限 | + +### 4.2 会话表 (sessions) + +#### 4.2.1 表结构 +```sql +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- 会话信息 + token_hash VARCHAR(255) NOT NULL, + refresh_token_hash VARCHAR(255), + + -- 设备信息 + device_type VARCHAR(20), -- 'desktop', 'mobile', 'tablet' + device_name VARCHAR(100), + browser VARCHAR(50), + os VARCHAR(50), + + -- 网络信息 + ip_address INET, + user_agent TEXT, + location VARCHAR(100), + + -- 状态信息 + is_active BOOLEAN DEFAULT TRUE, + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + + -- 检查约束 + CONSTRAINT sessions_expires_future CHECK (expires_at > created_at) +); + +-- 索引优化 +CREATE INDEX idx_sessions_user_id ON sessions(user_id); +CREATE INDEX idx_sessions_token_hash ON sessions(token_hash); +CREATE INDEX idx_sessions_is_active ON sessions(is_active); +CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); +CREATE INDEX idx_sessions_last_used_at ON sessions(last_used_at); +``` + +## 5. 系统配置表设计 + +### 5.1 系统设置表 (settings) + +#### 5.1.1 表结构 +```sql +CREATE TABLE settings ( + key VARCHAR(50) PRIMARY KEY, + value TEXT, + description VARCHAR(255), + + -- 类型信息 + type VARCHAR(20) DEFAULT 'string' CHECK (type IN ('string', 'number', 'boolean', 'json', 'array')), + category VARCHAR(50) DEFAULT 'general', + + -- 验证信息 + validation_rules JSONB, + default_value TEXT, + + -- 权限信息 + is_public BOOLEAN DEFAULT FALSE, + required_role VARCHAR(20) DEFAULT 'admin', + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 检查约束 + CONSTRAINT settings_key_not_empty CHECK (LENGTH(key) > 0) +); + +-- 索引优化 +CREATE INDEX idx_settings_category ON settings(category); +CREATE INDEX idx_settings_type ON settings(type); +CREATE INDEX idx_settings_is_public ON settings(is_public); +``` + +#### 5.1.2 预设配置数据 +```sql +-- 网站基本配置 +INSERT INTO settings (key, value, description, type, category, is_public) VALUES +('site_title', '摄影作品集', '网站标题', 'string', 'general', TRUE), +('site_description', '专业摄影师作品展示平台', '网站描述', 'string', 'general', TRUE), +('site_keywords', '摄影,作品集,艺术,创作', '网站关键词', 'string', 'seo', TRUE), +('site_author', '摄影师姓名', '网站作者', 'string', 'general', TRUE), +('site_email', 'contact@example.com', '联系邮箱', 'string', 'general', TRUE), +('site_language', 'zh-CN', '默认语言', 'string', 'general', TRUE), +('site_timezone', 'Asia/Shanghai', '默认时区', 'string', 'general', FALSE); + +-- 上传配置 +INSERT INTO settings (key, value, description, type, category, validation_rules) VALUES +('upload_max_file_size', '52428800', '最大文件大小(字节)', 'number', 'upload', '{"min": 1048576, "max": 104857600}'), +('upload_allowed_types', '["image/jpeg", "image/png", "image/raw", "image/heic"]', '允许的文件类型', 'json', 'upload', NULL), +('upload_max_files_per_batch', '50', '批量上传最大文件数', 'number', 'upload', '{"min": 1, "max": 100}'), +('upload_auto_publish', 'false', '自动发布上传的照片', 'boolean', 'upload', NULL), +('upload_generate_thumbnails', 'true', '自动生成缩略图', 'boolean', 'upload', NULL); + +-- 图片处理配置 +INSERT INTO settings (key, value, description, type, category, validation_rules) VALUES +('image_quality_jpg', '85', 'JPEG质量', 'number', 'image', '{"min": 1, "max": 100}'), +('image_quality_webp', '80', 'WebP质量', 'number', 'image', '{"min": 1, "max": 100}'), +('image_max_width', '1920', '最大宽度', 'number', 'image', '{"min": 100, "max": 4096}'), +('image_max_height', '1080', '最大高度', 'number', 'image', '{"min": 100, "max": 4096}'), +('image_watermark_enabled', 'false', '启用水印', 'boolean', 'image', NULL), +('image_watermark_text', '© 摄影师姓名', '水印文字', 'string', 'image', NULL); + +-- 显示配置 +INSERT INTO settings (key, value, description, type, category, is_public) VALUES +('display_photos_per_page', '20', '每页显示照片数', 'number', 'display', TRUE), +('display_thumbnail_size', '300', '缩略图尺寸', 'number', 'display', TRUE), +('display_theme_primary_color', '#d4af37', '主题主色调', 'string', 'display', TRUE), +('display_theme_secondary_color', '#2d2d2d', '主题辅助色', 'string', 'display', TRUE), +('display_enable_dark_mode', 'true', '启用深色模式', 'boolean', 'display', TRUE), +('display_enable_animations', 'true', '启用动画效果', 'boolean', 'display', TRUE); + +-- 缓存配置 +INSERT INTO settings (key, value, description, type, category) VALUES +('cache_enabled', 'true', '启用缓存', 'boolean', 'cache'), +('cache_ttl_photos', '3600', '照片缓存时间(秒)', 'number', 'cache'), +('cache_ttl_categories', '7200', '分类缓存时间(秒)', 'number', 'cache'), +('cache_ttl_tags', '3600', '标签缓存时间(秒)', 'number', 'cache'), +('cache_ttl_settings', '86400', '设置缓存时间(秒)', 'number', 'cache'); +``` + +### 5.2 操作日志表 (admin_logs) + +#### 5.2.1 表结构 +```sql +CREATE TABLE admin_logs ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + + -- 操作信息 + action VARCHAR(50) NOT NULL, + resource_type VARCHAR(50), + resource_id INTEGER, + + -- 详细信息 + details JSONB, + old_values JSONB, + new_values JSONB, + + -- 请求信息 + ip_address INET, + user_agent TEXT, + request_method VARCHAR(10), + request_url VARCHAR(500), + + -- 结果信息 + status VARCHAR(20) DEFAULT 'success' CHECK (status IN ('success', 'error', 'warning')), + error_message TEXT, + + -- 时间信息 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 检查约束 + CONSTRAINT admin_logs_action_not_empty CHECK (LENGTH(action) > 0) +); + +-- 索引优化 +CREATE INDEX idx_admin_logs_user_id ON admin_logs(user_id); +CREATE INDEX idx_admin_logs_action ON admin_logs(action); +CREATE INDEX idx_admin_logs_resource_type ON admin_logs(resource_type); +CREATE INDEX idx_admin_logs_resource_id ON admin_logs(resource_id); +CREATE INDEX idx_admin_logs_created_at ON admin_logs(created_at); +CREATE INDEX idx_admin_logs_status ON admin_logs(status); +CREATE INDEX idx_admin_logs_details ON admin_logs USING gin(details); +``` + +#### 5.2.2 操作类型说明 +| 操作类型 | 描述 | 示例 | +|----------|------|------| +| photo.create | 创建照片 | 上传新照片 | +| photo.update | 更新照片 | 修改照片信息 | +| photo.delete | 删除照片 | 删除照片 | +| photo.publish | 发布照片 | 发布草稿照片 | +| photo.archive | 归档照片 | 归档照片 | +| category.create | 创建分类 | 新建分类 | +| category.update | 更新分类 | 修改分类信息 | +| category.delete | 删除分类 | 删除分类 | +| tag.create | 创建标签 | 新建标签 | +| tag.update | 更新标签 | 修改标签信息 | +| tag.delete | 删除标签 | 删除标签 | +| user.login | 用户登录 | 管理员登录 | +| user.logout | 用户登出 | 管理员登出 | +| settings.update | 更新设置 | 修改系统设置 | +| system.backup | 系统备份 | 数据库备份 | + +## 6. 数据库函数和存储过程 + +### 6.1 照片统计函数 + +#### 6.1.1 更新分类照片数量 +```sql +CREATE OR REPLACE FUNCTION update_category_photo_count() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE categories + SET photo_count = photo_count + 1, + direct_photo_count = direct_photo_count + 1 + WHERE id = NEW.category_id; + + -- 更新父级分类的photo_count + UPDATE categories + SET photo_count = photo_count + 1 + WHERE id IN ( + SELECT DISTINCT unnest(string_to_array(path, '.'))::INTEGER + FROM categories + WHERE id = NEW.category_id + AND parent_id IS NOT NULL + ); + + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + UPDATE categories + SET photo_count = photo_count - 1, + direct_photo_count = direct_photo_count - 1 + WHERE id = OLD.category_id; + + -- 更新父级分类的photo_count + UPDATE categories + SET photo_count = photo_count - 1 + WHERE id IN ( + SELECT DISTINCT unnest(string_to_array(path, '.'))::INTEGER + FROM categories + WHERE id = OLD.category_id + AND parent_id IS NOT NULL + ); + + RETURN OLD; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- 创建触发器 +CREATE TRIGGER update_category_photo_count_trigger + AFTER INSERT OR DELETE ON photo_categories + FOR EACH ROW + EXECUTE FUNCTION update_category_photo_count(); +``` + +#### 6.1.2 更新标签使用次数 +```sql +CREATE OR REPLACE FUNCTION update_tag_usage_count() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE tags + SET usage_count = usage_count + 1, + last_used_at = CURRENT_TIMESTAMP + WHERE id = NEW.tag_id; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + UPDATE tags + SET usage_count = usage_count - 1 + WHERE id = OLD.tag_id; + RETURN OLD; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- 创建触发器 +CREATE TRIGGER update_tag_usage_count_trigger + AFTER INSERT OR DELETE ON photo_tags + FOR EACH ROW + EXECUTE FUNCTION update_tag_usage_count(); +``` + +### 6.2 全文搜索函数 + +#### 6.2.1 更新搜索向量 +```sql +CREATE OR REPLACE FUNCTION update_photo_search_vector() +RETURNS TRIGGER AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.location_name, '')), 'C') || + setweight(to_tsvector('english', COALESCE(NEW.camera, '')), 'D') || + setweight(to_tsvector('english', COALESCE(NEW.lens, '')), 'D'); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 创建触发器 +CREATE TRIGGER update_photo_search_vector_trigger + BEFORE INSERT OR UPDATE ON photos + FOR EACH ROW + EXECUTE FUNCTION update_photo_search_vector(); +``` + +#### 6.2.2 搜索函数 +```sql +CREATE OR REPLACE FUNCTION search_photos( + search_query TEXT, + limit_count INTEGER DEFAULT 20, + offset_count INTEGER DEFAULT 0 +) +RETURNS TABLE( + id INTEGER, + title VARCHAR(255), + description TEXT, + rank REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + p.id, + p.title, + p.description, + ts_rank(p.search_vector, plainto_tsquery('english', search_query)) AS rank + FROM photos p + WHERE p.search_vector @@ plainto_tsquery('english', search_query) + AND p.status = 'published' + AND p.visibility = 'public' + ORDER BY rank DESC + LIMIT limit_count + OFFSET offset_count; +END; +$$ LANGUAGE plpgsql; +``` + +### 6.3 数据清理函数 + +#### 6.3.1 清理过期会话 +```sql +CREATE OR REPLACE FUNCTION cleanup_expired_sessions() +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER; +BEGIN + DELETE FROM sessions + WHERE expires_at < CURRENT_TIMESTAMP + OR last_used_at < CURRENT_TIMESTAMP - INTERVAL '30 days'; + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; + +-- 创建定时任务 (需要pg_cron扩展) +-- SELECT cron.schedule('cleanup-sessions', '0 2 * * *', 'SELECT cleanup_expired_sessions();'); +``` + +#### 6.3.2 清理孤立文件记录 +```sql +CREATE OR REPLACE FUNCTION cleanup_orphaned_formats() +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER; +BEGIN + DELETE FROM photo_formats + WHERE photo_id NOT IN (SELECT id FROM photos); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; +``` + +## 7. 数据库优化策略 + +### 7.1 分区表设计 + +#### 7.1.1 按时间分区日志表 +```sql +-- 创建分区表 +CREATE TABLE admin_logs_partitioned ( + LIKE admin_logs INCLUDING ALL +) PARTITION BY RANGE (created_at); + +-- 创建分区 +CREATE TABLE admin_logs_2024_01 PARTITION OF admin_logs_partitioned + FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); + +CREATE TABLE admin_logs_2024_02 PARTITION OF admin_logs_partitioned + FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); + +-- 创建默认分区 +CREATE TABLE admin_logs_default PARTITION OF admin_logs_partitioned + DEFAULT; + +-- 创建自动分区函数 +CREATE OR REPLACE FUNCTION create_monthly_partition(table_name TEXT, start_date DATE) +RETURNS VOID AS $$ +DECLARE + partition_name TEXT; + end_date DATE; +BEGIN + partition_name := table_name || '_' || to_char(start_date, 'YYYY_MM'); + end_date := start_date + INTERVAL '1 month'; + + EXECUTE format('CREATE TABLE %I PARTITION OF %I + FOR VALUES FROM (%L) TO (%L)', + partition_name, table_name, start_date, end_date); +END; +$$ LANGUAGE plpgsql; +``` + +### 7.2 索引优化 + +#### 7.2.1 复合索引优化 +```sql +-- 照片查询常用复合索引 +CREATE INDEX idx_photos_status_visibility_created_at + ON photos(status, visibility, created_at DESC); + +CREATE INDEX idx_photos_taken_at_status + ON photos(taken_at DESC, status) + WHERE status = 'published'; + +-- 部分索引 (只索引已发布的照片) +CREATE INDEX idx_photos_published_taken_at + ON photos(taken_at DESC) + WHERE status = 'published' AND visibility = 'public'; + +-- 表达式索引 +CREATE INDEX idx_photos_title_lower + ON photos(LOWER(title)); + +-- JSON索引 +CREATE INDEX idx_photos_metadata_gin + ON photos USING gin(metadata); +``` + +#### 7.2.2 索引使用监控 +```sql +-- 查看索引使用情况 +CREATE VIEW index_usage_stats AS +SELECT + schemaname, + tablename, + indexname, + idx_tup_read, + idx_tup_fetch, + idx_scan, + idx_tup_read::DECIMAL / NULLIF(idx_scan, 0) AS avg_tuples_per_scan +FROM pg_stat_user_indexes +ORDER BY idx_scan DESC; + +-- 查看未使用的索引 +CREATE VIEW unused_indexes AS +SELECT + schemaname, + tablename, + indexname, + pg_size_pretty(pg_relation_size(indexrelid)) AS size +FROM pg_stat_user_indexes +WHERE idx_scan = 0 + AND NOT indisunique + AND indexrelname NOT LIKE '%_pkey' +ORDER BY pg_relation_size(indexrelid) DESC; +``` + +### 7.3 查询优化 + +#### 7.3.1 查询计划分析 +```sql +-- 分析慢查询 +CREATE VIEW slow_queries AS +SELECT + query, + calls, + total_time, + mean_time, + rows, + 100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent +FROM pg_stat_statements +WHERE mean_time > 1000 -- 平均执行时间超过1秒 +ORDER BY mean_time DESC; + +-- 创建查询优化函数 +CREATE OR REPLACE FUNCTION explain_query(query_text TEXT) +RETURNS TABLE(plan TEXT) AS $$ +BEGIN + RETURN QUERY + EXECUTE 'EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ' || query_text; +END; +$$ LANGUAGE plpgsql; +``` + +### 7.4 缓存策略 + +#### 7.4.1 Redis缓存键设计 +``` +# 缓存键命名规范 +photo:detail:{photo_id} # 照片详情 +photo:list:{status}:{page}:{limit} # 照片列表 +category:tree # 分类树 +category:stats # 分类统计 +tag:cloud # 标签云 +tag:suggestions:{query} # 标签建议 +user:session:{user_id} # 用户会话 +settings:all # 系统设置 +timeline:{year} # 时间线数据 +``` + +#### 7.4.2 缓存更新策略 +```sql +-- 创建缓存失效函数 +CREATE OR REPLACE FUNCTION invalidate_cache_keys(key_pattern TEXT) +RETURNS VOID AS $$ +BEGIN + -- 这里需要配合应用层实现 + -- 通过Redis发布/订阅机制通知应用层清除缓存 + PERFORM pg_notify('cache_invalidate', key_pattern); +END; +$$ LANGUAGE plpgsql; + +-- 创建缓存失效触发器 +CREATE OR REPLACE FUNCTION photos_cache_invalidate_trigger() +RETURNS TRIGGER AS $$ +BEGIN + -- 清除相关缓存 + PERFORM invalidate_cache_keys('photo:*'); + PERFORM invalidate_cache_keys('category:*'); + PERFORM invalidate_cache_keys('timeline:*'); + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER photos_cache_invalidate + AFTER INSERT OR UPDATE OR DELETE ON photos + FOR EACH ROW + EXECUTE FUNCTION photos_cache_invalidate_trigger(); +``` + +## 8. 数据库备份与恢复 + +### 8.1 备份策略 + +#### 8.1.1 自动备份脚本 +```bash +#!/bin/bash +# backup_database.sh + +# 配置 +DB_NAME="photography" +DB_USER="postgres" +DB_HOST="localhost" +DB_PORT="5432" +BACKUP_DIR="/var/backups/postgresql" +RETENTION_DAYS=30 + +# 创建备份目录 +mkdir -p $BACKUP_DIR + +# 备份文件名 +BACKUP_FILE="$BACKUP_DIR/photography_$(date +%Y%m%d_%H%M%S).sql.gz" + +# 执行备份 +pg_dump -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME | gzip > $BACKUP_FILE + +# 检查备份是否成功 +if [ $? -eq 0 ]; then + echo "Backup successful: $BACKUP_FILE" + + # 删除过期备份 + find $BACKUP_DIR -name "photography_*.sql.gz" -mtime +$RETENTION_DAYS -delete + + # 记录备份日志 + echo "$(date): Backup completed successfully" >> /var/log/postgresql_backup.log +else + echo "Backup failed!" + echo "$(date): Backup failed" >> /var/log/postgresql_backup.log + exit 1 +fi +``` + +#### 8.1.2 增量备份 +```bash +#!/bin/bash +# incremental_backup.sh + +# WAL归档备份 +ARCHIVE_DIR="/var/backups/postgresql/wal" +mkdir -p $ARCHIVE_DIR + +# 配置postgresql.conf +# wal_level = replica +# archive_mode = on +# archive_command = 'test ! -f /var/backups/postgresql/wal/%f && cp %p /var/backups/postgresql/wal/%f' + +# 基础备份 +pg_basebackup -D /var/backups/postgresql/base -Ft -z -P -U postgres +``` + +### 8.2 恢复策略 + +#### 8.2.1 完整恢复 +```bash +#!/bin/bash +# restore_database.sh + +BACKUP_FILE=$1 +DB_NAME="photography" +DB_USER="postgres" + +if [ -z "$BACKUP_FILE" ]; then + echo "Usage: $0 " + exit 1 +fi + +# 停止应用服务 +systemctl stop photography-backend + +# 删除现有数据库 +dropdb -U $DB_USER $DB_NAME + +# 创建新数据库 +createdb -U $DB_USER $DB_NAME + +# 恢复数据 +gunzip -c $BACKUP_FILE | psql -U $DB_USER -d $DB_NAME + +# 启动应用服务 +systemctl start photography-backend + +echo "Database restored successfully" +``` + +#### 8.2.2 时间点恢复 +```bash +#!/bin/bash +# point_in_time_recovery.sh + +RECOVERY_TIME=$1 +BASE_BACKUP="/var/backups/postgresql/base" +WAL_ARCHIVE="/var/backups/postgresql/wal" + +# 恢复到指定时间点 +pg_ctl stop -D /var/lib/postgresql/data + +# 恢复基础备份 +rm -rf /var/lib/postgresql/data/* +tar -xzf $BASE_BACKUP/base.tar.gz -C /var/lib/postgresql/data/ + +# 创建recovery.conf +cat > /var/lib/postgresql/data/recovery.conf << EOF +restore_command = 'cp $WAL_ARCHIVE/%f %p' +recovery_target_time = '$RECOVERY_TIME' +recovery_target_action = 'promote' +EOF + +# 启动数据库 +pg_ctl start -D /var/lib/postgresql/data +``` + +## 9. 性能监控 + +### 9.1 性能指标视图 + +#### 9.1.1 数据库性能监控 +```sql +-- 数据库连接统计 +CREATE VIEW database_connections AS +SELECT + datname, + numbackends, + xact_commit, + xact_rollback, + blks_read, + blks_hit, + tup_returned, + tup_fetched, + tup_inserted, + tup_updated, + tup_deleted +FROM pg_stat_database +WHERE datname = 'photography'; + +-- 表操作统计 +CREATE VIEW table_stats AS +SELECT + schemaname, + tablename, + seq_scan, + seq_tup_read, + idx_scan, + idx_tup_fetch, + n_tup_ins, + n_tup_upd, + n_tup_del, + n_tup_hot_upd, + n_live_tup, + n_dead_tup, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze +FROM pg_stat_user_tables +ORDER BY seq_scan + idx_scan DESC; +``` + +#### 9.1.2 查询性能监控 +```sql +-- 安装pg_stat_statements扩展 +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + +-- 慢查询分析 +CREATE VIEW slow_queries_detailed AS +SELECT + query, + calls, + total_time, + mean_time, + max_time, + min_time, + stddev_time, + rows, + 100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent, + 100.0 * (shared_blks_hit + shared_blks_read) / nullif(calls, 0) AS avg_blocks_per_call +FROM pg_stat_statements +WHERE mean_time > 100 -- 平均执行时间超过100ms +ORDER BY total_time DESC; +``` + +### 9.2 自动化监控 + +#### 9.2.1 监控脚本 +```bash +#!/bin/bash +# monitor_database.sh + +# 配置 +DB_NAME="photography" +DB_USER="postgres" +ALERT_EMAIL="admin@example.com" +LOG_FILE="/var/log/postgresql_monitor.log" + +# 检查数据库连接 +check_database_connection() { + psql -U $DB_USER -d $DB_NAME -c "SELECT 1;" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "$(date): Database connection failed" >> $LOG_FILE + send_alert "Database connection failed" + return 1 + fi + return 0 +} + +# 检查慢查询 +check_slow_queries() { + SLOW_QUERY_COUNT=$(psql -U $DB_USER -d $DB_NAME -t -c " + SELECT COUNT(*) FROM pg_stat_statements + WHERE mean_time > 1000; + ") + + if [ $SLOW_QUERY_COUNT -gt 10 ]; then + echo "$(date): Too many slow queries: $SLOW_QUERY_COUNT" >> $LOG_FILE + send_alert "Too many slow queries detected: $SLOW_QUERY_COUNT" + fi +} + +# 检查数据库大小 +check_database_size() { + DB_SIZE=$(psql -U $DB_USER -d $DB_NAME -t -c " + SELECT pg_size_pretty(pg_database_size('$DB_NAME')); + ") + + echo "$(date): Database size: $DB_SIZE" >> $LOG_FILE +} + +# 发送警报 +send_alert() { + MESSAGE=$1 + echo "$MESSAGE" | mail -s "Database Alert - Photography" $ALERT_EMAIL +} + +# 执行检查 +check_database_connection +check_slow_queries +check_database_size + +echo "$(date): Database monitoring completed" >> $LOG_FILE +``` + +## 10. 数据库迁移 + +### 10.1 迁移脚本 + +#### 10.1.1 版本1.0.0 - 初始化 +```sql +-- migrations/001_initial_schema.sql +-- 创建基础表结构 + +-- 照片表 +CREATE TABLE photos ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + slug VARCHAR(255) UNIQUE, + original_filename VARCHAR(255), + file_size BIGINT, + mime_type VARCHAR(100), + status VARCHAR(20) DEFAULT 'published', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 分类表 +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + slug VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 标签表 +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + slug VARCHAR(50) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 关联表 +CREATE TABLE photo_categories ( + photo_id INTEGER REFERENCES photos(id) ON DELETE CASCADE, + category_id INTEGER REFERENCES categories(id) ON DELETE CASCADE, + PRIMARY KEY (photo_id, category_id) +); + +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 INDEX idx_photos_status ON photos(status); +CREATE INDEX idx_photos_created_at ON photos(created_at); +``` + +#### 10.1.2 版本1.1.0 - 添加EXIF支持 +```sql +-- migrations/002_add_exif_support.sql +-- 添加EXIF元数据字段 + +ALTER TABLE photos ADD COLUMN camera VARCHAR(100); +ALTER TABLE photos ADD COLUMN lens VARCHAR(100); +ALTER TABLE photos ADD COLUMN iso INTEGER; +ALTER TABLE photos ADD COLUMN aperture VARCHAR(10); +ALTER TABLE photos ADD COLUMN shutter_speed VARCHAR(20); +ALTER TABLE photos ADD COLUMN focal_length VARCHAR(20); +ALTER TABLE photos ADD COLUMN taken_at TIMESTAMP; + +-- 创建照片格式表 +CREATE TABLE photo_formats ( + id SERIAL PRIMARY KEY, + photo_id INTEGER NOT NULL REFERENCES photos(id) ON DELETE CASCADE, + format_type VARCHAR(20) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + width INTEGER, + height INTEGER, + quality INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(photo_id, format_type) +); + +CREATE INDEX idx_photo_formats_photo_id ON photo_formats(photo_id); +CREATE INDEX idx_photo_formats_format_type ON photo_formats(format_type); +``` + +#### 10.1.3 版本1.2.0 - 用户管理 +```sql +-- migrations/003_add_user_management.sql +-- 添加用户管理功能 + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) DEFAULT 'user', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_sessions_user_id ON sessions(user_id); +CREATE INDEX idx_sessions_token_hash ON sessions(token_hash); +``` + +### 10.2 迁移工具 + +#### 10.2.1 迁移管理器 +```bash +#!/bin/bash +# migrate.sh + +DB_NAME="photography" +DB_USER="postgres" +MIGRATIONS_DIR="migrations" +MIGRATIONS_TABLE="schema_migrations" + +# 创建迁移记录表 +create_migrations_table() { + psql -U $DB_USER -d $DB_NAME -c " + CREATE TABLE IF NOT EXISTS $MIGRATIONS_TABLE ( + version VARCHAR(50) PRIMARY KEY, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + " +} + +# 获取当前版本 +get_current_version() { + psql -U $DB_USER -d $DB_NAME -t -c " + SELECT version FROM $MIGRATIONS_TABLE ORDER BY version DESC LIMIT 1; + " | tr -d ' ' +} + +# 执行迁移 +run_migration() { + local migration_file=$1 + local version=$(basename $migration_file .sql) + + echo "Running migration: $version" + + # 开始事务 + psql -U $DB_USER -d $DB_NAME -c "BEGIN;" + + # 执行迁移 + psql -U $DB_USER -d $DB_NAME -f $migration_file + + if [ $? -eq 0 ]; then + # 记录迁移 + psql -U $DB_USER -d $DB_NAME -c " + INSERT INTO $MIGRATIONS_TABLE (version) VALUES ('$version'); + " + + # 提交事务 + psql -U $DB_USER -d $DB_NAME -c "COMMIT;" + + echo "Migration $version completed successfully" + else + # 回滚事务 + psql -U $DB_USER -d $DB_NAME -c "ROLLBACK;" + echo "Migration $version failed" + exit 1 + fi +} + +# 主函数 +main() { + create_migrations_table + + current_version=$(get_current_version) + echo "Current version: $current_version" + + # 执行待处理的迁移 + for migration_file in $MIGRATIONS_DIR/*.sql; do + version=$(basename $migration_file .sql) + + # 检查是否已执行 + executed=$(psql -U $DB_USER -d $DB_NAME -t -c " + SELECT COUNT(*) FROM $MIGRATIONS_TABLE WHERE version = '$version'; + " | tr -d ' ') + + if [ $executed -eq 0 ]; then + run_migration $migration_file + else + echo "Migration $version already executed, skipping" + fi + done + + echo "All migrations completed" +} + +# 运行 +main +``` + +## 11. 数据库安全 + +### 11.1 权限管理 + +#### 11.1.1 角色权限设计 +```sql +-- 创建角色 +CREATE ROLE photography_admin; +CREATE ROLE photography_editor; +CREATE ROLE photography_viewer; + +-- 管理员权限 (完全权限) +GRANT ALL PRIVILEGES ON DATABASE photography TO photography_admin; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO photography_admin; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO photography_admin; + +-- 编辑者权限 (读写权限,不能删除) +GRANT CONNECT ON DATABASE photography TO photography_editor; +GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO photography_editor; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO photography_editor; +REVOKE DELETE ON photos, categories, tags FROM photography_editor; + +-- 查看者权限 (只读权限) +GRANT CONNECT ON DATABASE photography TO photography_viewer; +GRANT SELECT ON photos, categories, tags, photo_categories, photo_tags, photo_formats TO photography_viewer; + +-- 创建用户并分配角色 +CREATE USER admin_user WITH PASSWORD 'secure_password'; +GRANT photography_admin TO admin_user; + +CREATE USER editor_user WITH PASSWORD 'secure_password'; +GRANT photography_editor TO editor_user; +``` + +### 11.2 数据加密 + +#### 11.2.1 敏感数据加密 +```sql +-- 创建加密扩展 +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- 加密函数 +CREATE OR REPLACE FUNCTION encrypt_sensitive_data(data TEXT, key TEXT) +RETURNS TEXT AS $$ +BEGIN + RETURN encode(encrypt(data::bytea, key::bytea, 'aes'), 'base64'); +END; +$$ LANGUAGE plpgsql; + +-- 解密函数 +CREATE OR REPLACE FUNCTION decrypt_sensitive_data(encrypted_data TEXT, key TEXT) +RETURNS TEXT AS $$ +BEGIN + RETURN convert_from(decrypt(decode(encrypted_data, 'base64'), key::bytea, 'aes'), 'UTF8'); +END; +$$ LANGUAGE plpgsql; + +-- 使用示例 +UPDATE users SET + email = encrypt_sensitive_data(email, 'encryption_key') +WHERE id = 1; +``` + +### 11.3 审计日志 + +#### 11.3.1 详细审计配置 +```sql +-- 创建审计触发器函数 +CREATE OR REPLACE FUNCTION audit_trigger_function() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + INSERT INTO admin_logs ( + action, + resource_type, + resource_id, + old_values, + ip_address, + user_agent + ) VALUES ( + TG_TABLE_NAME || '.delete', + TG_TABLE_NAME, + OLD.id, + row_to_json(OLD), + inet_client_addr(), + current_setting('application_name', true) + ); + RETURN OLD; + ELSIF TG_OP = 'UPDATE' THEN + INSERT INTO admin_logs ( + action, + resource_type, + resource_id, + old_values, + new_values, + ip_address, + user_agent + ) VALUES ( + TG_TABLE_NAME || '.update', + TG_TABLE_NAME, + NEW.id, + row_to_json(OLD), + row_to_json(NEW), + inet_client_addr(), + current_setting('application_name', true) + ); + RETURN NEW; + ELSIF TG_OP = 'INSERT' THEN + INSERT INTO admin_logs ( + action, + resource_type, + resource_id, + new_values, + ip_address, + user_agent + ) VALUES ( + TG_TABLE_NAME || '.insert', + TG_TABLE_NAME, + NEW.id, + row_to_json(NEW), + inet_client_addr(), + current_setting('application_name', true) + ); + RETURN NEW; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- 为关键表创建审计触发器 +CREATE TRIGGER photos_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON photos + FOR EACH ROW + EXECUTE FUNCTION audit_trigger_function(); + +CREATE TRIGGER categories_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON categories + FOR EACH ROW + EXECUTE FUNCTION audit_trigger_function(); + +CREATE TRIGGER users_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON users + FOR EACH ROW + EXECUTE FUNCTION audit_trigger_function(); +``` + +## 12. 总结 + +这个数据库设计文档提供了摄影作品集网站的完整数据库架构,包括: + +### 🎯 设计亮点 +- **完整的数据模型**: 支持照片、分类、标签、用户管理等所有功能 +- **性能优化**: 合理的索引设计、分区表、缓存策略 +- **可扩展性**: 支持层级分类、多格式文件、扩展元数据 +- **安全性**: 权限管理、数据加密、审计日志 + +### 🔧 技术特性 +- **PostgreSQL 15+**: 利用最新特性提升性能 +- **JSONB支持**: 灵活的元数据存储 +- **全文搜索**: 内置的搜索功能 +- **触发器自动化**: 数据一致性保证 + +### 📊 监控运维 +- **性能监控**: 慢查询分析、索引使用统计 +- **自动化备份**: 完整备份和增量备份 +- **迁移管理**: 版本化的数据库迁移 + +这个设计为Golang后端提供了强大的数据支撑,可以满足摄影作品集网站的所有需求。 \ No newline at end of file diff --git a/docs/v2/README.md b/docs/v2/README.md new file mode 100644 index 0000000..e5ebc83 --- /dev/null +++ b/docs/v2/README.md @@ -0,0 +1,440 @@ +# 摄影作品集网站 v2.0 - 规划文档 + +## 📋 v2.0 版本概述 + +v2.0 是摄影作品集网站的高级功能版本,在v1.0稳定运行的基础上,引入AI增强、社交功能、高级搜索等创新特性,并进行架构升级以支持更大规模的用户和数据量。 + +### 🎯 版本目标 +- 引入AI技术提升用户体验 +- 增加社交互动功能 +- 实现高性能的搜索体验 +- 优化系统架构支持高并发 +- 提供更丰富的数据分析能力 + +### 📅 开发周期 +- **开始时间**: 2024-06-01 (预计) +- **预计完成**: 2024-12-01 (预计) +- **当前状态**: 规划阶段 + +## 🚀 新功能特性 + +### 🤖 AI增强功能 + +#### 智能标签系统 +- **自动标签识别**: 基于图像识别技术自动生成标签 +- **内容理解**: 分析图片内容、色彩、构图等特征 +- **语义关联**: 建立标签之间的语义关系 +- **个性化推荐**: 基于用户行为推荐相关标签 + +#### 智能分类系统 +- **场景识别**: 自动识别拍摄场景 (室内/户外/风景/人像等) +- **风格分析**: 分析摄影风格 (黑白/彩色/复古/现代等) +- **质量评估**: 评估图片质量和技术参数 +- **自动归档**: 根据分析结果自动归类到相应分类 + +#### 内容推荐引擎 +- **相似图片推荐**: 基于视觉相似度推荐相关作品 +- **用户兴趣模型**: 建立用户偏好模型 +- **个性化首页**: 根据用户兴趣定制首页内容 +- **热门趋势**: 分析全站数据展示热门内容 + +### 👥 社交功能 + +#### 用户互动系统 +- **评论系统**: 支持多层级评论和回复 +- **点赞收藏**: 作品点赞和个人收藏功能 +- **分享功能**: 社交媒体分享和链接生成 +- **关注系统**: 用户关注和粉丝管理 + +#### 社区功能 +- **用户主页**: 个人作品展示和信息管理 +- **活动系统**: 摄影比赛和主题活动 +- **排行榜**: 作品热度和用户活跃度排行 +- **消息中心**: 站内消息和通知系统 + +#### 协作功能 +- **作品协作**: 多人协作编辑和管理 +- **权限管理**: 细粒度的协作权限控制 +- **版本控制**: 作品编辑历史和版本管理 +- **审核流程**: 内容审核和发布流程 + +### 🔍 高级搜索 + +#### 全文搜索引擎 +- **ElasticSearch**: 替换PostgreSQL全文搜索 +- **多语言支持**: 中英文分词和搜索优化 +- **搜索建议**: 实时搜索建议和自动完成 +- **搜索历史**: 用户搜索历史和热门搜索 + +#### 高级筛选 +- **地理位置搜索**: 基于GPS坐标的地理搜索 +- **时间范围搜索**: 灵活的时间段筛选 +- **技术参数搜索**: 根据相机、镜头、参数筛选 +- **视觉搜索**: 上传图片搜索相似作品 + +#### 搜索分析 +- **搜索统计**: 搜索词热度和趋势分析 +- **用户行为**: 搜索到点击的转化分析 +- **内容优化**: 基于搜索数据优化内容策略 + +### 📱 移动端优化 + +#### PWA支持 +- **离线访问**: 核心功能离线可用 +- **推送通知**: 实时消息推送 +- **原生体验**: 类似原生应用的交互 +- **桌面图标**: 支持添加到主屏幕 + +#### 移动端专属功能 +- **拍照上传**: 直接调用相机拍照上传 +- **位置标记**: 自动获取GPS位置信息 +- **手势操作**: 支持滑动、缩放等手势 +- **语音搜索**: 语音输入搜索功能 + +### 🌐 国际化支持 + +#### 多语言系统 +- **界面本地化**: 支持中英文界面切换 +- **内容翻译**: 自动翻译和人工翻译结合 +- **地区化设置**: 时间、日期、货币格式本地化 +- **RTL支持**: 支持从右到左的语言 + +#### 全球化部署 +- **CDN加速**: 全球CDN节点部署 +- **地域优化**: 不同地区的性能优化 +- **合规性**: 符合各地区的法律法规 +- **支付集成**: 支持多种支付方式 + +## 🏗️ 架构升级 + +### 微服务架构 + +#### 服务拆分 +``` +┌─────────────────────────────────────────────────────────┐ +│ API Gateway │ +│ (Kong/Envoy) │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 用户服务 │ │ 照片服务 │ │ 搜索服务 │ │ +│ │ (User Svc) │ │(Photo Svc) │ │(Search Svc) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 社交服务 │ │ AI服务 │ │ 通知服务 │ │ +│ │(Social Svc) │ │ (AI Svc) │ │(Notify Svc) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Message Queue │ +│ (RabbitMQ/Kafka) │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ElasticSearch│ │ Redis │ │ +│ │ (主数据) │ │ (搜索) │ │ (缓存) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 服务职责 + +1. **用户服务 (User Service)** + - 用户认证和授权 + - 用户资料管理 + - 权限控制 + +2. **照片服务 (Photo Service)** + - 照片CRUD操作 + - 分类标签管理 + - 文件存储管理 + +3. **搜索服务 (Search Service)** + - 全文搜索 + - 高级筛选 + - 搜索建议 + +4. **社交服务 (Social Service)** + - 评论点赞 + - 关注粉丝 + - 消息通知 + +5. **AI服务 (AI Service)** + - 图像识别 + - 内容推荐 + - 数据分析 + +6. **通知服务 (Notification Service)** + - 消息推送 + - 邮件通知 + - 站内消息 + +### GraphQL接口 + +#### 统一数据查询 +```graphql +# 用户查询示例 +query GetUserProfile($userId: ID!) { + user(id: $userId) { + id + username + avatar + photos(first: 10) { + edges { + node { + id + title + thumbnail + createdAt + } + } + } + followers { + count + } + following { + count + } + } +} + +# 照片查询示例 +query GetPhotos($filters: PhotoFilters, $sort: PhotoSort) { + photos(filters: $filters, sort: $sort) { + edges { + node { + id + title + description + formats { + type + url + } + tags { + name + color + } + author { + username + avatar + } + stats { + views + likes + comments + } + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +#### 实时订阅 +```graphql +# 实时通知订阅 +subscription NotificationUpdates($userId: ID!) { + notificationUpdates(userId: $userId) { + id + type + message + createdAt + read + } +} + +# 实时评论订阅 +subscription CommentUpdates($photoId: ID!) { + commentUpdates(photoId: $photoId) { + id + content + author { + username + avatar + } + createdAt + } +} +``` + +### 消息队列系统 + +#### 异步任务处理 +- **图片处理队列**: 图片上传、转换、优化 +- **AI分析队列**: 图像识别、标签生成 +- **通知队列**: 消息推送、邮件发送 +- **搜索索引队列**: 搜索索引更新 + +#### 事件驱动架构 +- **用户事件**: 注册、登录、关注 +- **照片事件**: 上传、更新、删除 +- **社交事件**: 评论、点赞、分享 +- **系统事件**: 异常、性能、安全 + +### 监控体系 + +#### Prometheus + Grafana +- **应用监控**: 服务性能、错误率 +- **基础设施监控**: 服务器、数据库 +- **业务监控**: 用户行为、转化率 +- **告警系统**: 异常检测和通知 + +#### 日志中心 (ELK Stack) +- **日志收集**: 各服务日志统一收集 +- **日志分析**: 错误分析、性能分析 +- **日志检索**: 快速定位问题 +- **日志可视化**: 图表和仪表板 + +## 🔧 技术栈升级 + +### 前端技术栈 +```yaml +v2.0 前端升级: + - Framework: Next.js 15+ (保持) + - State Management: Redux Toolkit + RTK Query + - GraphQL Client: Apollo Client + - PWA: Workbox + Service Worker + - Testing: Jest + React Testing Library + - Mobile: React Native (新增) +``` + +### 后端技术栈 +```yaml +v2.0 后端升级: + - Microservices: Golang + gRPC + - API Gateway: Kong/Envoy + - Message Queue: RabbitMQ/Kafka + - Search Engine: ElasticSearch + - AI/ML: Python + TensorFlow/PyTorch + - Container: Kubernetes +``` + +### 数据存储 +```yaml +v2.0 存储升级: + - Primary DB: PostgreSQL (分库分表) + - Search DB: ElasticSearch + - Cache: Redis Cluster + - Object Storage: MinIO Cluster + - Time Series: InfluxDB (监控数据) +``` + +## 📊 性能目标 + +### 性能指标 +- **页面加载时间**: < 2s (优化前 < 3s) +- **搜索响应时间**: < 200ms (新增) +- **图片处理时间**: < 5s (优化前 < 10s) +- **并发用户数**: 10,000+ (优化前 1,000+) + +### 可用性目标 +- **系统可用性**: 99.9% +- **数据持久性**: 99.99% +- **恢复时间**: < 5分钟 +- **备份频率**: 实时备份 + +## 🧪 测试策略 + +### 测试类型扩展 +- **微服务测试**: 服务间集成测试 +- **性能测试**: 负载测试、压力测试 +- **安全测试**: 渗透测试、漏洞扫描 +- **兼容性测试**: 多浏览器、多设备测试 + +### 测试自动化 +- **CI/CD管道**: 自动化测试和部署 +- **测试覆盖率**: 前端70%+, 后端85%+ +- **测试环境**: 开发、测试、预发布、生产 +- **监控测试**: 生产环境监控和告警 + +## 🚀 部署策略 + +### 容器化部署 +- **Kubernetes**: 服务编排和管理 +- **Docker**: 应用容器化 +- **Helm**: 应用包管理 +- **Istio**: 服务网格管理 + +### 灰度发布 +- **金丝雀发布**: 小流量验证 +- **蓝绿部署**: 零宕机部署 +- **A/B测试**: 功能验证 +- **回滚策略**: 快速回滚机制 + +## 📈 发布计划 + +### 开发阶段 + +#### 阶段1: 基础设施 (2024-06-01 ~ 2024-08-01) +- [ ] 微服务架构搭建 +- [ ] API Gateway配置 +- [ ] 消息队列系统 +- [ ] 监控体系建设 + +#### 阶段2: 核心功能 (2024-08-01 ~ 2024-10-01) +- [ ] AI智能标签系统 +- [ ] 高级搜索引擎 +- [ ] 社交功能开发 +- [ ] PWA功能实现 + +#### 阶段3: 优化完善 (2024-10-01 ~ 2024-12-01) +- [ ] 性能优化 +- [ ] 国际化支持 +- [ ] 测试和修复 +- [ ] 文档完善 + +### 里程碑 +- **M1**: 微服务架构完成 (2024-08-01) +- **M2**: AI功能上线 (2024-09-01) +- **M3**: 搜索系统上线 (2024-10-01) +- **M4**: 社交功能上线 (2024-11-01) +- **M5**: v2.0正式发布 (2024-12-01) + +## 💰 成本评估 + +### 开发成本 +- **人力成本**: 5-8人团队 × 6个月 +- **技术成本**: 新技术学习和培训 +- **测试成本**: 测试环境和工具 +- **部署成本**: 云服务器和存储 + +### 运维成本 +- **服务器成本**: 增加2-3倍 +- **存储成本**: 增加5-10倍 +- **带宽成本**: 增加3-5倍 +- **监控成本**: 新增监控工具 + +## 🎯 成功指标 + +### 技术指标 +- **系统性能**: 响应时间提升50% +- **可扩展性**: 支持10倍用户增长 +- **稳定性**: 99.9%系统可用性 +- **安全性**: 通过安全审计 + +### 业务指标 +- **用户活跃度**: 提升30% +- **用户留存率**: 提升25% +- **内容质量**: AI标签准确率90%+ +- **搜索效率**: 搜索成功率95%+ + +## 🔮 未来展望 + +### v2.1 增强功能 +- **视频支持**: 支持视频作品上传和管理 +- **直播功能**: 摄影教学直播 +- **VR/AR**: 虚拟现实作品展示 +- **区块链**: 作品版权保护 + +### v3.0 愿景 +- **全球化平台**: 多地区部署 +- **AI助手**: 智能摄影助手 +- **生态系统**: 完整的摄影生态 +- **商业化**: 作品销售和授权 + +--- + +📅 **最后更新**: 2024-01-15 +📝 **文档版本**: v2.0 (规划) +👨‍💻 **维护者**: Claude Code Assistant +🎯 **状态**: 规划阶段,等待v1.0完成后开始实施 \ No newline at end of file