diff --git a/.promptx/pouch.json b/.promptx/pouch.json new file mode 100644 index 0000000..d9a4533 --- /dev/null +++ b/.promptx/pouch.json @@ -0,0 +1,23 @@ +{ + "currentState": "service_discovery", + "stateHistory": [ + { + "from": "initial", + "command": "init", + "timestamp": "2025-07-21T14:37:13.506Z", + "args": [ + { + "workingDirectory": "/Users/iriver/workspace/photography", + "ideType": "trae" + } + ] + }, + { + "from": "initialized", + "command": "welcome", + "timestamp": "2025-07-21T14:37:23.299Z", + "args": [] + } + ], + "lastUpdated": "2025-07-21T14:37:23.301Z" +} diff --git a/.promptx/resource/project.registry.json b/.promptx/resource/project.registry.json new file mode 100644 index 0000000..01a2813 --- /dev/null +++ b/.promptx/resource/project.registry.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "source": "project", + "metadata": { + "version": "2.0.0", + "description": "project 级资源注册表", + "createdAt": "2025-07-21T14:37:13.518Z", + "updatedAt": "2025-07-21T14:37:13.519Z", + "resourceCount": 0 + }, + "resources": [], + "stats": { + "totalResources": 0, + "byProtocol": {}, + "bySource": {} + } +} diff --git a/docs/README.md b/docs/README.md index 9403749..6117e73 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,211 +2,53 @@ ## 📚 文档结构 -本项目采用版本化的文档管理策略,按照功能迭代分为不同版本,便于开发和维护。 +本项目文档采用清晰的模块化结构,便于快速查找和维护。 ``` 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/ # 部署文档 +├── README.md # 文档总览(当前文件) +├── guides/ # 📖 用户指南 +├── technical/ # 🔧 技术文档 +├── operations/ # 🚀 运维部署 +└── api/ # 📡 API文档 ``` -## 🚀 版本规划 +## 🎯 快速导航 -### v1.0 - 核心功能版本 (当前开发) +### 👥 普通用户 +- [用户指南](./guides/) - 网站使用教程 -**目标**: 实现完整的摄影作品集网站核心功能 +### 💻 开发者 +- [技术文档](./technical/) - 架构设计和开发指南 +- [API文档](./api/) - 接口说明和示例 -#### 📋 功能范围 -- ✅ **前端展示**: Next.js 15 + React 19 静态网站 -- 🔄 **管理后台**: React + TypeScript 管理界面 -- 🔄 **后端API**: Golang + Gin + PostgreSQL -- 🔄 **图片处理**: 多格式转换和优化 -- 🔄 **用户管理**: JWT认证和权限控制 -- 🔄 **文件存储**: MinIO/S3 对象存储 +### 🛠️ 运维人员 +- [运维部署](./operations/) - 部署配置和运维指南 -#### 📖 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 +| 目录 | 用途 | 目标用户 | +|---|---|---| +| `guides/` | 使用教程和帮助文档 | 所有用户 | +| `technical/` | 技术架构和开发文档 | 开发者 | +| `operations/` | 部署配置和运维文档 | 运维人员 | +| `api/` | API接口文档 | 前后端开发者 | -后端: - - Golang + Gin Framework - - GORM + PostgreSQL - - Redis + MinIO/S3 +## 🚀 快速开始 -部署: - - Docker + Docker Compose - - Caddy Web Server - - Gitea Actions CI/CD -``` +### 新用户入门 +1. 查看 [用户指南](./guides/) 了解网站功能 +2. 阅读 [技术文档](./technical/) 了解项目架构 +3. 参考 [运维部署](./operations/) 进行环境搭建 -### v2.0 - 高级功能版本 (规划中) +### 开发者工作流 +1. **前端开发**: 查看技术文档中的前端部分 +2. **后端开发**: 查看技术文档中的后端部分 +3. **API对接**: 查看api目录下的接口文档 +4. **部署上线**: 参考运维部署文档 -**目标**: 扩展高级功能,优化性能和用户体验 +## 📞 技术支持 -#### 🎯 规划功能 -- 🔮 **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流程 - - 监控和日志 - -## 📞 联系方式 - -### 项目维护者 -- **项目负责人**: [待填写] -- **技术负责人**: [待填写] +- **项目地址**: https://photography.iriver.top - **文档维护**: 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 +- **最后更新**: 2024-12-21 \ No newline at end of file diff --git a/docs/deployment-existing-services.md b/docs/deployment-existing-services.md deleted file mode 100644 index 5ad2895..0000000 --- a/docs/deployment-existing-services.md +++ /dev/null @@ -1,212 +0,0 @@ -# 使用现有服务的部署指南 - -本指南适用于已有 PostgreSQL 和 Redis 服务的情况。 - -## 📋 前提条件 - -确保你的服务器上已经安装并运行: -- **PostgreSQL** (推荐版本 14+) -- **Redis** (推荐版本 6+) - -## ⚙️ 配置步骤 - -### 1. 环境变量配置 - -复制环境变量模板: -```bash -cp .env.example .env -``` - -编辑 `.env` 文件,配置你的现有服务: - -```bash -# 数据库配置 -DB_HOST=localhost # 或你的 PostgreSQL 服务器地址 -DB_PORT=5432 # PostgreSQL 端口 -DB_NAME=photography # 数据库名称 -DB_USER=postgres # 数据库用户 -DB_PASSWORD=your_actual_password # 数据库密码 - -# Redis 配置 -REDIS_HOST=localhost # 或你的 Redis 服务器地址 -REDIS_PORT=6379 # Redis 端口 -REDIS_PASSWORD=your_redis_password # Redis 密码 (如果有) - -# JWT 配置 -JWT_SECRET=your_jwt_secret_at_least_32_characters_long -JWT_EXPIRES_IN=24h -``` - -### 2. 数据库准备 - -连接到你的 PostgreSQL,创建数据库: - -```sql --- 创建数据库 -CREATE DATABASE photography; - --- 创建用户 (如果需要) -CREATE USER photography_user WITH ENCRYPTED PASSWORD 'your_password'; -GRANT ALL PRIVILEGES ON DATABASE photography TO photography_user; - --- 使用数据库 -\c photography; - --- 创建必要的扩展 -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -``` - -### 3. 后端服务部署 - -#### 方式一:Docker 部署 (推荐) - -```bash -# 构建并启动后端服务 -docker-compose up -d backend - -# 查看日志 -docker-compose logs -f backend - -# 检查健康状态 -curl http://localhost:8080/health -``` - -#### 方式二:直接运行 - -```bash -# 进入后端目录 -cd backend - -# 安装依赖 -go mod download - -# 运行数据库迁移 -go run cmd/server/main.go migrate - -# 启动服务 -go run cmd/server/main.go -``` - -### 4. 数据库迁移 - -如果你的后端支持自动迁移,服务启动时会自动创建表结构。 - -如果需要手动迁移: -```bash -# 使用 Docker -docker-compose exec backend ./main migrate - -# 或者直接运行 -cd backend && go run cmd/server/main.go migrate -``` - -### 5. 验证部署 - -```bash -# 检查后端健康状态 -curl http://localhost:8080/health - -# 检查数据库连接 -curl http://localhost:8080/api/v1/health - -# 检查 Redis 连接 (如果有相关接口) -curl http://localhost:8080/api/v1/cache/health -``` - -## 🔧 常见问题 - -### 数据库连接失败 - -1. **检查数据库是否运行**: - ```bash - sudo systemctl status postgresql - ``` - -2. **检查连接权限**: - ```bash - psql -h localhost -U postgres -d photography - ``` - -3. **检查防火墙**: - ```bash - sudo ufw status - ``` - -### Redis 连接失败 - -1. **检查 Redis 状态**: - ```bash - sudo systemctl status redis - ``` - -2. **测试 Redis 连接**: - ```bash - redis-cli ping - ``` - -### 容器网络问题 - -如果容器无法访问宿主机服务,尝试: - -1. **使用 host 网络模式** (已配置) -2. **检查服务绑定地址**: - - PostgreSQL: 确保监听 `0.0.0.0:5432` 或 `localhost:5432` - - Redis: 确保监听 `0.0.0.0:6379` 或 `localhost:6379` - -## 📊 监控和维护 - -### 日志查看 - -```bash -# 后端日志 -docker-compose logs -f backend - -# 系统日志 -journalctl -u your-service-name -f -``` - -### 性能监控 - -```bash -# 运行监控脚本 -./scripts/monitor.sh - -# 手动检查资源使用 -docker stats photography_backend -``` - -### 备份 - -```bash -# 手动备份数据库 -./scripts/backup.sh - -# 或者使用 Docker 备份服务 -docker-compose --profile backup up backup -``` - -## 🚀 自动化部署 - -如果你使用 CI/CD,确保在部署环境中设置正确的环境变量: - -```yaml -# .github/workflows 或 .gitea/workflows -env: - DB_HOST: your-db-host - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - REDIS_HOST: your-redis-host - REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} -``` - -## 🔒 安全建议 - -1. **使用强密码** -2. **限制数据库访问**:只允许必要的 IP 连接 -3. **启用 SSL/TLS**:用于数据库和 Redis 连接 -4. **定期更新**:保持服务和依赖的最新版本 -5. **监控日志**:定期检查异常访问 - ---- - -这种配置更加轻量和实用,避免了重复安装已有服务的资源浪费。 \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 42fd02c..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,760 +0,0 @@ -# 摄影作品集项目部署文档 - -本文档详细说明了摄影作品集项目的完整部署流程,包括前端、管理后台、后端 API 和数据库的部署配置。 - -## 📋 目录 - -- [系统要求](#系统要求) -- [环境配置](#环境配置) -- [后端部署](#后端部署) -- [前端部署](#前端部署) -- [管理后台部署](#管理后台部署) -- [数据库配置](#数据库配置) -- [Web 服务器配置](#web-服务器配置) -- [SSL 证书配置](#ssl-证书配置) -- [监控和备份](#监控和备份) -- [故障排除](#故障排除) - -## 🖥️ 系统要求 - -### 硬件要求 -- **CPU**: 2 核心以上 -- **内存**: 4GB 以上 (推荐 8GB) -- **存储**: 20GB 以上 SSD 存储 -- **网络**: 稳定的互联网连接 - -### 软件要求 -- **操作系统**: Ubuntu 20.04 LTS / CentOS 8 / Debian 11 -- **Docker**: 20.10 或更高版本 -- **Docker Compose**: 2.0 或更高版本 -- **Git**: 2.20 或更高版本 -- **域名**: 用于生产环境部署 - -## ⚙️ 环境配置 - -### 1. 安装 Docker 和 Docker Compose - -```bash -# Ubuntu/Debian -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh -sudo usermod -aG docker $USER - -# 安装 Docker Compose -sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose - -# 验证安装 -docker --version -docker-compose --version -``` - -### 2. 克隆项目仓库 - -```bash -git clone photography -cd photography -``` - -### 3. 创建环境变量文件 - -```bash -# 复制环境变量模板 -cp .env.example .env - -# 编辑环境变量 -nano .env -``` - -## 🚀 后端部署 - -### 1. 后端 Docker 配置 - -查看后端 Dockerfile: - -```dockerfile -# backend/Dockerfile -FROM golang:1.21-alpine AS builder - -# 设置工作目录 -WORKDIR /app - -# 安装依赖 -RUN apk add --no-cache git ca-certificates tzdata - -# 复制 go mod 文件 -COPY go.mod go.sum ./ -RUN go mod download - -# 复制源代码 -COPY . . - -# 构建应用 -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go - -# 生产环境镜像 -FROM alpine:latest - -# 安装必要的包 -RUN apk --no-cache add ca-certificates wget - -# 创建用户 -RUN addgroup -S appgroup && adduser -S appuser -G appgroup - -# 设置工作目录 -WORKDIR /app - -# 从构建阶段复制文件 -COPY --from=builder /app/main . -COPY --from=builder /app/configs ./configs -COPY --from=builder /app/migrations ./migrations - -# 创建必要的目录 -RUN mkdir -p uploads logs && chown -R appuser:appgroup /app - -# 切换到非 root 用户 -USER appuser - -# 暴露端口 -EXPOSE 8080 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -# 启动命令 -CMD ["./main"] -``` - -### 2. 后端 Docker Compose 服务 - -```yaml -# docker-compose.yml - 后端部分 -services: - # PostgreSQL 数据库 - postgres: - image: postgres:15-alpine - container_name: photography_postgres - restart: unless-stopped - environment: - POSTGRES_DB: ${DB_NAME:-photography} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./backend/migrations:/docker-entrypoint-initdb.d - ports: - - "127.0.0.1:5432:5432" - networks: - - photography_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-photography}"] - interval: 30s - timeout: 10s - retries: 5 - - # Redis 缓存 - redis: - image: redis:7-alpine - container_name: photography_redis - restart: unless-stopped - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} - volumes: - - redis_data:/data - ports: - - "127.0.0.1:6379:6379" - networks: - - photography_network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - - # 后端 API 服务 - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: photography_backend - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - environment: - # 数据库配置 - DB_HOST: postgres - DB_PORT: 5432 - DB_NAME: ${DB_NAME:-photography} - DB_USER: ${DB_USER:-postgres} - DB_PASSWORD: ${DB_PASSWORD} - - # Redis 配置 - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: ${REDIS_PASSWORD} - - # JWT 配置 - JWT_SECRET: ${JWT_SECRET} - JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h} - - # 服务器配置 - PORT: 8080 - GIN_MODE: release - - # 文件存储配置 - STORAGE_TYPE: ${STORAGE_TYPE:-local} - STORAGE_PATH: /app/uploads - MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-10MB} - - # 日志配置 - LOG_LEVEL: ${LOG_LEVEL:-info} - LOG_FORMAT: json - volumes: - - ./backend/uploads:/app/uploads - - ./backend/logs:/app/logs - - ./backend/configs:/app/configs - ports: - - "127.0.0.1:8080:8080" - networks: - - photography_network - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - -networks: - photography_network: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 -``` - -### 3. 启动后端服务 - -```bash -# 构建并启动后端服务 -docker-compose up -d postgres redis -docker-compose up -d backend - -# 查看日志 -docker-compose logs -f backend - -# 验证服务状态 -curl http://localhost:8080/health -``` - -## 🌐 前端部署 - -### 1. 前端构建配置 - -```bash -# 进入前端目录 -cd frontend/ - -# 安装依赖 -bun install - -# 构建生产版本 -bun run build - -# 验证构建结果 -ls -la out/ -``` - -### 2. 前端部署脚本 - -创建前端部署脚本: - -```bash -#!/bin/bash -# scripts/deploy-frontend.sh - -set -e - -echo "🚀 开始部署前端..." - -# 配置变量 -FRONTEND_DIR="/home/gitea/www/photography" -BACKUP_DIR="/home/gitea/backups/photography-frontend" -BUILD_DIR="./frontend/out" - -# 创建备份 -echo "📦 创建当前版本备份..." -mkdir -p "$BACKUP_DIR" -if [ -d "$FRONTEND_DIR" ]; then - tar -czf "$BACKUP_DIR/frontend-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$FRONTEND_DIR" . -fi - -# 创建部署目录 -echo "📁 准备部署目录..." -mkdir -p "$FRONTEND_DIR" - -# 部署新版本 -echo "🔄 部署新版本..." -rsync -av --delete "$BUILD_DIR/" "$FRONTEND_DIR/" - -# 设置权限 -echo "🔐 设置文件权限..." -chown -R gitea:gitea "$FRONTEND_DIR" -chmod -R 755 "$FRONTEND_DIR" - -# 清理旧备份 (保留最近5个) -echo "🧹 清理旧备份..." -cd "$BACKUP_DIR" -ls -t frontend-*.tar.gz | tail -n +6 | xargs -r rm - -echo "✅ 前端部署完成!" -echo "🌐 访问地址: https://photography.iriver.top" -``` - -## 🛠️ 管理后台部署 - -### 1. 管理后台构建配置 - -```bash -# 进入管理后台目录 -cd admin/ - -# 安装依赖 -npm install - -# 构建生产版本 -npm run build - -# 验证构建结果 -ls -la dist/ -``` - -### 2. 管理后台部署脚本 - -```bash -#!/bin/bash -# scripts/deploy-admin.sh - -set -e - -echo "🚀 开始部署管理后台..." - -# 配置变量 -ADMIN_DIR="/home/gitea/www/photography-admin" -BACKUP_DIR="/home/gitea/backups/photography-admin" -BUILD_DIR="./admin/dist" - -# 创建备份 -echo "📦 创建当前版本备份..." -mkdir -p "$BACKUP_DIR" -if [ -d "$ADMIN_DIR" ]; then - tar -czf "$BACKUP_DIR/admin-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$ADMIN_DIR" . -fi - -# 创建部署目录 -echo "📁 准备部署目录..." -mkdir -p "$ADMIN_DIR" - -# 部署新版本 -echo "🔄 部署新版本..." -rsync -av --delete "$BUILD_DIR/" "$ADMIN_DIR/" - -# 设置权限 -echo "🔐 设置文件权限..." -chown -R gitea:gitea "$ADMIN_DIR" -chmod -R 755 "$ADMIN_DIR" - -# 清理旧备份 (保留最近5个) -echo "🧹 清理旧备份..." -cd "$BACKUP_DIR" -ls -t admin-*.tar.gz | tail -n +6 | xargs -r rm - -echo "✅ 管理后台部署完成!" -echo "🌐 访问地址: https://admin.photography.iriver.top" -``` - -## 🗄️ 数据库配置 - -### 1. 数据库初始化 - -```sql --- 创建数据库 -CREATE DATABASE photography; -CREATE USER photography_user WITH ENCRYPTED PASSWORD 'your_password'; -GRANT ALL PRIVILEGES ON DATABASE photography TO photography_user; - --- 使用数据库 -\c photography; - --- 创建必要的扩展 -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; - --- 设置时区 -SET timezone = 'Asia/Shanghai'; -``` - -### 2. 数据库备份脚本 - -```bash -#!/bin/bash -# scripts/backup.sh - -set -e - -# 配置变量 -DB_HOST="${DB_HOST:-postgres}" -DB_NAME="${DB_NAME:-photography}" -DB_USER="${DB_USER:-postgres}" -BACKUP_DIR="/backups" -DATE=$(date +%Y%m%d-%H%M%S) - -echo "🗄️ 开始数据库备份..." - -# 创建备份目录 -mkdir -p "$BACKUP_DIR" - -# 执行备份 -pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" \ - --no-password --verbose --clean --no-acl --no-owner \ - -f "$BACKUP_DIR/photography-$DATE.sql" - -# 压缩备份文件 -gzip "$BACKUP_DIR/photography-$DATE.sql" - -# 清理旧备份 (保留最近7天) -find "$BACKUP_DIR" -name "photography-*.sql.gz" -mtime +7 -delete - -echo "✅ 数据库备份完成: photography-$DATE.sql.gz" -``` - -## 🌐 Web 服务器配置 - -### Caddy 配置文件 - -```caddyfile -# docs/deployment/Caddyfile - -# 前端网站 -photography.iriver.top { - # 根目录 - root * /home/gitea/www/photography - - # 静态文件服务 - file_server - - # SPA 路由支持 - try_files {path} /index.html - - # 压缩 - encode gzip - - # 缓存配置 - header { - # 静态资源缓存 - Cache-Control "public, max-age=31536000" { - path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.woff *.woff2 - } - - # HTML 文件不缓存 - Cache-Control "no-cache" { - path *.html - } - - # 安全头 - X-Frame-Options "DENY" - X-Content-Type-Options "nosniff" - Referrer-Policy "strict-origin-when-cross-origin" - } - - # 日志 - log { - output file /var/log/caddy/photography.log - format json - } -} - -# 管理后台 -admin.photography.iriver.top { - # 根目录 - root * /home/gitea/www/photography-admin - - # 静态文件服务 - file_server - - # SPA 路由支持 - try_files {path} /index.html - - # 压缩 - encode gzip - - # 基本认证 (可选) - # basicauth { - # admin $2a$14$encrypted_password_hash - # } - - # 缓存配置 - header { - Cache-Control "public, max-age=31536000" { - path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.woff *.woff2 - } - Cache-Control "no-cache" { - path *.html - } - X-Frame-Options "SAMEORIGIN" - X-Content-Type-Options "nosniff" - } - - # 日志 - log { - output file /var/log/caddy/admin.photography.log - format json - } -} - -# API 服务 -api.photography.iriver.top { - # 反向代理到后端服务 - reverse_proxy localhost:8080 { - # 健康检查 - health_uri /health - health_interval 30s - health_timeout 10s - - # 请求头 - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-Proto {scheme} - header_up X-Forwarded-For {remote_host} - } - - # CORS 处理 - header { - Access-Control-Allow-Origin "https://photography.iriver.top, https://admin.photography.iriver.top" - Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" - Access-Control-Allow-Headers "Content-Type, Authorization" - Access-Control-Max-Age "86400" - } - - # 预检请求处理 - @options method OPTIONS - respond @options 204 - - # 上传文件大小限制 - request_body { - max_size 10MB - } - - # 日志 - log { - output file /var/log/caddy/api.photography.log - format json - } -} - -# 全局配置 -{ - # 自动 HTTPS - auto_https on - - # Let's Encrypt 邮箱 - email admin@iriver.top - - # 日志级别 - log { - level INFO - } -} -``` - -## 🔒 SSL 证书配置 - -Caddy 会自动处理 SSL 证书,但也可以手动配置: - -```bash -# 手动获取证书 (如果需要) -certbot certonly --webroot -w /var/www/html -d photography.iriver.top -d admin.photography.iriver.top -d api.photography.iriver.top - -# 设置自动续期 -echo "0 3 * * * certbot renew --quiet && systemctl reload caddy" | crontab - -``` - -## 📊 监控和备份 - -### 1. 系统监控脚本 - -```bash -#!/bin/bash -# scripts/monitor.sh - -# 检查服务状态 -check_service() { - local service=$1 - local url=$2 - - echo "检查 $service..." - if curl -f -s "$url" > /dev/null; then - echo "✅ $service 正常运行" - else - echo "❌ $service 服务异常" - # 发送告警 (可集成邮件、微信等) - # send_alert "$service 服务异常" - fi -} - -# 检查磁盘空间 -check_disk() { - local usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//') - echo "磁盘使用率: $usage%" - - if [ "$usage" -gt 80 ]; then - echo "⚠️ 磁盘空间不足" - # send_alert "磁盘空间不足: $usage%" - fi -} - -# 检查内存使用 -check_memory() { - local usage=$(free | grep Mem | awk '{printf "%.1f", $3/$2 * 100.0}') - echo "内存使用率: $usage%" -} - -# 执行检查 -echo "🔍 开始系统监控..." -check_service "前端" "https://photography.iriver.top" -check_service "管理后台" "https://admin.photography.iriver.top" -check_service "API" "https://api.photography.iriver.top/health" -check_disk -check_memory -echo "✅ 监控检查完成" -``` - -### 2. 自动备份任务 - -```bash -# 设置定时任务 -crontab -e - -# 添加以下内容: -# 每天凌晨2点备份数据库 -0 2 * * * /path/to/photography/scripts/backup.sh - -# 每周日凌晨3点重启服务 (可选) -0 3 * * 0 cd /path/to/photography && docker-compose restart - -# 每小时检查一次服务状态 -0 * * * * /path/to/photography/scripts/monitor.sh -``` - -## 🐛 故障排除 - -### 常见问题和解决方案 - -#### 1. 后端服务无法启动 -```bash -# 查看日志 -docker-compose logs backend - -# 检查配置 -docker-compose config - -# 重新构建 -docker-compose build backend --no-cache -``` - -#### 2. 数据库连接失败 -```bash -# 检查数据库状态 -docker-compose ps postgres - -# 检查数据库日志 -docker-compose logs postgres - -# 手动连接测试 -docker-compose exec postgres psql -U postgres -d photography -``` - -#### 3. 前端/管理后台访问异常 -```bash -# 检查文件权限 -ls -la /home/gitea/www/photography/ -ls -la /home/gitea/www/photography-admin/ - -# 检查 Caddy 配置 -caddy validate --config /etc/caddy/Caddyfile - -# 重新加载 Caddy -systemctl reload caddy -``` - -#### 4. SSL 证书问题 -```bash -# 检查证书状态 -curl -I https://photography.iriver.top - -# 查看 Caddy 日志 -tail -f /var/log/caddy/photography.log - -# 手动重新申请证书 -systemctl stop caddy -caddy run --config /etc/caddy/Caddyfile -``` - -### 日志文件位置 -- **后端日志**: `./backend/logs/` -- **Caddy 日志**: `/var/log/caddy/` -- **数据库日志**: `docker-compose logs postgres` -- **系统日志**: `/var/log/syslog` - -## 🚀 部署命令速查 - -```bash -# 完整部署流程 -git pull origin main -cd photography - -# 后端部署 -docker-compose build backend -docker-compose up -d - -# 前端部署 -cd frontend && bun run build && cd .. -./scripts/deploy-frontend.sh - -# 管理后台部署 -cd admin && npm run build && cd .. -./scripts/deploy-admin.sh - -# 重启 Web 服务器 -sudo systemctl reload caddy - -# 检查服务状态 -docker-compose ps -curl https://photography.iriver.top -curl https://admin.photography.iriver.top -curl https://api.photography.iriver.top/health -``` - -## 📞 技术支持 - -如果在部署过程中遇到问题,请: - -1. 查看相关日志文件 -2. 检查系统资源使用情况 -3. 验证配置文件语法 -4. 联系技术支持团队 - ---- - -📝 **文档版本**: v1.0 -🕒 **最后更新**: 2024年 -👥 **维护团队**: 摄影作品集项目组 \ No newline at end of file diff --git a/docs/user-guide/README.md b/docs/guides/README.md similarity index 100% rename from docs/user-guide/README.md rename to docs/guides/README.md diff --git a/docs/deployment/CLAUDE.md b/docs/operations/deployment/CLAUDE.md similarity index 100% rename from docs/deployment/CLAUDE.md rename to docs/operations/deployment/CLAUDE.md diff --git a/docs/deployment/Caddyfile b/docs/operations/deployment/Caddyfile similarity index 100% rename from docs/deployment/Caddyfile rename to docs/operations/deployment/Caddyfile diff --git a/docs/deployment/README.md b/docs/operations/deployment/README.md similarity index 100% rename from docs/deployment/README.md rename to docs/operations/deployment/README.md diff --git a/docs/deployment/caddy-setup.md b/docs/operations/deployment/caddy-setup.md similarity index 100% rename from docs/deployment/caddy-setup.md rename to docs/operations/deployment/caddy-setup.md diff --git a/docs/deployment/fix-caddy-permissions.sh b/docs/operations/deployment/fix-caddy-permissions.sh similarity index 100% rename from docs/deployment/fix-caddy-permissions.sh rename to docs/operations/deployment/fix-caddy-permissions.sh diff --git a/docs/development/README.md b/docs/technical/README.md similarity index 100% rename from docs/development/README.md rename to docs/technical/README.md diff --git a/docs/development/saved-docs/api-design.md b/docs/technical/saved-docs/api-design.md similarity index 100% rename from docs/development/saved-docs/api-design.md rename to docs/technical/saved-docs/api-design.md diff --git a/docs/development/saved-docs/management-backend.md b/docs/technical/saved-docs/management-backend.md similarity index 100% rename from docs/development/saved-docs/management-backend.md rename to docs/technical/saved-docs/management-backend.md diff --git a/docs/development/saved-docs/operations-monitoring.md b/docs/technical/saved-docs/operations-monitoring.md similarity index 100% rename from docs/development/saved-docs/operations-monitoring.md rename to docs/technical/saved-docs/operations-monitoring.md diff --git a/docs/development/saved-docs/v1.0-overview.md b/docs/technical/saved-docs/v1.0-overview.md similarity index 100% rename from docs/development/saved-docs/v1.0-overview.md rename to docs/technical/saved-docs/v1.0-overview.md diff --git a/ui/.gitignore b/docs/ui/.gitignore similarity index 100% rename from ui/.gitignore rename to docs/ui/.gitignore diff --git a/ui/CLAUDE.md b/docs/ui/CLAUDE.md similarity index 100% rename from ui/CLAUDE.md rename to docs/ui/CLAUDE.md diff --git a/ui/app/globals.css b/docs/ui/app/globals.css similarity index 100% rename from ui/app/globals.css rename to docs/ui/app/globals.css diff --git a/ui/app/layout.tsx b/docs/ui/app/layout.tsx similarity index 100% rename from ui/app/layout.tsx rename to docs/ui/app/layout.tsx diff --git a/ui/app/page.tsx b/docs/ui/app/page.tsx similarity index 100% rename from ui/app/page.tsx rename to docs/ui/app/page.tsx diff --git a/ui/components.json b/docs/ui/components.json similarity index 100% rename from ui/components.json rename to docs/ui/components.json diff --git a/ui/components/about-view.tsx b/docs/ui/components/about-view.tsx similarity index 100% rename from ui/components/about-view.tsx rename to docs/ui/components/about-view.tsx diff --git a/ui/components/contact-view.tsx b/docs/ui/components/contact-view.tsx similarity index 100% rename from ui/components/contact-view.tsx rename to docs/ui/components/contact-view.tsx diff --git a/ui/components/filter-bar.tsx b/docs/ui/components/filter-bar.tsx similarity index 100% rename from ui/components/filter-bar.tsx rename to docs/ui/components/filter-bar.tsx diff --git a/ui/components/loading-spinner.tsx b/docs/ui/components/loading-spinner.tsx similarity index 100% rename from ui/components/loading-spinner.tsx rename to docs/ui/components/loading-spinner.tsx diff --git a/ui/components/navigation.tsx b/docs/ui/components/navigation.tsx similarity index 100% rename from ui/components/navigation.tsx rename to docs/ui/components/navigation.tsx diff --git a/ui/components/photo-gallery.tsx b/docs/ui/components/photo-gallery.tsx similarity index 100% rename from ui/components/photo-gallery.tsx rename to docs/ui/components/photo-gallery.tsx diff --git a/ui/components/photo-modal.tsx b/docs/ui/components/photo-modal.tsx similarity index 100% rename from ui/components/photo-modal.tsx rename to docs/ui/components/photo-modal.tsx diff --git a/ui/components/theme-provider.tsx b/docs/ui/components/theme-provider.tsx similarity index 100% rename from ui/components/theme-provider.tsx rename to docs/ui/components/theme-provider.tsx diff --git a/ui/components/timeline-stats.tsx b/docs/ui/components/timeline-stats.tsx similarity index 100% rename from ui/components/timeline-stats.tsx rename to docs/ui/components/timeline-stats.tsx diff --git a/ui/components/timeline-view.tsx b/docs/ui/components/timeline-view.tsx similarity index 100% rename from ui/components/timeline-view.tsx rename to docs/ui/components/timeline-view.tsx diff --git a/ui/components/ui/accordion.tsx b/docs/ui/components/ui/accordion.tsx similarity index 100% rename from ui/components/ui/accordion.tsx rename to docs/ui/components/ui/accordion.tsx diff --git a/ui/components/ui/alert-dialog.tsx b/docs/ui/components/ui/alert-dialog.tsx similarity index 100% rename from ui/components/ui/alert-dialog.tsx rename to docs/ui/components/ui/alert-dialog.tsx diff --git a/ui/components/ui/alert.tsx b/docs/ui/components/ui/alert.tsx similarity index 100% rename from ui/components/ui/alert.tsx rename to docs/ui/components/ui/alert.tsx diff --git a/ui/components/ui/aspect-ratio.tsx b/docs/ui/components/ui/aspect-ratio.tsx similarity index 100% rename from ui/components/ui/aspect-ratio.tsx rename to docs/ui/components/ui/aspect-ratio.tsx diff --git a/ui/components/ui/avatar.tsx b/docs/ui/components/ui/avatar.tsx similarity index 100% rename from ui/components/ui/avatar.tsx rename to docs/ui/components/ui/avatar.tsx diff --git a/ui/components/ui/badge.tsx b/docs/ui/components/ui/badge.tsx similarity index 100% rename from ui/components/ui/badge.tsx rename to docs/ui/components/ui/badge.tsx diff --git a/ui/components/ui/breadcrumb.tsx b/docs/ui/components/ui/breadcrumb.tsx similarity index 100% rename from ui/components/ui/breadcrumb.tsx rename to docs/ui/components/ui/breadcrumb.tsx diff --git a/ui/components/ui/button.tsx b/docs/ui/components/ui/button.tsx similarity index 100% rename from ui/components/ui/button.tsx rename to docs/ui/components/ui/button.tsx diff --git a/ui/components/ui/calendar.tsx b/docs/ui/components/ui/calendar.tsx similarity index 100% rename from ui/components/ui/calendar.tsx rename to docs/ui/components/ui/calendar.tsx diff --git a/ui/components/ui/card.tsx b/docs/ui/components/ui/card.tsx similarity index 100% rename from ui/components/ui/card.tsx rename to docs/ui/components/ui/card.tsx diff --git a/ui/components/ui/carousel.tsx b/docs/ui/components/ui/carousel.tsx similarity index 100% rename from ui/components/ui/carousel.tsx rename to docs/ui/components/ui/carousel.tsx diff --git a/ui/components/ui/chart.tsx b/docs/ui/components/ui/chart.tsx similarity index 100% rename from ui/components/ui/chart.tsx rename to docs/ui/components/ui/chart.tsx diff --git a/ui/components/ui/checkbox.tsx b/docs/ui/components/ui/checkbox.tsx similarity index 100% rename from ui/components/ui/checkbox.tsx rename to docs/ui/components/ui/checkbox.tsx diff --git a/ui/components/ui/collapsible.tsx b/docs/ui/components/ui/collapsible.tsx similarity index 100% rename from ui/components/ui/collapsible.tsx rename to docs/ui/components/ui/collapsible.tsx diff --git a/ui/components/ui/command.tsx b/docs/ui/components/ui/command.tsx similarity index 100% rename from ui/components/ui/command.tsx rename to docs/ui/components/ui/command.tsx diff --git a/ui/components/ui/context-menu.tsx b/docs/ui/components/ui/context-menu.tsx similarity index 100% rename from ui/components/ui/context-menu.tsx rename to docs/ui/components/ui/context-menu.tsx diff --git a/ui/components/ui/dialog.tsx b/docs/ui/components/ui/dialog.tsx similarity index 100% rename from ui/components/ui/dialog.tsx rename to docs/ui/components/ui/dialog.tsx diff --git a/ui/components/ui/drawer.tsx b/docs/ui/components/ui/drawer.tsx similarity index 100% rename from ui/components/ui/drawer.tsx rename to docs/ui/components/ui/drawer.tsx diff --git a/ui/components/ui/dropdown-menu.tsx b/docs/ui/components/ui/dropdown-menu.tsx similarity index 100% rename from ui/components/ui/dropdown-menu.tsx rename to docs/ui/components/ui/dropdown-menu.tsx diff --git a/ui/components/ui/form.tsx b/docs/ui/components/ui/form.tsx similarity index 100% rename from ui/components/ui/form.tsx rename to docs/ui/components/ui/form.tsx diff --git a/ui/components/ui/hover-card.tsx b/docs/ui/components/ui/hover-card.tsx similarity index 100% rename from ui/components/ui/hover-card.tsx rename to docs/ui/components/ui/hover-card.tsx diff --git a/ui/components/ui/input-otp.tsx b/docs/ui/components/ui/input-otp.tsx similarity index 100% rename from ui/components/ui/input-otp.tsx rename to docs/ui/components/ui/input-otp.tsx diff --git a/ui/components/ui/input.tsx b/docs/ui/components/ui/input.tsx similarity index 100% rename from ui/components/ui/input.tsx rename to docs/ui/components/ui/input.tsx diff --git a/ui/components/ui/label.tsx b/docs/ui/components/ui/label.tsx similarity index 100% rename from ui/components/ui/label.tsx rename to docs/ui/components/ui/label.tsx diff --git a/ui/components/ui/menubar.tsx b/docs/ui/components/ui/menubar.tsx similarity index 100% rename from ui/components/ui/menubar.tsx rename to docs/ui/components/ui/menubar.tsx diff --git a/ui/components/ui/navigation-menu.tsx b/docs/ui/components/ui/navigation-menu.tsx similarity index 100% rename from ui/components/ui/navigation-menu.tsx rename to docs/ui/components/ui/navigation-menu.tsx diff --git a/ui/components/ui/pagination.tsx b/docs/ui/components/ui/pagination.tsx similarity index 100% rename from ui/components/ui/pagination.tsx rename to docs/ui/components/ui/pagination.tsx diff --git a/ui/components/ui/popover.tsx b/docs/ui/components/ui/popover.tsx similarity index 100% rename from ui/components/ui/popover.tsx rename to docs/ui/components/ui/popover.tsx diff --git a/ui/components/ui/progress.tsx b/docs/ui/components/ui/progress.tsx similarity index 100% rename from ui/components/ui/progress.tsx rename to docs/ui/components/ui/progress.tsx diff --git a/ui/components/ui/radio-group.tsx b/docs/ui/components/ui/radio-group.tsx similarity index 100% rename from ui/components/ui/radio-group.tsx rename to docs/ui/components/ui/radio-group.tsx diff --git a/ui/components/ui/resizable.tsx b/docs/ui/components/ui/resizable.tsx similarity index 100% rename from ui/components/ui/resizable.tsx rename to docs/ui/components/ui/resizable.tsx diff --git a/ui/components/ui/scroll-area.tsx b/docs/ui/components/ui/scroll-area.tsx similarity index 100% rename from ui/components/ui/scroll-area.tsx rename to docs/ui/components/ui/scroll-area.tsx diff --git a/ui/components/ui/select.tsx b/docs/ui/components/ui/select.tsx similarity index 100% rename from ui/components/ui/select.tsx rename to docs/ui/components/ui/select.tsx diff --git a/ui/components/ui/separator.tsx b/docs/ui/components/ui/separator.tsx similarity index 100% rename from ui/components/ui/separator.tsx rename to docs/ui/components/ui/separator.tsx diff --git a/ui/components/ui/sheet.tsx b/docs/ui/components/ui/sheet.tsx similarity index 100% rename from ui/components/ui/sheet.tsx rename to docs/ui/components/ui/sheet.tsx diff --git a/ui/components/ui/sidebar.tsx b/docs/ui/components/ui/sidebar.tsx similarity index 100% rename from ui/components/ui/sidebar.tsx rename to docs/ui/components/ui/sidebar.tsx diff --git a/ui/components/ui/skeleton.tsx b/docs/ui/components/ui/skeleton.tsx similarity index 100% rename from ui/components/ui/skeleton.tsx rename to docs/ui/components/ui/skeleton.tsx diff --git a/ui/components/ui/slider.tsx b/docs/ui/components/ui/slider.tsx similarity index 100% rename from ui/components/ui/slider.tsx rename to docs/ui/components/ui/slider.tsx diff --git a/ui/components/ui/sonner.tsx b/docs/ui/components/ui/sonner.tsx similarity index 100% rename from ui/components/ui/sonner.tsx rename to docs/ui/components/ui/sonner.tsx diff --git a/ui/components/ui/switch.tsx b/docs/ui/components/ui/switch.tsx similarity index 100% rename from ui/components/ui/switch.tsx rename to docs/ui/components/ui/switch.tsx diff --git a/ui/components/ui/table.tsx b/docs/ui/components/ui/table.tsx similarity index 100% rename from ui/components/ui/table.tsx rename to docs/ui/components/ui/table.tsx diff --git a/ui/components/ui/tabs.tsx b/docs/ui/components/ui/tabs.tsx similarity index 100% rename from ui/components/ui/tabs.tsx rename to docs/ui/components/ui/tabs.tsx diff --git a/ui/components/ui/textarea.tsx b/docs/ui/components/ui/textarea.tsx similarity index 100% rename from ui/components/ui/textarea.tsx rename to docs/ui/components/ui/textarea.tsx diff --git a/ui/components/ui/toast.tsx b/docs/ui/components/ui/toast.tsx similarity index 100% rename from ui/components/ui/toast.tsx rename to docs/ui/components/ui/toast.tsx diff --git a/ui/components/ui/toaster.tsx b/docs/ui/components/ui/toaster.tsx similarity index 100% rename from ui/components/ui/toaster.tsx rename to docs/ui/components/ui/toaster.tsx diff --git a/ui/components/ui/toggle-group.tsx b/docs/ui/components/ui/toggle-group.tsx similarity index 100% rename from ui/components/ui/toggle-group.tsx rename to docs/ui/components/ui/toggle-group.tsx diff --git a/ui/components/ui/toggle.tsx b/docs/ui/components/ui/toggle.tsx similarity index 100% rename from ui/components/ui/toggle.tsx rename to docs/ui/components/ui/toggle.tsx diff --git a/ui/components/ui/tooltip.tsx b/docs/ui/components/ui/tooltip.tsx similarity index 100% rename from ui/components/ui/tooltip.tsx rename to docs/ui/components/ui/tooltip.tsx diff --git a/ui/components/ui/use-mobile.tsx b/docs/ui/components/ui/use-mobile.tsx similarity index 100% rename from ui/components/ui/use-mobile.tsx rename to docs/ui/components/ui/use-mobile.tsx diff --git a/ui/components/ui/use-toast.ts b/docs/ui/components/ui/use-toast.ts similarity index 100% rename from ui/components/ui/use-toast.ts rename to docs/ui/components/ui/use-toast.ts diff --git a/ui/hooks/use-mobile.tsx b/docs/ui/hooks/use-mobile.tsx similarity index 100% rename from ui/hooks/use-mobile.tsx rename to docs/ui/hooks/use-mobile.tsx diff --git a/ui/hooks/use-toast.ts b/docs/ui/hooks/use-toast.ts similarity index 100% rename from ui/hooks/use-toast.ts rename to docs/ui/hooks/use-toast.ts diff --git a/ui/lib/utils.ts b/docs/ui/lib/utils.ts similarity index 100% rename from ui/lib/utils.ts rename to docs/ui/lib/utils.ts diff --git a/ui/next.config.mjs b/docs/ui/next.config.mjs similarity index 100% rename from ui/next.config.mjs rename to docs/ui/next.config.mjs diff --git a/ui/package.json b/docs/ui/package.json similarity index 100% rename from ui/package.json rename to docs/ui/package.json diff --git a/ui/pnpm-lock.yaml b/docs/ui/pnpm-lock.yaml similarity index 100% rename from ui/pnpm-lock.yaml rename to docs/ui/pnpm-lock.yaml diff --git a/ui/postcss.config.mjs b/docs/ui/postcss.config.mjs similarity index 100% rename from ui/postcss.config.mjs rename to docs/ui/postcss.config.mjs diff --git a/ui/public/placeholder-logo.png b/docs/ui/public/placeholder-logo.png similarity index 100% rename from ui/public/placeholder-logo.png rename to docs/ui/public/placeholder-logo.png diff --git a/ui/public/placeholder-logo.svg b/docs/ui/public/placeholder-logo.svg similarity index 100% rename from ui/public/placeholder-logo.svg rename to docs/ui/public/placeholder-logo.svg diff --git a/ui/public/placeholder-user.jpg b/docs/ui/public/placeholder-user.jpg similarity index 100% rename from ui/public/placeholder-user.jpg rename to docs/ui/public/placeholder-user.jpg diff --git a/ui/public/placeholder.jpg b/docs/ui/public/placeholder.jpg similarity index 100% rename from ui/public/placeholder.jpg rename to docs/ui/public/placeholder.jpg diff --git a/ui/public/placeholder.svg b/docs/ui/public/placeholder.svg similarity index 100% rename from ui/public/placeholder.svg rename to docs/ui/public/placeholder.svg diff --git a/ui/styles/globals.css b/docs/ui/styles/globals.css similarity index 100% rename from ui/styles/globals.css rename to docs/ui/styles/globals.css diff --git a/ui/tailwind.config.ts b/docs/ui/tailwind.config.ts similarity index 100% rename from ui/tailwind.config.ts rename to docs/ui/tailwind.config.ts diff --git a/ui/tsconfig.json b/docs/ui/tsconfig.json similarity index 100% rename from ui/tsconfig.json rename to docs/ui/tsconfig.json diff --git a/docs/v1/README.md b/docs/v1/README.md deleted file mode 100644 index 00b5ff1..0000000 --- a/docs/v1/README.md +++ /dev/null @@ -1,289 +0,0 @@ -# 摄影作品集网站 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 deleted file mode 100644 index 822b623..0000000 --- a/docs/v1/admin/管理后台开发文档.md +++ /dev/null @@ -1,1546 +0,0 @@ -# 摄影作品集网站 - 管理后台开发文档 - -## 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 日志管理模块 (Log Management) - -#### 2.6.1 日志查看页面 -``` -┌─────────────────────────────────────────────────────────────┐ -│ 日志管理 Log Management │ -├─────────────────────────────────────────────────────────────┤ -│ 📊 统计卡片 │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ 总日志 │ │ 错误数 │ │ 警告数 │ │ 信息数 │ │ -│ │ 12,345 │ │ 23 │ │ 156 │ │ 11,234 │ │ -│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ -│ │ -│ 🔍 过滤控件 │ -│ [日志级别▼] [搜索关键词...] [Trace ID...] [100条▼] [刷新] │ -│ │ -│ 📋 日志列表 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 时间 级别 Trace ID 消息 │ │ -│ │ 2024-01-15 10:30 INFO trace-abc123 Photo created │ │ -│ │ 2024-01-15 10:31 WARN trace-def456 Cache miss │ │ -│ │ 2024-01-15 10:32 ERROR trace-abc123 Process failed │ │ -│ │ 2024-01-15 10:33 INFO trace-ghi789 User login │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ 📄 分页: [← 上一页] [1] [2] [3] [下一页 →] │ -│ ⏰ [自动刷新] 🗑️ [清空显示] │ -└─────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.2 日志详情展示 -``` -┌─────────────────────────────────────────────────────────────┐ -│ 日志详情 Log Details │ -├─────────────────────────────────────────────────────────────┤ -│ ← 返回列表 │ -│ │ -│ 📋 基本信息 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 时间: 2024-01-15 10:32:45 │ │ -│ │ 级别: ERROR │ │ -│ │ 消息: Failed to process image │ │ -│ │ Trace ID: trace-abc123 [点击查看相关日志] │ │ -│ │ 用户: admin (user-123) │ │ -│ │ 操作: image_process │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ 🔍 相关日志 (同一Trace ID) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 10:30:00 INFO Photo upload started │ │ -│ │ 10:31:30 INFO File validation passed │ │ -│ │ 10:32:00 WARN Large file detected │ │ -│ │ 10:32:45 ERROR Failed to process image │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ 📊 统计信息 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 该Trace ID共有 4 条日志 │ │ -│ │ 错误: 1 | 警告: 1 | 信息: 2 │ │ -│ │ 耗时: 2分45秒 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.3 日志管理功能 -- **实时查看**: 支持自动刷新,实时监控系统状态 -- **智能过滤**: 按级别、关键词、Trace ID、时间范围过滤 -- **链路追踪**: 点击Trace ID查看完整请求链路 -- **统计分析**: 显示各级别日志数量统计 -- **搜索高亮**: 搜索关键词在日志中高亮显示 -- **权限控制**: 仅管理员可访问日志管理功能 - -### 2.7 系统设置模块 (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 # 系统设置 -│ │ │ ├── logs.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 # 上传服务 -│ │ ├── logs_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 -│ │ ├── logs/ # 日志管理组件 -│ │ │ ├── log-viewer.tsx -│ │ │ ├── log-filter.tsx -│ │ │ ├── log-detail.tsx -│ │ │ └── log-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 -│ │ ├── logs/ -│ │ │ ├── index.tsx -│ │ │ └── detail.tsx -│ │ └── settings/ -│ │ └── index.tsx -│ ├── hooks/ # 自定义Hooks -│ │ ├── useAuth.ts -│ │ ├── usePhotos.ts -│ │ ├── useCategories.ts -│ │ ├── useLogs.ts -│ │ └── useUpload.ts -│ ├── services/ # API服务 -│ │ ├── api.ts -│ │ ├── auth.ts -│ │ ├── photo.ts -│ │ ├── category.ts -│ │ ├── logs.ts -│ │ └── upload.ts -│ ├── store/ # 状态管理 -│ │ ├── auth.ts -│ │ ├── photo.ts -│ │ ├── logs.ts -│ │ └── ui.ts -│ ├── utils/ # 工具函数 -│ │ ├── format.ts -│ │ ├── validation.ts -│ │ └── constants.ts -│ └── types/ # 类型定义 -│ ├── api.ts -│ ├── photo.ts -│ ├── logs.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 deleted file mode 100644 index 01f3bfa..0000000 --- a/docs/v1/api/API接口设计文档.md +++ /dev/null @@ -1,1569 +0,0 @@ -# 摄影作品集网站 - 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 deleted file mode 100644 index 8facec1..0000000 --- a/docs/v1/backend/Golang项目架构文档.md +++ /dev/null @@ -1,1940 +0,0 @@ -# 摄影作品集网站 - 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 // 日志轮转 -opentracing/opentracing-go // 链路追踪 -jaegertracing/jaeger-client-go // Jaeger客户端 - -// 验证和工具 -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/ # 日志系统 -│ ├── tracing/ # 链路追踪 -│ ├── 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" - "github.com/opentracing/opentracing-go" -) - -// 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) { - // 创建span用于链路追踪 - span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.GetPhotos") - defer span.Finish() - - // 获取trace ID - traceID := s.getTraceID(span) - logger := s.logger.WithField("trace_id", traceID) - - // 参数验证 - if err := req.Validate(); err != nil { - span.SetTag("error", true) - span.LogKV("event", "validation_error", "error", err.Error()) - logger.WithError(err).Error("Invalid request parameters") - 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 { - span.SetTag("error", true) - span.LogKV("event", "repository_error", "error", err.Error()) - 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) - } - - // 记录成功日志 - span.SetTag("photos_count", len(photos)) - span.SetTag("total_count", total) - logger.WithFields(logrus.Fields{ - "photos_count": len(photos), - "total_count": total, - "page": req.Page, - "limit": req.Limit, - }).Info("Photos retrieved successfully") - - return response, nil -} - -// getTraceID 获取trace ID -func (s *photoServiceImpl) getTraceID(span opentracing.Span) string { - if span == nil { - return "" - } - - // 从span context获取trace ID - spanContext := span.Context() - if jaegerSpanContext, ok := spanContext.(interface{ TraceID() string }); ok { - return jaegerSpanContext.TraceID() - } - - // 如果无法获取trace ID,生成一个UUID作为fallback - return uuid.New().String() -} - -// CreatePhoto 创建照片 -func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error) { - // 创建span用于链路追踪 - span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.CreatePhoto") - defer span.Finish() - - // 获取trace ID - traceID := s.getTraceID(span) - logger := s.logger.WithField("trace_id", traceID) - - // 参数验证 - if err := req.Validate(); err != nil { - span.SetTag("error", true) - span.LogKV("event", "validation_error", "error", err.Error()) - logger.WithError(err).Error("Invalid request parameters") - 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 { - span.SetTag("error", true) - span.LogKV("event", "database_error", "error", err.Error()) - 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:*") - - // 记录成功日志 - span.SetTag("photo_id", photo.ID.Value()) - span.SetTag("photo_title", photo.Title) - logger.WithFields(logrus.Fields{ - "photo_id": photo.ID.Value(), - "photo_title": photo.Title, - "photo_slug": photo.Slug, - }).Info("Photo created successfully") - - 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"` - Tracing TracingConfig `mapstructure:"tracing"` - 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"` -} - -// TracingConfig 链路追踪配置 -type TracingConfig struct { - Enabled bool `mapstructure:"enabled"` - ServiceName string `mapstructure:"service_name"` - Jaeger struct { - Endpoint string `mapstructure:"endpoint"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - } `mapstructure:"jaeger"` - SamplingRate float64 `mapstructure:"sampling_rate"` -} - -// 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("tracing.enabled", true) - viper.SetDefault("tracing.service_name", "photography-backend") - viper.SetDefault("tracing.jaeger.endpoint", "http://localhost:14268/api/traces") - viper.SetDefault("tracing.sampling_rate", 1.0) - - // 上传默认配置 - 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 - - # Jaeger链路追踪 - jaeger: - image: jaegertracing/all-in-one:latest - ports: - - "16686:16686" - - "14268:14268" - environment: - - COLLECTOR_OTLP_ENABLED=true - 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/backend/运维监控方案.md b/docs/v1/backend/运维监控方案.md deleted file mode 100644 index b2991db..0000000 --- a/docs/v1/backend/运维监控方案.md +++ /dev/null @@ -1,1320 +0,0 @@ -# 摄影作品集网站 - 日志管理方案 - -## 🎯 方案概述 - -这是一个**简单实用的日志管理方案**,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的Web界面。 - -### 设计原则 -- **集成化**: 日志查看功能集成到管理后台 -- **用户友好**: 提供美观易用的Web界面 -- **问题导向**: 专注于快速定位和修复问题 -- **低维护成本**: 几乎零维护的方案 -- **渐进式**: 后续可以根据需要扩展 - -## 📋 核心组件 - -### 日志管理架构 -``` -┌─────────────────────────────────────────────────────────────┐ -│ 后端应用日志 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ 错误日志 │ │ 访问日志 │ │ 业务日志 │ │ -│ │ (JSON格式) │ │ (HTTP日志) │ │ (操作日志) │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ 管理后台日志模块 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ 日志查看器 │ │ 实时监控 │ │ 统计分析 │ │ -│ │ (Web界面) │ │ (自动刷新) │ │ (图表展示) │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ API接口层 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ 日志查询API │ │ 统计API │ │ 搜索API │ │ -│ │(/api/logs) │ │(/api/stats) │ │(/api/search)│ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 🔧 技术选择 - -### 日志方案 -- **日志存储**: 本地文件 (JSON格式) -- **日志轮转**: lumberjack.v2 -- **日志查看**: 管理后台Web界面 -- **Trace ID**: 集成OpenTracing (已在架构文档中添加) -- **权限控制**: 基于管理后台的用户权限 - -### 集成方式 -- **前端**: 管理后台的日志管理模块 -- **后端**: Gin路由提供日志查询API -- **认证**: 复用管理后台的登录认证 -- **权限**: 仅管理员可访问日志功能 - -## 📝 日志配置 - -### 1. 应用日志配置 - -#### 日志配置文件 (config.yaml) -```yaml -# config/config.yaml -logger: - level: "info" - format: "json" - output: "file" - filename: "/app/logs/app.log" - max_size: 100 # MB - max_age: 7 # days - compress: true - -# 可选:如果需要链路追踪 -tracing: - enabled: true - service_name: "photography-backend" - jaeger: - endpoint: "http://localhost:14268/api/traces" - sampling_rate: 1.0 -``` - -### 2. 日志格式标准化 - -#### 统一的日志格式 -```json -{ - "timestamp": "2024-01-15T10:30:00Z", - "level": "info", - "message": "Photo created successfully", - "service": "photography-backend", - "trace_id": "abc123def456", - "request_id": "req-789", - "user_id": "user-123", - "operation": "create_photo", - "photo_id": 1001, - "duration": 0.5, - "error": null -} -``` - -### 3. 日志分类 - -#### 三种核心日志类型 -```bash -# 日志目录结构 -logs/ -├── app.log # 应用日志 (所有级别) -├── error.log # 错误日志 (ERROR级别) -└── access.log # HTTP访问日志 -``` - -#### 日志级别使用 -```go -// 日志级别使用指南 -logger.Info("正常业务操作") // 记录重要的业务操作 -logger.Warn("需要关注的情况") // 记录警告信息 -logger.Error("错误情况") // 记录错误信息 -logger.Debug("调试信息") // 开发调试用 -``` - -## 🔍 问题诊断工具 - -### 1. 命令行工具 - -#### 快速查看错误 -```bash -#!/bin/bash -# scripts/check-errors.sh - -echo "📋 最近的错误日志 (最近1小时):" -tail -f /app/logs/app.log | grep -i error | jq -r '.timestamp + " " + .message' - -echo "📊 错误统计:" -grep -i error /app/logs/app.log | jq -r '.message' | sort | uniq -c | sort -nr | head -10 -``` - -#### 按trace ID查找日志 -```bash -#!/bin/bash -# scripts/find-by-trace.sh - -TRACE_ID=$1 -if [ -z "$TRACE_ID" ]; then - echo "用法: ./find-by-trace.sh " - exit 1 -fi - -echo "🔍 查找 trace ID: $TRACE_ID" -grep "$TRACE_ID" /app/logs/app.log | jq -r '.timestamp + " [" + .level + "] " + .message' -``` - -#### 实时监控错误 -```bash -#!/bin/bash -# scripts/monitor-errors.sh - -echo "🚨 实时错误监控 (按Ctrl+C退出):" -tail -f /app/logs/app.log | grep --line-buffered -i error | while read line; do - echo -e "\033[31m[ERROR]\033[0m $(echo "$line" | jq -r '.timestamp + " " + .message')" -done -``` - -### 2. 管理后台日志模块 - -#### 日志查看组件 (React/Vue) -```html - - - - 📋 日志查看器 - - - - -
-
-

📋 日志查看器

-
- - - - - - - - - - -
-
- -
📊 正在加载统计...
- -
-
📱 正在加载日志...
-
-
- - - - -``` - -#### 管理后台日志API接口 -集成到现有的管理后台后端: - -```go -// internal/api/handlers/admin/logs_handler.go -package admin - -import ( - "bufio" - "encoding/json" - "net/http" - "os" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "photography-backend/pkg/middleware" -) - -// LogEntry 日志条目 -type LogEntry struct { - Timestamp string `json:"timestamp"` - Level string `json:"level"` - Message string `json:"message"` - TraceID string `json:"trace_id,omitempty"` - UserID string `json:"user_id,omitempty"` - Operation string `json:"operation,omitempty"` -} - -// LogHandler 日志处理器 -type LogHandler struct { - logFile string -} - -// NewLogHandler 创建日志处理器 -func NewLogHandler(logFile string) *LogHandler { - return &LogHandler{logFile: logFile} -} - -// GetLogs 获取日志列表 -// @Summary 获取系统日志 -// @Description 获取系统日志列表,支持过滤和搜索 -// @Tags 日志管理 -// @Accept json -// @Produce json -// @Param level query string false "日志级别" Enums(error,warn,info,debug) -// @Param search query string false "搜索关键词" -// @Param trace_id query string false "Trace ID" -// @Param lines query int false "返回行数" default(100) -// @Success 200 {object} LogListResponse -// @Failure 401 {object} ErrorResponse -// @Failure 403 {object} ErrorResponse -// @Failure 500 {object} ErrorResponse -// @Router /admin/api/logs [get] -func (h *LogHandler) GetLogs(c *gin.Context) { - // 获取参数 - levelFilter := c.Query("level") - searchFilter := c.Query("search") - traceID := c.Query("trace_id") - lines := 100 - if l := c.Query("lines"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 { - lines = parsed - } - } - - // 读取日志文件 - logs, err := h.readLogs(lines, levelFilter, searchFilter, traceID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "读取日志失败", - "details": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "code": 0, - "message": "success", - "data": gin.H{ - "logs": logs, - "total": len(logs), - }, - }) -} - -// GetLogStats 获取日志统计 -// @Summary 获取日志统计信息 -// @Description 获取各级别日志的统计数据 -// @Tags 日志管理 -// @Accept json -// @Produce json -// @Success 200 {object} LogStatsResponse -// @Failure 401 {object} ErrorResponse -// @Failure 403 {object} ErrorResponse -// @Failure 500 {object} ErrorResponse -// @Router /admin/api/logs/stats [get] -func (h *LogHandler) GetLogStats(c *gin.Context) { - stats := map[string]int{ - "total": 0, - "error": 0, - "warn": 0, - "info": 0, - "debug": 0, - } - - file, err := os.Open(h.logFile) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "读取日志文件失败", - "details": err.Error(), - }) - return - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if line == "" { - continue - } - - stats["total"]++ - - // 解析日志级别 - var entry LogEntry - if err := json.Unmarshal([]byte(line), &entry); err == nil { - if count, exists := stats[entry.Level]; exists { - stats[entry.Level] = count + 1 - } - } - } - - c.JSON(http.StatusOK, gin.H{ - "code": 0, - "message": "success", - "data": stats, - }) -} - -// readLogs 读取日志文件 -func (h *LogHandler) readLogs(maxLines int, levelFilter, searchFilter, traceFilter string) ([]LogEntry, error) { - file, err := os.Open(h.logFile) - if err != nil { - return nil, err - } - defer file.Close() - - var lines []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - - // 从后往前读取最新的日志 - start := len(lines) - maxLines*2 // 预读更多行用于过滤 - if start < 0 { - start = 0 - } - - var logs []LogEntry - for i := start; i < len(lines) && len(logs) < maxLines; i++ { - line := lines[i] - if line == "" { - continue - } - - var entry LogEntry - if err := json.Unmarshal([]byte(line), &entry); err != nil { - continue // 跳过无法解析的行 - } - - // 应用过滤条件 - if levelFilter != "" && entry.Level != levelFilter { - continue - } - if searchFilter != "" { - searchLower := strings.ToLower(searchFilter) - if !strings.Contains(strings.ToLower(entry.Message), searchLower) && - !strings.Contains(strings.ToLower(entry.TraceID), searchLower) && - !strings.Contains(strings.ToLower(entry.Operation), searchLower) { - continue - } - } - if traceFilter != "" && !strings.Contains(entry.TraceID, traceFilter) { - continue - } - - logs = append(logs, entry) - } - - // 反转数组,让最新的日志在前面 - for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 { - logs[i], logs[j] = logs[j], logs[i] - } - - return logs, nil -} - -// RegisterLogRoutes 注册日志相关路由 -func RegisterLogRoutes(r *gin.RouterGroup, logFile string) { - logHandler := NewLogHandler(logFile) - - // 需要管理员权限的路由组 - adminGroup := r.Group("") - adminGroup.Use(middleware.RequireAuth()) // 需要登录 - adminGroup.Use(middleware.RequireAdmin()) // 需要管理员权限 - - { - adminGroup.GET("/logs", logHandler.GetLogs) - adminGroup.GET("/logs/stats", logHandler.GetLogStats) - } -} -``` - -#### 管理后台前端集成 -在管理后台中添加日志管理页面: - -```javascript -// admin/src/pages/Logs/LogViewer.jsx -import React, { useState, useEffect } from 'react'; -import { - Card, - Table, - Select, - Input, - Button, - Tag, - Space, - Statistic, - Row, - Col, - message -} from 'antd'; -import { - ReloadOutlined, - SearchOutlined, - ExclamationCircleOutlined, - WarningOutlined, - InfoCircleOutlined, - BugOutlined -} from '@ant-design/icons'; -import { adminApi } from '../../services/api'; - -const { Option } = Select; -const { Search } = Input; - -const LogViewer = () => { - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(false); - const [filters, setFilters] = useState({ - level: '', - search: '', - trace_id: '', - lines: 100, - }); - const [stats, setStats] = useState({}); - const [autoRefresh, setAutoRefresh] = useState(false); - - // 日志级别配置 - const levelConfig = { - error: { color: 'red', icon: }, - warn: { color: 'orange', icon: }, - info: { color: 'blue', icon: }, - debug: { color: 'default', icon: }, - }; - - // 获取日志数据 - const fetchLogs = async () => { - setLoading(true); - try { - const response = await adminApi.get('/logs', { params: filters }); - setLogs(response.data.data.logs || []); - } catch (error) { - message.error('获取日志失败'); - } finally { - setLoading(false); - } - }; - - // 获取统计数据 - const fetchStats = async () => { - try { - const response = await adminApi.get('/logs/stats'); - setStats(response.data.data || {}); - } catch (error) { - console.error('获取统计数据失败', error); - } - }; - - useEffect(() => { - fetchLogs(); - fetchStats(); - }, [filters]); - - // 自动刷新 - useEffect(() => { - let interval; - if (autoRefresh) { - interval = setInterval(() => { - fetchLogs(); - fetchStats(); - }, 5000); - } - return () => interval && clearInterval(interval); - }, [autoRefresh, filters]); - - // 表格列配置 - const columns = [ - { - title: '时间', - dataIndex: 'timestamp', - key: 'timestamp', - width: 180, - render: (timestamp) => new Date(timestamp).toLocaleString(), - }, - { - title: '级别', - dataIndex: 'level', - key: 'level', - width: 80, - render: (level) => { - const config = levelConfig[level] || levelConfig.info; - return ( - - {level.toUpperCase()} - - ); - }, - }, - { - title: 'Trace ID', - dataIndex: 'trace_id', - key: 'trace_id', - width: 120, - render: (traceId) => traceId ? ( - - ) : '-', - }, - { - title: '消息', - dataIndex: 'message', - key: 'message', - ellipsis: true, - render: (message, record) => { - // 高亮搜索关键词 - if (filters.search && message.toLowerCase().includes(filters.search.toLowerCase())) { - const regex = new RegExp(`(${filters.search})`, 'gi'); - const parts = message.split(regex); - return parts.map((part, index) => - part.toLowerCase() === filters.search.toLowerCase() ? - {part} : part - ); - } - return message; - }, - }, - { - title: '操作', - dataIndex: 'operation', - key: 'operation', - width: 120, - render: (operation) => operation || '-', - }, - ]; - - return ( -
- {/* 统计卡片 */} - - - - - - - - - } - /> - - - - - } - /> - - - - - } - /> - - - - - {/* 过滤控件 */} - - - - - setFilters(prev => ({ ...prev, search: e.target.value }))} - prefix={} - /> - - setFilters(prev => ({ ...prev, trace_id: e.target.value }))} - /> - - - - - - - - - - {/* 日志表格 */} - - `${record.timestamp}-${index}`} - pagination={{ - showSizeChanger: false, - showQuickJumper: true, - showTotal: (total) => `共 ${total} 条日志`, - }} - scroll={{ x: 800 }} - /> - - - ); -}; - -export default LogViewer; -``` - -## 🚀 集成到管理后台 - -### 1. 后端集成步骤 - -#### 在main.go中注册日志路由 -```go -// cmd/server/main.go -func main() { - // ... 其他初始化代码 - - // 创建Gin引擎 - r := gin.Default() - - // 注册API路由 - apiGroup := r.Group("/api") - { - // 其他API路由... - } - - // 注册管理后台路由 - adminGroup := r.Group("/admin/api") - { - // 注册日志管理路由 - admin.RegisterLogRoutes(adminGroup, "logs/app.log") - - // 其他管理后台路由... - } - - r.Run(":8080") -} -``` - -#### 添加权限中间件 -```go -// pkg/middleware/auth.go -package middleware - -import ( - "net/http" - "github.com/gin-gonic/gin" -) - -// RequireAuth 需要登录认证 -func RequireAuth() gin.HandlerFunc { - return func(c *gin.Context) { - // 检查JWT token或session - token := c.GetHeader("Authorization") - if token == "" { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "未授权访问", - }) - c.Abort() - return - } - - // 验证token并获取用户信息 - // ... token验证逻辑 - - c.Next() - } -} - -// RequireAdmin 需要管理员权限 -func RequireAdmin() gin.HandlerFunc { - return func(c *gin.Context) { - // 检查用户是否是管理员 - userRole := c.GetString("user_role") - if userRole != "admin" { - c.JSON(http.StatusForbidden, gin.H{ - "error": "需要管理员权限", - }) - c.Abort() - return - } - - c.Next() - } -} -``` - -### 2. 前端集成步骤 - -#### 在管理后台路由中添加日志管理 -```javascript -// admin/src/router/index.js -import LogViewer from '../pages/Logs/LogViewer.jsx'; - -const routes = [ - // 其他路由... - { - path: '/admin', - component: AdminLayout, - children: [ - { - path: 'logs', - component: LogViewer, - meta: { - title: '日志管理', - requireAuth: true, - requireAdmin: true - } - } - // 其他子路由... - ] - } -]; -``` - -#### 在管理后台菜单中添加日志入口 -```javascript -// admin/src/layout/AdminLayout.jsx -const menuItems = [ - { - key: 'dashboard', - icon: , - label: '仪表盘', - path: '/admin/dashboard' - }, - { - key: 'photos', - icon: , - label: '照片管理', - path: '/admin/photos' - }, - { - key: 'logs', - icon: , - label: '日志管理', - path: '/admin/logs' - }, - // 其他菜单项... -]; -``` - -### 3. 配置更新 - -#### 更新应用配置文件 -```yaml -# config/config.yaml -app: - name: "photography-backend" - version: "1.0.0" - environment: "production" - -logger: - level: "info" - format: "json" - output: "file" - filename: "logs/app.log" - max_size: 100 - max_age: 7 - compress: true - -tracing: - enabled: true - service_name: "photography-backend" - sampling_rate: 1.0 - -admin: - log_access: true # 启用日志访问功能 - log_retention_days: 30 # 日志保留天数 -``` - -### 4. 一键部署脚本 - -#### 部署管理后台日志功能 -```bash -#!/bin/bash -# deploy-admin-logs.sh - -echo "🚀 部署管理后台日志功能..." - -# 创建必要目录 -mkdir -p logs -mkdir -p admin/src/pages/Logs - -# 确保日志文件存在 -touch logs/app.log -chmod 664 logs/app.log - -# 创建日志轮转配置 -cat > /etc/logrotate.d/photography-backend << 'EOF' -/path/to/photography/logs/*.log { - daily - rotate 30 - compress - delaycompress - missingok - notifempty - create 664 app app - postrotate - systemctl reload photography-backend - endscript -} -EOF - -# 设置权限 -chown -R app:app logs/ -chmod 755 logs/ - -echo "✅ 管理后台日志功能部署完成!" -echo "" -echo "📋 访问方式:" -echo " 1. 登录管理后台: http://localhost:8080/admin" -echo " 2. 进入日志管理页面" -echo " 3. 使用管理员账号访问" -echo "" -echo "🔧 API地址:" -echo " 日志列表: GET /admin/api/logs" -echo " 日志统计: GET /admin/api/logs/stats" -``` - -## 🔧 故障排查流程 - -### 1. 问题诊断步骤 -```bash -# 快速诊断脚本 -#!/bin/bash -# scripts/quick-diagnosis.sh - -echo "🔍 快速问题诊断" - -# 1. 检查服务状态 -echo "1. 检查服务状态..." -docker-compose ps - -# 2. 检查最近错误 -echo "2. 最近10条错误日志..." -tail -n 1000 logs/app.log | grep -i error | tail -10 | jq -r '.timestamp + " " + .message' - -# 3. 检查磁盘空间 -echo "3. 检查磁盘空间..." -df -h - -# 4. 检查日志文件大小 -echo "4. 检查日志文件大小..." -ls -lh logs/ - -# 5. 检查内存使用 -echo "5. 检查内存使用..." -docker stats --no-stream photography-backend - -echo "✅ 诊断完成" -``` - -### 2. 常见问题解决 - -#### 问题1:无法找到错误原因 -```bash -# 1. 获取完整的错误上下文 -grep -B 5 -A 5 "error_message" logs/app.log - -# 2. 按时间范围查找 -grep "2024-01-15T10:" logs/app.log | grep -i error -``` - -#### 问题2:需要追踪特定用户的操作 -```bash -# 按用户ID查找 -grep "user-123" logs/app.log | jq -r '.timestamp + " " + .message' - -# 按操作类型查找 -grep "create_photo" logs/app.log | jq -r '.timestamp + " " + .message' -``` - -#### 问题3:日志文件太大 -```bash -# 手动轮转日志 -mv logs/app.log logs/app.log.old -sudo systemctl restart photography-backend - -# 或者使用logrotate -sudo logrotate -f /etc/logrotate.d/photography-backend -``` - -## 🎯 总结 - -这个集成到管理后台的日志方案提供了: - -### ✨ 核心特性 -- **🏢 集成化管理** - 完全集成到管理后台,统一的用户体验 -- **🔐 权限控制** - 基于管理后台的认证和授权体系 -- **📊 专业界面** - 使用Ant Design组件,美观且专业 -- **🔍 强大搜索** - 支持关键词、级别、Trace ID多维度过滤 -- **⏰ 实时监控** - 自动刷新功能,实时观察系统状态 -- **📈 统计分析** - 直观的统计卡片,快速了解系统健康状况 - -### 🚀 部署简单 -1. 运行部署脚本: `./deploy-admin-logs.sh` -2. 访问管理后台: `http://localhost:8080/admin` -3. 登录管理员账号,进入日志管理页面 - -### 💡 使用场景 -- **错误排查**: 管理员快速定位和分析错误日志 -- **性能监控**: 通过Trace ID追踪完整请求链路 -- **运营监控**: 实时观察系统运行状态 -- **历史分析**: 搜索和分析历史日志数据 -- **团队协作**: 多个管理员可以同时查看和分析日志 - -### 🔧 技术优势 -- **集成性**: 完全集成到现有管理后台 -- **安全性**: 基于管理后台的权限控制 -- **专业性**: 使用成熟的UI组件库 -- **可扩展**: 易于添加新的日志分析功能 -- **用户友好**: 直观的界面,无需学习成本 - -### 🔐 权限控制 -- **认证**: 需要管理后台登录 -- **授权**: 仅管理员可访问日志功能 -- **安全**: API接口有完整的权限验证 - -### 📋 API接口 -- `GET /admin/api/logs` - 获取日志列表 -- `GET /admin/api/logs/stats` - 获取日志统计 - -这就是最适合生产环境的日志管理方案 - **专业、安全、易用**! \ No newline at end of file diff --git a/docs/v1/database/数据库设计文档.md b/docs/v1/database/数据库设计文档.md deleted file mode 100644 index e2c90f7..0000000 --- a/docs/v1/database/数据库设计文档.md +++ /dev/null @@ -1,1699 +0,0 @@ -# 摄影作品集网站 - 数据库设计文档 - -## 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 deleted file mode 100644 index e5ebc83..0000000 --- a/docs/v2/README.md +++ /dev/null @@ -1,440 +0,0 @@ -# 摄影作品集网站 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