diff --git a/CLAUDE.md b/CLAUDE.md index 7719b16..dabfee0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,314 +1,62 @@ -# Photography Portfolio Project - CLAUDE.md +# Photography Portfolio - 项目总览 -此文件为 Claude Code 在此项目中工作时提供指导。本项目采用模块化结构,每个模块有独立的 CLAUDE.md 文件,所有给我看的提示都要用中文。 +> 📍 这是一个模块化摄影作品集项目,所有模块都有独立的CLAUDE.md文件 -## 🎯 项目概览 - -这是一个现代化的摄影作品集网站项目,使用 Next.js 15 + React 19 构建,支持静态部署。 - -### 主要特性 -- 📸 响应式照片画廊和时间轴视图 -- 🌙 深色/浅色主题切换 -- 📱 移动端优化 -- ⚡ 静态生成,极快加载速度 -- 🔒 自动 HTTPS (Let's Encrypt) -- 🚀 CI/CD 自动部署 - -### 技术栈 -- **前端**: Next.js 15, React 19, TypeScript, Tailwind CSS -- **管理后台**: React + TypeScript + Vite (使用 Bun) -- **组件**: Radix UI + shadcn/ui -- **部署**: Caddy + 静态文件 -- **CI/CD**: Gitea Actions - -## 📁 项目结构 +## 🎯 项目结构 ``` photography/ -├── CLAUDE.md # 🔍 当前文件 - 项目总览 -├── lint-staged.config.js # Pre-commit 配置 -├── frontend/ # 🎨 前端模块 -│ ├── CLAUDE.md # 前端开发指导 -│ ├── app/, components/, lib/ # Next.js 应用 -│ ├── package.json, Makefile # 构建工具 -│ └── out/ # 静态导出目录 -├── docs/ # 📚 文档模块 -│ └── deployment/ # 🚀 部署模块 -│ ├── CLAUDE.md # 部署配置指导 -│ ├── Caddyfile # Web 服务器配置 -│ ├── fix-caddy-permissions.sh # 权限修复脚本 -│ └── caddy-setup.md # 部署文档 -├── .gitea/workflows/ # ⚙️ CI/CD 模块 -│ ├── CLAUDE.md # CI/CD 配置指导 -│ └── deploy-frontend.yml # 自动部署工作流 -├── admin/ # 📋 管理后台模块 -│ └── CLAUDE.md # 管理后台开发指导 -├── backend/ # 🔧 后端 API 模块 -│ └── CLAUDE.md # 后端开发指导 -├── ui/ # 🎨 UI 备份模块 -│ └── CLAUDE.md # UI 备份模块管理 -└── scripts/ # 🛠️ 工具脚本 - └── README.md # 脚本说明 +├── frontend/ # 🎨 前端Next.js应用 +├── admin/ # 📋 管理后台 +├── backend/ # 🔧 Go后端API +├── docs/deployment/ # 🚀 部署配置 +├── .gitea/workflows/ # ⚙️ CI/CD流程 +├── ui/ # 🎨 UI备份 +└── scripts/ # 🛠️ 工具脚本 ``` -## 🎯 模块化工作指南 - -### 根据工作内容选择模块 - -#### 🎨 前端开发 -```bash -# 切换到前端模块 -cd frontend/ -# 参考 frontend/CLAUDE.md -``` -**适用场景**: UI 组件开发、样式调整、前端功能实现、React/Next.js 相关工作 - -#### 🚀 部署配置 -```bash -# 切换到部署模块 -cd docs/deployment/ -# 参考 docs/deployment/CLAUDE.md -``` -**适用场景**: 服务器配置、Caddy 设置、权限问题、域名配置、SSL 证书 - -#### ⚙️ CI/CD 流程 -```bash -# 切换到 CI/CD 模块 -cd .gitea/workflows/ -# 参考 .gitea/workflows/CLAUDE.md -``` -**适用场景**: 自动部署、构建流程、环境配置、工作流优化 - -#### 📋 管理后台开发 -```bash -# 切换到管理后台模块 -cd admin/ -# 参考 admin/CLAUDE.md -``` -**适用场景**: 管理界面开发、用户管理、内容管理、权限控制 - -#### 🔧 后端 API 开发 -```bash -# 切换到后端模块 -cd backend/ -# 参考 backend/CLAUDE.md -``` -**适用场景**: API 接口开发、数据库设计、认证服务、文件存储 - -#### 🎨 UI 备份和实验 -```bash -# 切换到 UI 备份模块 -cd ui/ -# 参考 ui/CLAUDE.md -``` -**适用场景**: 组件备份、A/B 测试、实验性功能、版本对比 - -#### 📚 文档和架构 -```bash -# 在根目录工作 -# 参考当前 CLAUDE.md -``` -**适用场景**: 项目架构、文档更新、模块协调、整体规划 - ## 🚀 快速开始 -### 开发环境设置 +### 选择工作模块 ```bash -# 1. 克隆项目 -git clone -cd photography +# 进入对应模块目录后,查看模块CLAUDE.md -# 2. 前端开发 -cd frontend/ -make setup # 初始化环境 -make quick # 安装依赖 + 启动开发服务器 -``` - -### 部署设置(首次) -```bash -# 1. 配置服务器 -scp docs/deployment/Caddyfile user@server:/etc/caddy/ -scp docs/deployment/fix-caddy-permissions.sh user@server:~/ -ssh user@server './fix-caddy-permissions.sh && sudo systemctl reload caddy' - -# 2. 推送代码自动部署 -git push origin main -``` - -## 🔧 全局配置 - -### Git Hooks (Pre-commit) -项目配置了 pre-commit hooks,会在提交前自动运行: -- ESLint 代码检查 -- TypeScript 类型检查 -- Prettier 代码格式化 - -配置文件:`lint-staged.config.js` - -### 环境要求 -- **Node.js**: 18+ -- **Bun**: 最新版本 (前端和管理后台包管理器) -- **Git**: 2.0+ - -## 🌐 访问地址 - -### 生产环境 -- **网站**: https://photography.iriver.top -- **部署**: 自动部署到 `/home/gitea/www/photography/` - -### 开发环境 -- **本地**: http://localhost:3000 -- **Mock API**: http://localhost:3001 - -## 📋 常用命令 - -### 项目级命令 -```bash # 前端开发 +cd frontend && cat CLAUDE.md + +# 后端开发 +cd backend && cat CLAUDE.md + +# 部署配置 +cd docs/deployment && cat CLAUDE.md +``` + +### 常用命令 +```bash +# 前端 cd frontend && make dev -# 管理后台开发 (使用 Bun) -cd admin && make quick # 快速启动 (推荐) -cd admin && make dev # 开发服务器 +# 后端 +cd backend && make run -# 代码检查 -cd frontend && make lint -cd admin && make lint - -# 构建项目 -cd frontend && make build -cd admin && make build - -# 部署准备 -cd frontend && make deploy-prep -cd admin && make deploy-prep +# 部署 +cd docs/deployment && ./deploy.sh ``` -### Git 工作流 -```bash -# 开发流程 -git checkout -b feature/new-feature -# 开发... -git add . -git commit -m "feat: 新功能描述" # pre-commit hooks 自动运行 -git push origin feature/new-feature +## 📋 模块指南 -# 部署流程 -git checkout main -git merge feature/new-feature -git push origin main # 触发自动部署 -``` +| 模块 | 用途 | 入口文件 | +|---|---|---| +| `frontend/` | Next.js前端 | `frontend/CLAUDE.md` | +| `admin/` | 管理后台 | `admin/CLAUDE.md` | +| `backend/` | Go API服务 | `backend/CLAUDE.md` | +| `docs/deployment/` | 服务器部署 | `docs/deployment/CLAUDE.md` | -## 🔍 问题排查 +## 🔗 项目地址 +- **网站**: https://photography.iriver.top +- **仓库**: Gitea自动部署 -### 模块特定问题 -- **前端问题**: 查看 `frontend/CLAUDE.md` -- **部署问题**: 查看 `docs/deployment/CLAUDE.md` -- **CI/CD 问题**: 查看 `.gitea/workflows/CLAUDE.md` -- **管理后台问题**: 查看 `admin/CLAUDE.md` -- **后端问题**: 查看 `backend/CLAUDE.md` -- **UI 备份问题**: 查看 `ui/CLAUDE.md` - -### 通用问题 -```bash -# 检查项目状态 -cd frontend && make status - -# 查看构建日志 -cd frontend && make build - -# 重置环境 -cd frontend && make clean && make install -``` - -## 📈 项目状态 - -### 已完成功能 -- ✅ 前端应用 (Next.js 15) -- ✅ CI/CD 自动部署 -- ✅ Web 服务器配置 (Caddy) -- ✅ 代码质量控制 (ESLint + Prettier + TypeScript) -- ✅ Pre-commit hooks - -### 开发中功能 -- 📋 管理后台 (架构设计完成) -- 📋 后端 API (架构设计完成) -- 📋 UI 备份系统 (已实现) - -### 计划中功能 -- 📋 多环境部署 -- 📋 性能监控 -- 📋 AI 功能集成 -- 📋 数据分析系统 - -## 🎨 模块协调原则 - -### 何时修改模块 CLAUDE.md -1. **前端模块** (`frontend/CLAUDE.md`): 组件、样式、前端逻辑变更时 -2. **部署模块** (`docs/deployment/CLAUDE.md`): 服务器、配置、部署流程变更时 -3. **CI/CD 模块** (`.gitea/workflows/CLAUDE.md`): 工作流、构建流程变更时 -4. **管理后台模块** (`admin/CLAUDE.md`): 后台功能、权限管理变更时 -5. **后端模块** (`backend/CLAUDE.md`): API 接口、数据库架构变更时 -6. **UI 备份模块** (`ui/CLAUDE.md`): 组件备份、实验功能变更时 -7. **根目录** (`CLAUDE.md`): 项目架构、模块关系变更时 - -### 模块间通信 -- **前端 ↔ 后端**: API 接口调用和数据交换 -- **管理后台 ↔ 后端**: 管理接口和数据操作 -- **UI 备份 ↔ 前端**: 组件同步和实验功能验证 -- **前端构建产物 → 部署模块**: 静态文件部署 -- **CI/CD 协调 → 所有模块**: 构建和部署流程 -- **配置变更 → 相关模块**: CLAUDE.md 同步更新 - -## 🔄 最佳实践 - -### 开发流程 -1. 确定工作模块,切换到对应目录 -2. 阅读模块的 CLAUDE.md 了解具体指导 -3. 完成开发和测试 -4. 更新相关模块的 CLAUDE.md (如有必要) -5. 提交代码触发自动部署 - -### 文档维护 -- **模块独立**: 每个模块的 CLAUDE.md 保持独立和聚焦 -- **架构统一**: 模块间的依赖关系在根目录 CLAUDE.md 中说明 -- **配置集中**: 重要的全局配置统一在根目录管理 -- **及时更新**: 功能变更后立即更新对应的 CLAUDE.md -- **一致性**: 保持各模块文档的格式和风格一致 - -### 上下文优化 -- **聚焦开发**: Claude 工作时只需关注单个模块的 CLAUDE.md -- **减少负载**: 避免加载无关模块的文档,减少上下文长度 -- **提高效率**: 模块化降低复杂性,提高开发效率 -- **避免幻觉**: 精确的模块指导减少 AI 产生错误信息的可能性 -- **快速定位**: 问题出现时能快速定位到相关模块和文档 - -## 📋 模块 CLAUDE.md 文件列表 - -### 核心模块 -- ✅ **根目录** (`CLAUDE.md`) - 项目总览和模块协调 -- ✅ **前端模块** (`frontend/CLAUDE.md`) - 前端开发指导 -- ✅ **部署模块** (`docs/deployment/CLAUDE.md`) - 部署配置指导 -- ✅ **CI/CD 模块** (`.gitea/workflows/CLAUDE.md`) - 自动化部署指导 - -### 新增模块 -- ✅ **管理后台模块** (`admin/CLAUDE.md`) - 管理界面开发指导 -- ✅ **后端模块** (`backend/CLAUDE.md`) - API 服务开发指导 -- ✅ **UI 备份模块** (`ui/CLAUDE.md`) - 组件备份和实验指导 - -### 辅助模块 -- ✅ **工具脚本** (`scripts/README.md`) - 自动化脚本说明 - -## 🎯 使用建议 - -### 选择正确的模块 -1. **明确任务类型**: 根据要解决的问题选择对应的模块 -2. **切换工作目录**: 进入相应的模块目录进行开发 -3. **参考模块文档**: 仔细阅读模块的 CLAUDE.md 文件 -4. **遵循模块规范**: 按照模块的开发规范和流程进行工作 - -### 跨模块协作 -1. **理解依赖关系**: 了解模块间的依赖和数据流向 -2. **协调接口变更**: 涉及多个模块时,确保接口一致性 -3. **同步更新文档**: 跨模块变更时,同步更新相关文档 -4. **测试集成功能**: 确保模块间的集成功能正常工作 \ No newline at end of file +## 🎯 工作原则 +1. **模块化**: 每个目录独立工作 +2. **轻量级**: 只关注当前模块 +3. **文档化**: 每个模块都有专属指导 \ No newline at end of file diff --git a/backend-old/.gitignore b/backend-old/.gitignore deleted file mode 100644 index 00b15c1..0000000 --- a/backend-old/.gitignore +++ /dev/null @@ -1,98 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - -# IDE directories -.vscode/ -.idea/ -*.swp -*.swo - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Local environment files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Log files -*.log -logs/ - -# Database files -*.db -*.sqlite -*.sqlite3 - -# Build output -/bin/ -/dist/ -/build/ - -# Temporary files -/tmp/ -/temp/ -*.tmp - -# Coverage reports -coverage.out -coverage.html - -# Test cache -/test-results/ - -# Local uploads directory -/uploads/ - -# Local storage -/storage/ - -# Config files with secrets (keep templates) -config.local.yaml -config.secret.yaml - -# Node.js (should not be in Go project) -node_modules/ -package-lock.json -package.json -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor backup files -*~ -*.bak -*.backup - -# Profiling output -*.prof -*.cpu -*.mem - -# Air live reload -.air.toml -tmp/ \ No newline at end of file diff --git a/backend-old/CLAUDE.md b/backend-old/CLAUDE.md deleted file mode 100644 index a4c1b65..0000000 --- a/backend-old/CLAUDE.md +++ /dev/null @@ -1,596 +0,0 @@ -# Backend API Service - CLAUDE.md - -本文件为 Claude Code 在后端 API 服务模块中工作时提供指导。 - -## 🎯 模块概览 - -这是一个基于 Go + Gin 框架的 REST API 后端服务,采用简洁的四层架构模式,遵循 Go 语言的简洁设计哲学。 - -### 主要特性 -- 🏗️ 简洁四层架构 (API → Service → Repository → Model) -- 🚀 多种部署模式 (生产/开发/Mock) -- 📊 多数据库支持 (PostgreSQL + SQLite + Redis) -- 🔐 JWT 认证 + 基于角色的访问控制 -- 📁 文件上传和存储管理 -- 🐳 Docker 容器化部署 -- 📊 健康检查和监控 -- 📚 API 文档生成 - -### 技术栈 -- **语言**: Go 1.23+ -- **框架**: Gin v1.10.1 -- **数据库**: PostgreSQL (生产) + SQLite (开发) + Redis (缓存) -- **ORM**: GORM v1.30.0 -- **认证**: JWT (golang-jwt/jwt/v5) -- **日志**: Uber Zap -- **配置**: Viper -- **容器化**: Docker + Docker Compose - -## 📁 简洁架构设计 - -### 核心模块结构(重构后) -``` -backend/ -├── CLAUDE.md # 📋 当前文件 - 后端总览 -├── cmd/ # 🚀 应用入口模块 -│ ├── server/ # 服务启动器 -│ │ ├── CLAUDE.md # 启动服务配置指导 -│ │ └── main.go # 统一入口(支持多模式) -│ └── migrate/ # 数据库迁移工具 -│ └── main.go -├── internal/ # 📦 核心业务模块 -│ ├── api/ # 🌐 HTTP 接口层 -│ │ ├── CLAUDE.md # API 路由和处理器指导 -│ │ ├── handlers/ # HTTP 处理器 -│ │ ├── middleware/ # 中间件 -│ │ ├── routes/ # 路由定义 -│ │ └── validators/ # 请求验证 -│ ├── service/ # 📋 业务逻辑层 -│ │ ├── CLAUDE.md # 业务逻辑开发指导 -│ │ ├── auth/ # 认证服务 -│ │ ├── user/ # 用户服务 -│ │ ├── photo/ # 照片服务 -│ │ ├── category/ # 分类服务 -│ │ └── storage/ # 文件存储服务 -│ ├── repository/ # 🔧 数据访问层 -│ │ ├── CLAUDE.md # 数据访问开发指导 -│ │ ├── interfaces/ # 仓储接口 -│ │ ├── postgres/ # PostgreSQL 实现 -│ │ ├── redis/ # Redis 实现 -│ │ └── sqlite/ # SQLite 实现 -│ ├── model/ # 📦 数据模型层 -│ │ ├── CLAUDE.md # 数据模型设计指导 -│ │ ├── entity/ # 实体模型 -│ │ ├── dto/ # 数据传输对象 -│ │ └── request/ # 请求响应模型 -│ └── config/ # ⚙️ 配置管理 -│ ├── CLAUDE.md # 配置文件管理指导 -│ └── config.go # 配置结构体 -├── pkg/ # 📦 共享包模块 -│ ├── CLAUDE.md # 公共工具包指导 -│ ├── logger/ # 日志工具 -│ ├── response/ # 响应格式 -│ ├── validator/ # 验证器 -│ └── utils/ # 通用工具 -├── configs/ # 📋 配置文件 -├── migrations/ # 📊 数据库迁移 -├── tests/ # 🧪 测试模块 -│ ├── CLAUDE.md # 测试编写和执行指导 -│ ├── unit/ # 单元测试 -│ ├── integration/ # 集成测试 -│ └── mocks/ # 模拟对象 -└── docs/ # 📚 文档模块 - ├── CLAUDE.md # API 文档和接口设计指导 - └── api/ # API 文档 -``` - -### Go 风格的四层架构 - -#### 🌐 API 层 (`internal/api/`) -- **职责**: HTTP 请求处理、路由定义、中间件、参数验证 -- **文件**: `handlers/`, `middleware/`, `routes/`, `validators/` -- **指导**: `internal/api/CLAUDE.md` - -#### 📋 Service 层 (`internal/service/`) -- **职责**: 业务逻辑处理、服务编排、事务管理 -- **文件**: `auth/`, `user/`, `photo/`, `category/`, `storage/` -- **指导**: `internal/service/CLAUDE.md` - -#### 🔧 Repository 层 (`internal/repository/`) -- **职责**: 数据访问、数据库操作、缓存管理 -- **文件**: `interfaces/`, `postgres/`, `redis/`, `sqlite/` -- **指导**: `internal/repository/CLAUDE.md` - -#### 📦 Model 层 (`internal/model/`) -- **职责**: 数据模型、实体定义、DTO 转换 -- **文件**: `entity/`, `dto/`, `request/` -- **指导**: `internal/model/CLAUDE.md` - -### 简洁性原则 - -1. **单一职责**: 每个模块只负责一个明确的功能 -2. **依赖注入**: 使用接口解耦,便于测试和扩展 -3. **配置集中**: 所有配置统一管理,支持多环境 -4. **错误处理**: 统一的错误处理机制 -5. **代码生成**: 减少重复代码,提高开发效率 - -## 🚀 快速开始 - -### 开发环境设置 -```bash -# 1. 环境准备 -cd backend/ -make setup # 初始化开发环境 - -# 2. 开发模式选择 -make dev-simple # Mock 服务器 (前端开发) -make dev # SQLite 开发服务器 (全功能) -make dev-full # PostgreSQL 开发服务器 (生产环境) - -# 3. 生产部署 -make prod-up # Docker 容器部署 -``` - -### 服务模式说明 -- **Mock 模式**: 快速响应的模拟 API,用于前端开发 -- **开发模式**: 完整功能的 SQLite 数据库,用于本地开发 -- **生产模式**: PostgreSQL + Redis,用于生产环境 - -## 🔧 Go 风格开发规范 - -### 代码结构规范 -1. **四层架构**: API → Service → Repository → Model -2. **接口导向**: 使用接口定义契约,便于测试和替换 -3. **依赖注入**: 构造函数注入,避免全局变量 -4. **错误处理**: 显式错误处理,避免 panic -5. **并发安全**: 使用 context 和 sync 包确保并发安全 - -### Go 语言命名规范 -``` -文件和目录: -- 文件名: snake_case (user_service.go) -- 包名: 小写单词 (userservice 或 user) -- 目录名: 小写单词 (auth, user, photo) - -代码命名: -- 结构体: PascalCase (UserService, PhotoEntity) -- 接口: PascalCase + er结尾 (UserServicer, PhotoStorer) -- 方法/函数: PascalCase (GetUser, CreatePhoto) -- 变量: camelCase (userService, photoList) -- 常量: PascalCase (MaxUserCount, DefaultPageSize) -- 枚举: PascalCase (UserStatusActive, UserStatusInactive) -``` - -### 接口设计规范 -```go -// 接口定义 -type UserServicer interface { - GetUser(ctx context.Context, id uint) (*entity.User, error) - CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) - UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) error - DeleteUser(ctx context.Context, id uint) error - ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error) -} - -// 实现规范 -type UserService struct { - userRepo repository.UserRepositoryr - logger logger.Logger -} - -func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) UserServicer { - return &UserService{ - userRepo: userRepo, - logger: logger, - } -} -``` - -### RESTful API 设计规范 -``` -资源路径规范: -GET /api/v1/users # 获取用户列表 -POST /api/v1/users # 创建用户 -GET /api/v1/users/:id # 获取用户详情 -PUT /api/v1/users/:id # 更新用户 -DELETE /api/v1/users/:id # 删除用户 - -嵌套资源: -GET /api/v1/users/:id/photos # 获取用户的照片 -POST /api/v1/users/:id/photos # 为用户创建照片 - -查询参数: -GET /api/v1/users?page=1&limit=20&sort=created_at&order=desc -``` - -### 统一响应格式 -```go -// 成功响应 -type SuccessResponse struct { - Success bool `json:"success"` - Data interface{} `json:"data,omitempty"` - Message string `json:"message,omitempty"` - Timestamp int64 `json:"timestamp"` -} - -// 错误响应 -type ErrorResponse struct { - Success bool `json:"success"` - Error Error `json:"error"` - Timestamp int64 `json:"timestamp"` -} - -type Error struct { - Code string `json:"code"` - Message string `json:"message"` - Details string `json:"details,omitempty"` -} -``` - -### 错误处理规范 -```go -// 自定义错误类型 -type AppError struct { - Code string - Message string - Details string - Err error -} - -func (e *AppError) Error() string { - return e.Message -} - -// 错误码定义 -const ( - ErrCodeUserNotFound = "USER_NOT_FOUND" - ErrCodeInvalidParameter = "INVALID_PARAMETER" - ErrCodePermissionDenied = "PERMISSION_DENIED" - ErrCodeInternalError = "INTERNAL_ERROR" -) - -// 错误处理函数 -func HandleError(c *gin.Context, err error) { - var appErr *AppError - if errors.As(err, &appErr) { - c.JSON(http.StatusBadRequest, ErrorResponse{ - Success: false, - Error: Error{ - Code: appErr.Code, - Message: appErr.Message, - Details: appErr.Details, - }, - Timestamp: time.Now().Unix(), - }) - return - } - - // 未知错误 - c.JSON(http.StatusInternalServerError, ErrorResponse{ - Success: false, - Error: Error{ - Code: ErrCodeInternalError, - Message: "内部服务器错误", - }, - Timestamp: time.Now().Unix(), - }) -} -``` - -### 日志记录规范 -```go -// 结构化日志 -logger.Info("user created successfully", - zap.String("user_id", user.ID), - zap.String("username", user.Username), - zap.String("operation", "create_user"), -) - -// 错误日志 -logger.Error("failed to create user", - zap.Error(err), - zap.String("username", req.Username), - zap.String("operation", "create_user"), -) -``` - -### 配置管理规范 -```go -// 配置结构体 -type Config struct { - Server ServerConfig `mapstructure:"server"` - Database DatabaseConfig `mapstructure:"database"` - Redis RedisConfig `mapstructure:"redis"` - JWT JWTConfig `mapstructure:"jwt"` - Storage StorageConfig `mapstructure:"storage"` -} - -// 环境变量映射 -type ServerConfig struct { - Port string `mapstructure:"port" env:"SERVER_PORT"` - Mode string `mapstructure:"mode" env:"SERVER_MODE"` - LogLevel string `mapstructure:"log_level" env:"LOG_LEVEL"` -} -``` - -## 📊 数据库设计 - -### 主要实体 -- **User**: 用户信息和权限 -- **Photo**: 照片信息和元数据 -- **Category**: 照片分类 -- **Tag**: 照片标签 -- **Album**: 相册管理 - -### 关系设计 -``` -User (1:N) Photo -Photo (N:M) Category -Photo (N:M) Tag -User (1:N) Album -Album (N:M) Photo -``` - -## 🔐 认证和授权 - -### JWT 认证流程 -1. 用户登录 → 验证凭据 -2. 生成 JWT Token (AccessToken + RefreshToken) -3. 客户端携带 Token 访问受保护资源 -4. 服务器验证 Token 有效性 - -### 权限角色 -- **Admin**: 系统管理员 (所有权限) -- **Editor**: 内容编辑者 (内容管理) -- **User**: 普通用户 (查看权限) - -## 🧪 测试策略 - -### 测试类型 -- **单元测试**: 业务逻辑和工具函数 -- **集成测试**: API 接口和数据库交互 -- **性能测试**: 接口响应时间和并发测试 - -### 测试工具 -- **Go Testing**: 内置测试框架 -- **Testify**: 断言和模拟工具 -- **Mockery**: 接口模拟生成 - -## 📚 API 文档 - -### 文档生成 -- **Swagger/OpenAPI**: 自动生成 API 文档 -- **Postman Collection**: 接口测试集合 -- **README**: 快速开始指南 - -### 文档维护 -- 接口变更时同步更新文档 -- 提供完整的请求/响应示例 -- 包含错误码和处理说明 - -## 🚀 部署配置 - -### 环境变量 -```bash -# 数据库配置 -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=photography -DB_USER=postgres -DB_PASSWORD=password - -# JWT 配置 -JWT_SECRET=your-secret-key -JWT_EXPIRES_IN=24h - -# 文件存储 -STORAGE_TYPE=local -STORAGE_PATH=./uploads -``` - -### Docker 部署 -```bash -# 构建镜像 -make build-image - -# 启动服务 -make prod-up - -# 查看日志 -make logs -``` - -## 📋 常用命令 - -### 开发命令 -```bash -# 代码生成 -make generate # 生成代码 (mocks, swagger) - -# 代码检查 -make lint # 代码检查 -make fmt # 代码格式化 -make vet # 代码分析 - -# 测试 -make test # 运行测试 -make test-cover # 测试覆盖率 -make test-integration # 集成测试 - -# 构建 -make build # 构建二进制文件 -make build-image # 构建 Docker 镜像 -``` - -### 数据库命令 -```bash -# 迁移 -make migrate-up # 应用迁移 -make migrate-down # 回滚迁移 -make migrate-create # 创建迁移文件 - -# 数据库管理 -make db-reset # 重置数据库 -make db-seed # 导入种子数据 -``` - -## 🔍 问题排查 - -### 常见问题 -1. **数据库连接失败**: 检查配置文件和环境变量 -2. **JWT 验证失败**: 检查密钥配置和 Token 格式 -3. **文件上传失败**: 检查存储配置和权限设置 -4. **API 响应慢**: 检查数据库查询和缓存配置 - -### 日志查看 -```bash -# 查看应用日志 -tail -f logs/app.log - -# 查看错误日志 -tail -f logs/error.log - -# 查看访问日志 -tail -f logs/access.log -``` - -## 🎯 模块工作指南 - -### 根据工作内容选择模块 - -#### 🚀 应用启动和配置 -```bash -cd cmd/server/ -# 参考 cmd/server/CLAUDE.md -``` -**适用场景**: 服务启动、配置初始化、依赖注入 - -#### 🌐 API 接口开发 -```bash -cd internal/api/ -# 参考 internal/api/CLAUDE.md -``` -**适用场景**: 路由定义、HTTP 处理器、中间件、请求验证 - -#### 📋 业务逻辑开发 -```bash -cd internal/application/ -# 参考 internal/application/CLAUDE.md -``` -**适用场景**: 业务逻辑、服务编排、数据传输对象 - -#### 🏢 领域模型设计 -```bash -cd internal/domain/ -# 参考 internal/domain/CLAUDE.md -``` -**适用场景**: 业务实体、业务规则、仓储接口 - -#### 🔧 基础设施开发 -```bash -cd internal/infrastructure/ -# 参考 internal/infrastructure/CLAUDE.md -``` -**适用场景**: 数据库、缓存、文件存储、外部服务 - -#### 📦 工具包开发 -```bash -cd pkg/ -# 参考 pkg/CLAUDE.md -``` -**适用场景**: 通用工具、日志、验证器、响应格式 - -#### 🧪 测试开发 -```bash -cd tests/ -# 参考 tests/CLAUDE.md -``` -**适用场景**: 单元测试、集成测试、性能测试 - -#### 📚 文档维护 -```bash -cd docs/ -# 参考 docs/CLAUDE.md -``` -**适用场景**: API 文档、架构设计、部署指南 - -## 🔄 最佳实践 - -### 开发流程 -1. **功能分析**: 确定需求和技术方案 -2. **选择模块**: 根据工作内容选择对应模块 -3. **阅读指导**: 详细阅读模块的 CLAUDE.md 文件 -4. **编码实现**: 遵循模块规范进行开发 -5. **测试验证**: 编写和运行相关测试 -6. **文档更新**: 同步更新相关文档 - -### 代码质量 -- **代码审查**: 提交前进行代码审查 -- **测试覆盖**: 保持合理的测试覆盖率 -- **性能优化**: 关注接口响应时间和资源使用 -- **安全检查**: 验证认证、授权和数据验证 - -### 模块协调 -- **接口一致性**: 确保模块间接口的一致性 -- **依赖管理**: 合理管理模块间的依赖关系 -- **配置统一**: 统一配置管理和环境变量 -- **错误处理**: 统一错误处理和响应格式 - -## 📈 项目状态 - -### 已完成功能 -- ✅ 清洁架构设计 -- ✅ 多数据库支持 -- ✅ JWT 认证系统 -- ✅ 文件上传功能 -- ✅ Docker 部署 -- ✅ 基础 API 接口 - -### 开发中功能 -- 🔄 完整的测试覆盖 -- 🔄 API 文档生成 -- 🔄 性能监控 -- 🔄 缓存优化 - -### 计划中功能 -- 📋 微服务架构 -- 📋 分布式文件存储 -- 📋 消息队列集成 -- 📋 监控和报警系统 - -## 🔧 开发环境 - -### 必需工具 -- **Go 1.23+**: 编程语言 -- **PostgreSQL 14+**: 主数据库 -- **Redis 6+**: 缓存数据库 -- **Docker**: 容器化部署 -- **Make**: 构建工具 - -### 推荐工具 -- **GoLand/VSCode**: 代码编辑器 -- **Postman**: API 测试 -- **DBeaver**: 数据库管理 -- **Redis Desktop Manager**: Redis 管理 - -## 💡 开发技巧 - -### 性能优化 -- 使用数据库连接池 -- 实现查询结果缓存 -- 优化 SQL 查询语句 -- 使用异步处理 - -### 安全防护 -- 输入参数验证 -- SQL 注入防护 -- XSS 攻击防护 -- 访问频率限制 - -### 错误处理 -- 统一错误响应格式 -- 详细的错误日志记录 -- 适当的错误码设计 -- 友好的错误提示 - -本 CLAUDE.md 文件为后端开发提供了全面的指导,每个子模块都有详细的 CLAUDE.md 文件,确保开发过程中可以快速获取相关信息,提高开发效率。 \ No newline at end of file diff --git a/backend-old/Dockerfile b/backend-old/Dockerfile deleted file mode 100644 index c40fa30..0000000 --- a/backend-old/Dockerfile +++ /dev/null @@ -1,61 +0,0 @@ -# 构建阶段 -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 tzdata - -# 设置时区 -RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime -RUN echo 'Asia/Shanghai' > /etc/timezone - -# 创建非root用户 -RUN addgroup -g 1001 -S appgroup && \ - adduser -u 1001 -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/photos uploads/thumbnails uploads/temp && \ - chown -R appuser:appgroup uploads - -# 切换到非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"] \ No newline at end of file diff --git a/backend-old/Makefile b/backend-old/Makefile deleted file mode 100644 index e7461b8..0000000 --- a/backend-old/Makefile +++ /dev/null @@ -1,197 +0,0 @@ -# Photography Backend Makefile -# Simple and functional Makefile for Go backend project with Docker support - -.PHONY: help dev dev-up dev-down build clean docker-build docker-run prod-up prod-down status health fmt mod - -# Color definitions -GREEN := \033[0;32m -YELLOW := \033[1;33m -BLUE := \033[0;34m -RED := \033[0;31m -NC := \033[0m # No Color - -# Application configuration -APP_NAME := photography-backend -VERSION := 1.0.0 -BUILD_TIME := $(shell date +%Y%m%d_%H%M%S) -LDFLAGS := -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) - -# Build configuration -BUILD_DIR := bin -MAIN_FILE := cmd/server/main.go - -# Database configuration -DB_URL := postgres://postgres:password@localhost:5432/photography?sslmode=disable -MIGRATION_DIR := migrations - -# Default target -.DEFAULT_GOAL := help - -##@ Development Environment Commands - -dev: ## Start development server with SQLite database - @printf "$(GREEN)🚀 Starting development server with SQLite...\n$(NC)" - @go run cmd/server/main_with_db.go - -dev-simple: ## Start simple development server (mock data) - @printf "$(GREEN)🚀 Starting simple development server...\n$(NC)" - @go run cmd/server/simple_main.go - -dev-up: ## Start development environment with Docker - @printf "$(GREEN)🐳 Starting development environment...\n$(NC)" - @docker-compose -f docker-compose.dev.yml up -d - @printf "$(GREEN)✅ Development environment started successfully!\n$(NC)" - -dev-down: ## Stop development environment - @printf "$(GREEN)🛑 Stopping development environment...\n$(NC)" - @docker-compose -f docker-compose.dev.yml down - @printf "$(GREEN)✅ Development environment stopped!\n$(NC)" - -##@ Build Commands - -build: ## Build the Go application - @printf "$(GREEN)🔨 Building $(APP_NAME)...\n$(NC)" - @mkdir -p $(BUILD_DIR) - @CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE) - @printf "$(GREEN)✅ Build completed: $(BUILD_DIR)/$(APP_NAME)\n$(NC)" - -clean: ## Clean build artifacts - @printf "$(GREEN)🧹 Cleaning build files...\n$(NC)" - @rm -rf $(BUILD_DIR) - @rm -f coverage.out coverage.html - @printf "$(GREEN)✅ Clean completed!\n$(NC)" - -##@ Docker Commands - -docker-build: ## Build Docker image - @printf "$(GREEN)🐳 Building Docker image...\n$(NC)" - @docker build -t $(APP_NAME):$(VERSION) . - @docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest - @printf "$(GREEN)✅ Docker image built: $(APP_NAME):$(VERSION)\n$(NC)" - -docker-run: ## Run application in Docker container - @printf "$(GREEN)🐳 Running Docker container...\n$(NC)" - @docker-compose up -d - @printf "$(GREEN)✅ Docker container started!\n$(NC)" - -##@ Production Commands - -prod-up: ## Start production environment - @printf "$(GREEN)🚀 Starting production environment...\n$(NC)" - @docker-compose up -d - @printf "$(GREEN)✅ Production environment started!\n$(NC)" - -prod-down: ## Stop production environment - @printf "$(GREEN)🛑 Stopping production environment...\n$(NC)" - @docker-compose down - @printf "$(GREEN)✅ Production environment stopped!\n$(NC)" - -##@ Health Check & Status Commands - -status: ## Check application and services status - @printf "$(GREEN)📊 Checking application status...\n$(NC)" - @printf "$(BLUE)Docker containers:$(NC)\n" - @docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "($(APP_NAME)|postgres|redis)" || echo "No containers running" - @printf "$(BLUE)Application build:$(NC)\n" - @if [ -f "$(BUILD_DIR)/$(APP_NAME)" ]; then \ - printf "$(GREEN)✅ Binary exists: $(BUILD_DIR)/$(APP_NAME)\n$(NC)"; \ - ls -lh $(BUILD_DIR)/$(APP_NAME); \ - else \ - printf "$(RED)❌ Binary not found. Run 'make build' first.\n$(NC)"; \ - fi - -health: ## Check health of running services - @printf "$(GREEN)🏥 Checking service health...\n$(NC)" - @printf "$(BLUE)Testing application endpoint...\n$(NC)" - @curl -f http://localhost:8080/health 2>/dev/null && printf "$(GREEN)✅ Application is healthy\n$(NC)" || printf "$(RED)❌ Application is not responding\n$(NC)" - @printf "$(BLUE)Database connection...\n$(NC)" - @docker exec photography-postgres pg_isready -U postgres 2>/dev/null && printf "$(GREEN)✅ Database is ready\n$(NC)" || printf "$(RED)❌ Database is not ready\n$(NC)" - -##@ Code Quality Commands - -fmt: ## Format Go code - @printf "$(GREEN)🎨 Formatting Go code...\n$(NC)" - @go fmt ./... - @printf "$(GREEN)✅ Code formatted!\n$(NC)" - -mod: ## Tidy Go modules - @printf "$(GREEN)📦 Tidying Go modules...\n$(NC)" - @go mod tidy - @go mod download - @printf "$(GREEN)✅ Modules tidied!\n$(NC)" - -lint: ## Run code linter - @printf "$(GREEN)🔍 Running linter...\n$(NC)" - @golangci-lint run - @printf "$(GREEN)✅ Linting completed!\n$(NC)" - -test: ## Run tests - @printf "$(GREEN)🧪 Running tests...\n$(NC)" - @go test -v ./... - @printf "$(GREEN)✅ Tests completed!\n$(NC)" - -##@ Utility Commands - -install: ## Install dependencies - @printf "$(GREEN)📦 Installing dependencies...\n$(NC)" - @go mod download - @go mod tidy - @printf "$(GREEN)✅ Dependencies installed!\n$(NC)" - -logs: ## Show application logs - @printf "$(GREEN)📄 Showing application logs...\n$(NC)" - @docker-compose logs -f $(APP_NAME) - -migrate-up: ## Run database migrations - @printf "$(GREEN)🗄️ Running database migrations...\n$(NC)" - @migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" up - @printf "$(GREEN)✅ Migrations completed!\n$(NC)" - -migrate-down: ## Rollback database migrations - @printf "$(GREEN)🗄️ Rolling back database migrations...\n$(NC)" - @migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" down - @printf "$(GREEN)✅ Migrations rolled back!\n$(NC)" - -##@ Database Commands - -db-reset: ## Reset SQLite database (delete and recreate) - @printf "$(GREEN)🗄️ Resetting SQLite database...\n$(NC)" - @rm -f photography.db - @printf "$(GREEN)✅ Database reset! Will be recreated on next startup.\n$(NC)" - -db-backup: ## Backup SQLite database - @printf "$(GREEN)💾 Backing up SQLite database...\n$(NC)" - @cp photography.db photography_backup_$(BUILD_TIME).db - @printf "$(GREEN)✅ Database backed up to photography_backup_$(BUILD_TIME).db\n$(NC)" - -db-shell: ## Open SQLite database shell - @printf "$(GREEN)🐚 Opening SQLite database shell...\n$(NC)" - @sqlite3 photography.db - -db-status: ## Show database status and table info - @printf "$(GREEN)📊 Database status:\n$(NC)" - @if [ -f "photography.db" ]; then \ - printf "$(BLUE)Database file: photography.db ($(shell ls -lh photography.db | awk '{print $$5}'))\\n$(NC)"; \ - printf "$(BLUE)Tables:\\n$(NC)"; \ - sqlite3 photography.db ".tables"; \ - printf "$(BLUE)Row counts:\\n$(NC)"; \ - sqlite3 photography.db "SELECT 'Users: ' || COUNT(*) FROM users; SELECT 'Photos: ' || COUNT(*) FROM photos; SELECT 'Categories: ' || COUNT(*) FROM categories; SELECT 'Tags: ' || COUNT(*) FROM tags;"; \ - else \ - printf "$(RED)❌ Database not found. Run 'make dev' to create it.\\n$(NC)"; \ - fi - -##@ Help - -help: ## Display this help message - @printf "$(GREEN)Photography Backend Makefile\n$(NC)" - @printf "$(GREEN)============================\n$(NC)" - @printf "$(YELLOW)Simple and functional Makefile for Go backend project with Docker support\n$(NC)\n" - @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?##/ { printf "$(BLUE)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(GREEN)%s\n$(NC)", substr($$0, 5) } ' $(MAKEFILE_LIST) - @printf "\n$(YELLOW)Examples:\n$(NC)" - @printf "$(BLUE) make dev$(NC) - Start development server\n" - @printf "$(BLUE) make dev-up$(NC) - Start development environment\n" - @printf "$(BLUE) make build$(NC) - Build the application\n" - @printf "$(BLUE) make docker-build$(NC) - Build Docker image\n" - @printf "$(BLUE) make status$(NC) - Check application status\n" - @printf "$(BLUE) make health$(NC) - Check service health\n" - @printf "\n$(GREEN)For more information, visit: https://github.com/iriver/photography\n$(NC)" \ No newline at end of file diff --git a/backend-old/README.md b/backend-old/README.md deleted file mode 100644 index 8392283..0000000 --- a/backend-old/README.md +++ /dev/null @@ -1,245 +0,0 @@ -# 摄影作品集后端 API - -基于 Go + Gin + GORM + PostgreSQL 的摄影作品集后端服务。 - -## 🚀 快速开始 - -### 环境要求 - -- Go 1.21+ -- PostgreSQL 12+ -- Git - -### 启动服务 - -```bash -# 1. 克隆项目(如果需要) -git clone -cd photography/backend - -# 2. 安装依赖 -go mod tidy - -# 3. 配置数据库 -# 确保 PostgreSQL 正在运行 -# 创建数据库: CREATE DATABASE photography_dev; - -# 4. 启动服务 -./start.sh -# 或者 -go run cmd/server/main_simple.go -``` - -服务启动后访问: -- 健康检查: http://localhost:8080/health -- API 根路径: http://localhost:8080/api/v1 - -## 📁 项目结构 - -``` -backend/ -├── cmd/server/ # 应用入口 -├── internal/ -│ ├── config/ # 配置管理 -│ ├── database/ # 数据库连接 -│ ├── models/ # 数据模型 -│ ├── service/ # 业务逻辑 -│ ├── handlers/ # HTTP 处理器 -│ └── middleware/ # 中间件 -├── migrations/ # 数据库迁移文件 -├── configs/ # 配置文件 -├── uploads/ # 文件上传目录 -└── test_api.http # API 测试文件 -``` - -## 🗄️ 数据库架构 - -### 核心表 - -- **users**: 用户表 -- **categories**: 分类表(支持层级) -- **tags**: 标签表 -- **albums**: 相册表 -- **photos**: 照片表 -- **photo_tags**: 照片标签关联表 -- **album_tags**: 相册标签关联表 - -### 数据库迁移 - -项目使用 SQL 迁移文件: - -1. `001_create_users.sql` - 用户表 -2. `002_create_photos.sql` - 照片表 -3. `003_create_albums.sql` - 相册表 -4. `004_create_categories.sql` - 分类表 -5. `005_create_tags.sql` - 标签表 -6. `006_add_foreign_keys.sql` - 外键约束 - -## 🔌 API 接口 - -### 认证接口 - -- `POST /api/v1/auth/register` - 用户注册 -- `POST /api/v1/auth/login` - 用户登录 -- `POST /api/v1/auth/refresh` - 刷新令牌 - -### 用户管理 - -- `GET /api/v1/users` - 获取用户列表 -- `POST /api/v1/users` - 创建用户 -- `GET /api/v1/users/:id` - 获取用户详情 -- `PUT /api/v1/users/:id` - 更新用户 -- `DELETE /api/v1/users/:id` - 删除用户 - -### 分类管理 - -- `GET /api/v1/categories` - 获取分类列表 -- `POST /api/v1/categories` - 创建分类 -- `GET /api/v1/categories/:id` - 获取分类详情 -- `PUT /api/v1/categories/:id` - 更新分类 -- `DELETE /api/v1/categories/:id` - 删除分类 - -### 标签管理 - -- `GET /api/v1/tags` - 获取标签列表 -- `POST /api/v1/tags` - 创建标签 -- `GET /api/v1/tags/:id` - 获取标签详情 -- `PUT /api/v1/tags/:id` - 更新标签 -- `DELETE /api/v1/tags/:id` - 删除标签 - -### 相册管理 - -- `GET /api/v1/albums` - 获取相册列表 -- `POST /api/v1/albums` - 创建相册 -- `GET /api/v1/albums/:id` - 获取相册详情 -- `PUT /api/v1/albums/:id` - 更新相册 -- `DELETE /api/v1/albums/:id` - 删除相册 - -### 照片管理 - -- `GET /api/v1/photos` - 获取照片列表 -- `POST /api/v1/photos` - 创建照片记录 -- `GET /api/v1/photos/:id` - 获取照片详情 -- `PUT /api/v1/photos/:id` - 更新照片 -- `DELETE /api/v1/photos/:id` - 删除照片 - -### 文件上传 - -- `POST /api/v1/upload/photo` - 上传照片(需要认证) -- `DELETE /api/v1/upload/photo/:id` - 删除照片(需要认证) -- `GET /api/v1/upload/stats` - 获取上传统计(需要管理员权限) - -### 静态文件 - -- `GET /uploads/photos/:filename` - 获取照片文件 -- `GET /uploads/thumbnails/:filename` - 获取缩略图 - -## 🔧 配置 - -### 环境配置文件 - -- `configs/config.yaml` - 基础配置 -- `configs/config.dev.yaml` - 开发环境 -- `configs/config.prod.yaml` - 生产环境 - -### 环境变量 - -```bash -# 数据库 -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=password -DB_NAME=photography_dev - -# JWT -JWT_SECRET=your-secret-key - -# 服务器 -PORT=8080 -``` - -## 🧪 测试 - -使用 `test_api.http` 文件进行 API 测试: - -```bash -# 1. 健康检查 -GET http://localhost:8080/health - -# 2. 用户注册 -POST http://localhost:8080/api/v1/auth/register -Content-Type: application/json - -{ - "username": "testuser", - "email": "test@example.com", - "password": "password123", - "name": "测试用户" -} -``` - -## 📝 功能特性 - -### 已实现功能 - -- ✅ 用户认证和授权 -- ✅ 用户管理(CRUD) -- ✅ 分类管理(支持层级) -- ✅ 标签管理(多对多关系) -- ✅ 相册管理 -- ✅ 照片管理和上传 -- ✅ 文件存储(本地) -- ✅ 数据库自动迁移 -- ✅ JWT 认证 -- ✅ 权限控制 -- ✅ 静态文件服务 - -### 计划功能 - -- 📋 密码加密(bcrypt) -- 📋 完整的 JWT 认证流程 -- 📋 图片缩略图生成 -- 📋 EXIF 数据提取 -- 📋 阿里云 OSS 集成 -- 📋 Redis 缓存 -- 📋 API 文档(Swagger) -- 📋 单元测试 -- 📋 Docker 支持 - -## 🔄 部署 - -### 开发环境 - -```bash -./start.sh -``` - -### 生产环境 - -```bash -export CONFIG_PATH="configs/config.prod.yaml" -go build -o photography-backend cmd/server/main_simple.go -./photography-backend -``` - -## 📚 依赖 - -- **Gin**: HTTP Web 框架 -- **GORM**: ORM 库 -- **PostgreSQL**: 数据库驱动 -- **JWT**: 认证令牌 -- **Viper**: 配置管理 -- **Zap**: 日志库 - -## 🤝 贡献 - -1. Fork 项目 -2. 创建功能分支 -3. 提交更改 -4. 推送到分支 -5. 创建 Pull Request - -## 📄 许可证 - -MIT License \ No newline at end of file diff --git a/backend-old/cmd/server/CLAUDE.md b/backend-old/cmd/server/CLAUDE.md deleted file mode 100644 index c945737..0000000 --- a/backend-old/cmd/server/CLAUDE.md +++ /dev/null @@ -1,363 +0,0 @@ -# Server Entry Point - CLAUDE.md - -本文件为 Claude Code 在服务器入口模块中工作时提供指导。 - -## 🎯 模块概览 - -这是后端服务的启动入口模块,负责应用初始化、配置加载、依赖注入和服务启动。 - -### 主要职责 -- 🚀 应用启动和生命周期管理 -- ⚙️ 配置文件加载和环境变量解析 -- 🔌 依赖注入和组件初始化 -- 🌐 HTTP 服务器启动和路由注册 -- 📊 数据库连接和迁移管理 -- 🔧 优雅关闭和资源清理 - -### 推荐文件结构(Go 风格) -``` -cmd/server/ -├── CLAUDE.md # 📋 当前文件 - 服务启动指导 -├── main.go # 🚀 统一入口点 -├── app.go # 🏗️ 应用构建器 -├── config.go # ⚙️ 配置加载器 -├── dependencies.go # 🔌 依赖注入容器 -└── server.go # 🌐 HTTP 服务器管理 -``` - -## 🚀 启动模式说明 - -### 生产模式 (`main.go`) -```go -// 特点: -// - PostgreSQL 数据库 -// - Redis 缓存 -// - 完整的中间件栈 -// - 生产级日志配置 -// - 健康检查端点 -``` - -**适用场景**: 生产环境、集成测试、性能测试 - -### 开发模式 (`main_with_db.go`) -```go -// 特点: -// - SQLite 数据库 -// - 内存缓存 -// - 开发友好的日志 -// - 热重载支持 -// - 调试工具集成 -``` - -**适用场景**: 本地开发、单元测试、功能验证 - -### Mock 模式 (`simple_main.go`) -```go -// 特点: -// - 内存数据存储 -// - 固定响应数据 -// - 极快启动速度 -// - 无数据库依赖 -// - 最小化配置 -``` - -**适用场景**: 前端开发、API 测试、演示环境 - -## 🔧 配置管理 - -### 配置文件层次 -``` -configs/ -├── config.yaml # 基础配置 -├── config.dev.yaml # 开发环境覆盖 -└── config.prod.yaml # 生产环境覆盖 -``` - -### 环境变量优先级 -``` -环境变量 > 配置文件 > 默认值 -``` - -### 配置结构 -```go -type Config struct { - Server ServerConfig `yaml:"server"` - Database DatabaseConfig `yaml:"database"` - Cache CacheConfig `yaml:"cache"` - Storage StorageConfig `yaml:"storage"` - Auth AuthConfig `yaml:"auth"` - Log LogConfig `yaml:"log"` -} -``` - -## 🏗️ 依赖注入 - -### 依赖层次 -``` -main.go -├── 📋 Config Service -├── 📊 Database Service -├── 💾 Cache Service -├── 📁 Storage Service -├── 🔐 Auth Service -├── 📚 Repository Layer -├── 🏢 Service Layer -└── 🌐 Handler Layer -``` - -### 依赖注入模式 -```go -// 1. 配置加载 -config := config.LoadConfig() - -// 2. 基础服务初始化 -db := database.NewConnection(config.Database) -cache := cache.NewRedisClient(config.Cache) -storage := storage.NewService(config.Storage) - -// 3. 仓储层初始化 -userRepo := repository.NewUserRepository(db) -photoRepo := repository.NewPhotoRepository(db) - -// 4. 服务层初始化 -userService := service.NewUserService(userRepo, cache) -photoService := service.NewPhotoService(photoRepo, storage) - -// 5. 处理器层初始化 -userHandler := handler.NewUserHandler(userService) -photoHandler := handler.NewPhotoHandler(photoService) - -// 6. 路由设置 -router := gin.New() -v1 := router.Group("/api/v1") -{ - v1.POST("/users", userHandler.Create) - v1.GET("/users", userHandler.List) - v1.GET("/photos", photoHandler.List) - v1.POST("/photos", photoHandler.Create) -} -``` - -## 🔄 启动流程 - -### 标准启动流程 -1. **配置加载**: 解析配置文件和环境变量 -2. **日志初始化**: 设置日志级别和输出格式 -3. **数据库连接**: 建立数据库连接池 -4. **缓存连接**: 初始化 Redis 连接 -5. **服务注册**: 注册各层服务 -6. **中间件设置**: 配置认证、日志、CORS 等中间件 -7. **路由注册**: 注册所有 API 路由 -8. **健康检查**: 启动健康检查端点 -9. **服务启动**: 启动 HTTP 服务器 -10. **优雅关闭**: 处理关闭信号 - -### 启动命令 -```bash -# 生产模式 -go run cmd/server/main.go - -# 开发模式 -go run cmd/server/main_with_db.go - -# Mock 模式 -go run cmd/server/simple_main.go - -# 使用 Makefile -make dev # 开发模式 -make dev-simple # Mock 模式 -make prod # 生产模式 -``` - -## 📊 健康检查 - -### 健康检查端点 -```go -GET /health -GET /health/ready -GET /health/live -``` - -### 健康检查内容 -- **数据库连接状态** -- **缓存服务状态** -- **存储服务状态** -- **外部服务状态** -- **系统资源状态** - -### 响应格式 -```json -{ - "status": "healthy", - "timestamp": "2024-01-01T00:00:00Z", - "version": "1.0.0", - "checks": { - "database": "healthy", - "cache": "healthy", - "storage": "healthy" - } -} -``` - -## 🔧 优雅关闭 - -### 关闭信号处理 -```go -// 监听关闭信号 -quit := make(chan os.Signal, 1) -signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - -// 等待关闭信号 -<-quit -log.Println("Shutting down server...") - -// 设置关闭超时 -ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -defer cancel() - -// 优雅关闭服务器 -if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server forced to shutdown:", err) -} -``` - -### 关闭流程 -1. **停止接收新请求** -2. **等待现有请求完成** -3. **关闭数据库连接** -4. **关闭缓存连接** -5. **清理临时资源** -6. **记录关闭日志** - -## 📋 环境变量配置 - -### 数据库配置 -```bash -# PostgreSQL -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=photography -DB_USER=postgres -DB_PASSWORD=your_password -DB_SSL_MODE=disable - -# SQLite (开发模式) -DB_TYPE=sqlite -DB_PATH=./photography.db -``` - -### 缓存配置 -```bash -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= -REDIS_DB=0 -``` - -### 认证配置 -```bash -# JWT -JWT_SECRET=your_jwt_secret -JWT_EXPIRES_IN=24h -JWT_REFRESH_EXPIRES_IN=168h -``` - -### 文件存储配置 -```bash -# 本地存储 -STORAGE_TYPE=local -STORAGE_PATH=./uploads -STORAGE_MAX_SIZE=10MB - -# S3 存储 -STORAGE_TYPE=s3 -AWS_ACCESS_KEY_ID=your_access_key -AWS_SECRET_ACCESS_KEY=your_secret_key -AWS_BUCKET_NAME=your_bucket -AWS_REGION=us-east-1 -``` - -## 🧪 开发调试 - -### 调试模式启动 -```bash -# 开启调试模式 -export GIN_MODE=debug -export LOG_LEVEL=debug - -# 启动服务 -go run cmd/server/main_with_db.go -``` - -### 调试工具 -- **pprof**: 性能分析 -- **gin-debug**: 路由调试 -- **hot-reload**: 代码热重载 -- **swagger**: API 文档 - -### 调试端点 -``` -GET /debug/pprof/ # 性能分析 -GET /debug/routes # 路由列表 -GET /debug/vars # 运行时变量 -GET /swagger/* # API 文档 -``` - -## 📊 监控指标 - -### 内置指标 -- **HTTP 请求数量** -- **HTTP 响应时间** -- **数据库连接数** -- **缓存命中率** -- **内存使用量** -- **CPU 使用率** - -### 指标端点 -``` -GET /metrics # Prometheus 指标 -GET /stats # 应用统计信息 -``` - -## 🔍 常见问题 - -### 启动失败 -1. **端口被占用**: 检查端口配置,使用 `lsof -i :8080` 查看 -2. **配置文件错误**: 检查 YAML 语法,验证配置项 -3. **数据库连接失败**: 检查数据库服务状态和连接配置 -4. **权限问题**: 检查文件读写权限 - -### 性能问题 -1. **启动慢**: 检查数据库连接池配置 -2. **内存泄漏**: 使用 pprof 分析内存使用 -3. **连接超时**: 调整超时配置和连接池大小 - -### 日志问题 -1. **日志文件过大**: 配置日志轮转 -2. **日志格式混乱**: 统一日志格式配置 -3. **敏感信息泄露**: 配置敏感信息过滤 - -## 💡 最佳实践 - -### 配置管理 -- 使用环境变量覆盖敏感配置 -- 配置验证和默认值设置 -- 配置变更的版本控制 - -### 错误处理 -- 统一错误响应格式 -- 详细的错误日志记录 -- 适当的错误码设计 - -### 安全考虑 -- 敏感信息不在日志中输出 -- 配置文件权限控制 -- 环境变量加密存储 - -### 性能优化 -- 合理的超时配置 -- 连接池大小调优 -- 资源及时释放 - -本模块为整个应用的入口,确保配置正确、启动流程清晰是项目成功的关键。在开发过程中,优先使用开发模式进行功能验证,在集成测试时使用生产模式。 \ No newline at end of file diff --git a/backend-old/cmd/server/main.go b/backend-old/cmd/server/main.go deleted file mode 100644 index b2e7887..0000000 --- a/backend-old/cmd/server/main.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/signal" - "syscall" - "context" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" - - "photography-backend/internal/config" - "photography-backend/internal/database" - "photography-backend/internal/repository/postgres" - "photography-backend/internal/service" - "photography-backend/internal/service/auth" - "photography-backend/internal/api/handlers" - "photography-backend/internal/api/middleware" - "photography-backend/internal/api/routes" - "photography-backend/pkg/logger" -) - -func main() { - // 加载配置 - cfg, err := config.LoadConfig("configs/config.yaml") - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - - // 初始化日志 - zapLogger, err := logger.InitLogger(&cfg.Logger) - if err != nil { - log.Fatalf("Failed to initialize logger: %v", err) - } - defer zapLogger.Sync() - - // 初始化数据库 - db, err := database.New(cfg) - if err != nil { - zapLogger.Fatal("Failed to connect to database", zap.Error(err)) - } - defer db.Close() - - // 自动迁移数据库 - if err := db.AutoMigrate(); err != nil { - zapLogger.Fatal("Failed to migrate database", zap.Error(err)) - } - - // 填充种子数据 - if err := db.Seed(); err != nil { - zapLogger.Warn("Failed to seed database", zap.Error(err)) - } - - // 初始化仓库 - userRepo := postgres.NewUserRepository(db.GetDB()) - photoRepo := postgres.NewPhotoRepository(db.GetDB()) - categoryRepo := postgres.NewCategoryRepository(db.GetDB()) - tagRepo := postgres.NewTagRepository(db.GetDB()) - - // 初始化服务 - jwtService := auth.NewJWTService(&cfg.JWT) - authService := auth.NewAuthService(userRepo, jwtService) - photoService := service.NewPhotoService(photoRepo) - categoryService := service.NewCategoryService(categoryRepo) - tagService := service.NewTagService(tagRepo) - userService := service.NewUserService(userRepo) - - // 初始化处理器 - authHandler := handlers.NewAuthHandler(authService) - photoHandler := handlers.NewPhotoHandler(photoService) - categoryHandler := handlers.NewCategoryHandler(categoryService) - tagHandler := handlers.NewTagHandler(tagService) - userHandler := handlers.NewUserHandler(userService) - - // 初始化中间件 - authMiddleware := middleware.NewAuthMiddleware(jwtService) - - // 设置Gin模式 - if cfg.IsProduction() { - gin.SetMode(gin.ReleaseMode) - } - - // 创建Gin引擎 - r := gin.New() - - // 添加中间件 - r.Use(middleware.RequestIDMiddleware()) - r.Use(middleware.LoggerMiddleware(zapLogger)) - r.Use(middleware.CORSMiddleware(&cfg.CORS)) - r.Use(gin.Recovery()) - - // 健康检查 - r.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "timestamp": time.Now().Unix(), - "version": cfg.App.Version, - }) - }) - - // 设置路由 - routes.SetupRoutes(r, &routes.Handlers{ - AuthHandler: authHandler, - PhotoHandler: photoHandler, - CategoryHandler: categoryHandler, - TagHandler: tagHandler, - UserHandler: userHandler, - }, authMiddleware, zapLogger) - - // 创建HTTP服务器 - server := &http.Server{ - Addr: cfg.GetServerAddr(), - Handler: r, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - - // 启动服务器 - go func() { - zapLogger.Info("Starting server", zap.String("addr", server.Addr)) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - zapLogger.Fatal("Failed to start server", zap.Error(err)) - } - }() - - // 等待中断信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - zapLogger.Info("Shutting down server...") - - // 优雅关闭 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - zapLogger.Fatal("Server forced to shutdown", zap.Error(err)) - } - - zapLogger.Info("Server exited") -} \ No newline at end of file diff --git a/backend-old/cmd/server/main_simple.go b/backend-old/cmd/server/main_simple.go deleted file mode 100644 index 38f9914..0000000 --- a/backend-old/cmd/server/main_simple.go +++ /dev/null @@ -1,683 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/gin-gonic/gin" - "photography-backend/internal/config" - "photography-backend/internal/database" - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service/upload" - "photography-backend/internal/service/auth" - "photography-backend/internal/api/handlers" - "photography-backend/internal/api/middleware" -) - -func main() { - // 加载配置 - configPath := os.Getenv("CONFIG_PATH") - if configPath == "" { - configPath = "configs/config.yaml" - } - - cfg, err := config.LoadConfig(configPath) - if err != nil { - log.Fatalf("加载配置失败: %v", err) - } - - // 初始化数据库 - if err := database.InitDatabase(cfg); err != nil { - log.Fatalf("数据库初始化失败: %v", err) - } - - // 自动迁移数据表 - if err := database.AutoMigrate(); err != nil { - log.Fatalf("数据库迁移失败: %v", err) - } - - // 创建服务 - uploadService := upload.NewUploadService(cfg) - jwtService := auth.NewJWTService(&cfg.JWT) - - // 创建处理器 - uploadHandler := handlers.NewUploadHandler(uploadService) - - // 创建中间件 - authMiddleware := middleware.NewAuthMiddleware(jwtService) - - // 设置 Gin 模式 - if cfg.IsProduction() { - gin.SetMode(gin.ReleaseMode) - } - - // 创建 Gin 引擎 - r := gin.Default() - - // 添加 CORS 中间件 - r.Use(func(c *gin.Context) { - c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - }) - - // 健康检查 - r.GET("/health", func(c *gin.Context) { - // 检查数据库连接 - if err := database.HealthCheck(); err != nil { - c.JSON(http.StatusServiceUnavailable, gin.H{ - "status": "error", - "message": "数据库连接失败", - "error": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "timestamp": time.Now().Unix(), - "version": cfg.App.Version, - "database": "connected", - }) - }) - - // 数据库统计 - r.GET("/stats", func(c *gin.Context) { - stats := database.GetStats() - c.JSON(http.StatusOK, gin.H{ - "database_stats": stats, - }) - }) - - // 静态文件服务 - r.Static("/uploads", cfg.Storage.Local.BasePath) - - // 基础 API 路由 - api := r.Group("/api/v1") - { - // 文件上传路由 - upload := api.Group("/upload") - { - upload.POST("/photo", authMiddleware.RequireAuth(), uploadHandler.UploadPhoto) - upload.DELETE("/photo/:id", authMiddleware.RequireAuth(), uploadHandler.DeletePhoto) - upload.GET("/stats", authMiddleware.RequireAdmin(), uploadHandler.GetUploadStats) - } - - // 认证路由 - auth := api.Group("/auth") - { - auth.POST("/login", login) - auth.POST("/register", register) - auth.POST("/refresh", refreshToken) - } - - // 用户相关 - users := api.Group("/users") - { - users.GET("", getUsers) - users.POST("", createUser) - users.GET("/:id", getUser) - users.PUT("/:id", updateUser) - users.DELETE("/:id", deleteUser) - } - - // 分类相关 - categories := api.Group("/categories") - { - categories.GET("", getCategories) - categories.POST("", createCategory) - categories.GET("/:id", getCategory) - categories.PUT("/:id", updateCategory) - categories.DELETE("/:id", deleteCategory) - } - - // 标签相关 - tags := api.Group("/tags") - { - tags.GET("", getTags) - tags.POST("", createTag) - tags.GET("/:id", getTag) - tags.PUT("/:id", updateTag) - tags.DELETE("/:id", deleteTag) - } - - // 相册相关 - albums := api.Group("/albums") - { - albums.GET("", getAlbums) - albums.POST("", createAlbum) - albums.GET("/:id", getAlbum) - albums.PUT("/:id", updateAlbum) - albums.DELETE("/:id", deleteAlbum) - } - - // 照片相关 - photos := api.Group("/photos") - { - photos.GET("", getPhotos) - photos.POST("", createPhoto) - photos.GET("/:id", getPhoto) - photos.PUT("/:id", updatePhoto) - photos.DELETE("/:id", deletePhoto) - } - } - - // 创建 HTTP 服务器 - server := &http.Server{ - Addr: cfg.GetServerAddr(), - Handler: r, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - - // 启动服务器 - go func() { - fmt.Printf("服务器启动在 %s\n", server.Addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("服务器启动失败: %v", err) - } - }() - - // 等待中断信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - fmt.Println("正在关闭服务器...") - - // 优雅关闭 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - log.Fatalf("服务器强制关闭: %v", err) - } - - // 关闭数据库连接 - if err := database.Close(); err != nil { - log.Printf("关闭数据库连接失败: %v", err) - } - - fmt.Println("服务器已关闭") -} - -// 认证相关处理函数 -func login(c *gin.Context) { - var req dto.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 查找用户 - var user entity.User - db := database.GetDB() - - // 可以使用用户名或邮箱登录 - if err := db.Where("username = ? OR email = ?", req.Email, req.Email).First(&user).Error; err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) - return - } - - // TODO: 验证密码 (需要集成bcrypt) - // 这里暂时跳过密码验证 - - // 生成JWT令牌 (简化实现) - c.JSON(http.StatusOK, gin.H{ - "message": "登录成功", - "user": gin.H{ - "id": user.ID, - "username": user.Username, - "email": user.Email, - "role": user.Role, - }, - "token": "mock-jwt-token", // 实际项目中应该生成真实的JWT - }) -} - -func register(c *gin.Context) { - var req dto.CreateUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 检查用户名是否已存在 - var existingUser entity.User - db := database.GetDB() - if err := db.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil { - c.JSON(http.StatusConflict, gin.H{"error": "用户名或邮箱已存在"}) - return - } - - // 创建用户 - user := entity.User{ - Username: req.Username, - Email: req.Email, - Password: req.Password, // 实际项目中应该加密 - Name: req.Name, - Role: entity.UserRoleUser, - IsActive: true, - } - - if err := db.Create(&user).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // 清除密码 - user.Password = "" - - c.JSON(http.StatusCreated, gin.H{ - "message": "注册成功", - "user": user, - }) -} - -func refreshToken(c *gin.Context) { - // TODO: 实现刷新令牌逻辑 - c.JSON(http.StatusOK, gin.H{ - "message": "刷新令牌成功", - "token": "new-mock-jwt-token", - }) -} - -// 用户 CRUD 操作 -func getUsers(c *gin.Context) { - var users []entity.User - db := database.GetDB() - - if err := db.Find(&users).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": users}) -} - -func createUser(c *gin.Context) { - var user entity.User - if err := c.ShouldBindJSON(&user); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := database.GetDB() - if err := db.Create(&user).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": user}) -} - -func getUser(c *gin.Context) { - id := c.Param("id") - var user entity.User - db := database.GetDB() - - if err := db.First(&user, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": user}) -} - -func updateUser(c *gin.Context) { - id := c.Param("id") - var user entity.User - db := database.GetDB() - - if err := db.First(&user, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) - return - } - - if err := c.ShouldBindJSON(&user); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&user).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": user}) -} - -func deleteUser(c *gin.Context) { - id := c.Param("id") - db := database.GetDB() - - if err := db.Delete(&entity.User{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"}) -} - -// 分类 CRUD 操作 -func getCategories(c *gin.Context) { - var categories []entity.Category - db := database.GetDB() - - if err := db.Find(&categories).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": categories}) -} - -func createCategory(c *gin.Context) { - var category entity.Category - if err := c.ShouldBindJSON(&category); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := database.GetDB() - if err := db.Create(&category).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": category}) -} - -func getCategory(c *gin.Context) { - id := c.Param("id") - var category entity.Category - db := database.GetDB() - - if err := db.First(&category, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": category}) -} - -func updateCategory(c *gin.Context) { - id := c.Param("id") - var category entity.Category - db := database.GetDB() - - if err := db.First(&category, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"}) - return - } - - if err := c.ShouldBindJSON(&category); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&category).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": category}) -} - -func deleteCategory(c *gin.Context) { - id := c.Param("id") - db := database.GetDB() - - if err := db.Delete(&entity.Category{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "分类删除成功"}) -} - -// 标签 CRUD 操作 -func getTags(c *gin.Context) { - var tags []entity.Tag - db := database.GetDB() - - if err := db.Find(&tags).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": tags}) -} - -func createTag(c *gin.Context) { - var tag entity.Tag - if err := c.ShouldBindJSON(&tag); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := database.GetDB() - if err := db.Create(&tag).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": tag}) -} - -func getTag(c *gin.Context) { - id := c.Param("id") - var tag entity.Tag - db := database.GetDB() - - if err := db.First(&tag, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": tag}) -} - -func updateTag(c *gin.Context) { - id := c.Param("id") - var tag entity.Tag - db := database.GetDB() - - if err := db.First(&tag, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"}) - return - } - - if err := c.ShouldBindJSON(&tag); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&tag).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": tag}) -} - -func deleteTag(c *gin.Context) { - id := c.Param("id") - db := database.GetDB() - - if err := db.Delete(&entity.Tag{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "标签删除成功"}) -} - -// 相册 CRUD 操作 -func getAlbums(c *gin.Context) { - var albums []entity.Album - db := database.GetDB() - - if err := db.Find(&albums).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": albums}) -} - -func createAlbum(c *gin.Context) { - var album entity.Album - if err := c.ShouldBindJSON(&album); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := database.GetDB() - if err := db.Create(&album).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": album}) -} - -func getAlbum(c *gin.Context) { - id := c.Param("id") - var album entity.Album - db := database.GetDB() - - if err := db.First(&album, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "相册不存在"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": album}) -} - -func updateAlbum(c *gin.Context) { - id := c.Param("id") - var album entity.Album - db := database.GetDB() - - if err := db.First(&album, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "相册不存在"}) - return - } - - if err := c.ShouldBindJSON(&album); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&album).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": album}) -} - -func deleteAlbum(c *gin.Context) { - id := c.Param("id") - db := database.GetDB() - - if err := db.Delete(&entity.Album{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "相册删除成功"}) -} - -// 照片 CRUD 操作 -func getPhotos(c *gin.Context) { - var photos []entity.Photo - db := database.GetDB() - - if err := db.Find(&photos).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": photos}) -} - -func createPhoto(c *gin.Context) { - var photo entity.Photo - if err := c.ShouldBindJSON(&photo); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := database.GetDB() - if err := db.Create(&photo).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": photo}) -} - -func getPhoto(c *gin.Context) { - id := c.Param("id") - var photo entity.Photo - db := database.GetDB() - - if err := db.First(&photo, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": photo}) -} - -func updatePhoto(c *gin.Context) { - id := c.Param("id") - var photo entity.Photo - db := database.GetDB() - - if err := db.First(&photo, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"}) - return - } - - if err := c.ShouldBindJSON(&photo); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&photo).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": photo}) -} - -func deletePhoto(c *gin.Context) { - id := c.Param("id") - db := database.GetDB() - - if err := db.Delete(&entity.Photo{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "照片删除成功"}) -} \ No newline at end of file diff --git a/backend-old/cmd/server/main_with_db.go b/backend-old/cmd/server/main_with_db.go deleted file mode 100644 index 3fc6e2e..0000000 --- a/backend-old/cmd/server/main_with_db.go +++ /dev/null @@ -1,786 +0,0 @@ -package main - -import ( - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/gin-contrib/cors" - "golang.org/x/crypto/bcrypt" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -// User 用户模型 -type User struct { - ID uint `gorm:"primaryKey" json:"id"` - Username string `gorm:"size:50;unique;not null" json:"username"` - Email string `gorm:"size:100;unique;not null" json:"email"` - Password string `gorm:"size:255;not null" json:"-"` - Name string `gorm:"size:100" json:"name"` - Avatar string `gorm:"size:500" json:"avatar"` - Role string `gorm:"size:20;default:user" json:"role"` - IsActive bool `gorm:"default:true" json:"is_active"` - LastLogin *time.Time `json:"last_login"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` -} - -// Category 分类模型 -type Category struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:100;not null" json:"name"` - Slug string `gorm:"size:100;unique;not null" json:"slug"` - Description string `gorm:"type:text" json:"description"` - ParentID *uint `json:"parent_id"` - Color string `gorm:"size:7;default:#3b82f6" json:"color"` - CoverImage string `gorm:"size:500" json:"cover_image"` - Sort int `gorm:"default:0" json:"sort"` - IsActive bool `gorm:"default:true" json:"is_active"` - PhotoCount int `gorm:"-" json:"photo_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` -} - -// Tag 标签模型 -type Tag struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:50;unique;not null" json:"name"` - Slug string `gorm:"size:50;unique;not null" json:"slug"` - Description string `gorm:"type:text" json:"description"` - Color string `gorm:"size:7;default:#10b981" json:"color"` - PhotoCount int `gorm:"-" json:"photo_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` -} - -// Photo 照片模型 -type Photo struct { - ID uint `gorm:"primaryKey" json:"id"` - Title string `gorm:"size:200;not null" json:"title"` - Description string `gorm:"type:text" json:"description"` - URL string `gorm:"size:500;not null" json:"url"` - ThumbnailURL string `gorm:"size:500" json:"thumbnail_url"` - OriginalFilename string `gorm:"size:255" json:"original_filename"` - FileSize int64 `json:"file_size"` - MimeType string `gorm:"size:100" json:"mime_type"` - Width int `json:"width"` - Height int `json:"height"` - Status string `gorm:"size:20;default:draft" json:"status"` - UserID uint `json:"user_id"` - User User `gorm:"foreignKey:UserID" json:"user,omitempty"` - Categories []Category `gorm:"many2many:photo_categories;" json:"categories,omitempty"` - Tags []Tag `gorm:"many2many:photo_tags;" json:"tags,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` -} - -// LoginRequest 登录请求 -type LoginRequest struct { - Username string `json:"username" binding:"required"` - Password string `json:"password" binding:"required"` -} - -// LoginResponse 登录响应 -type LoginResponse struct { - User User `json:"user"` - AccessToken string `json:"access_token"` - ExpiresIn int64 `json:"expires_in"` -} - -var db *gorm.DB - -func main() { - // 初始化数据库 - var err error - db, err = gorm.Open(sqlite.Open("photography.db"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), - }) - if err != nil { - log.Fatalf("Failed to connect to database: %v", err) - } - - // 自动迁移 - err = db.AutoMigrate(&User{}, &Category{}, &Tag{}, &Photo{}) - if err != nil { - log.Fatalf("Failed to migrate database: %v", err) - } - - // 初始化测试数据 - initTestData() - - // 创建 Gin 引擎 - r := gin.Default() - - // 配置 CORS - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"http://localhost:3000", "http://localhost:3002", "http://localhost:3003"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - AllowCredentials: true, - MaxAge: 12 * time.Hour, - })) - - // 静态文件服务 - r.Static("/uploads", "./uploads") - - // 健康检查 - r.GET("/health", healthHandler) - - // API 路由组 - v1 := r.Group("/api/v1") - { - // 认证相关 - auth := v1.Group("/auth") - { - auth.POST("/login", loginHandler) - } - - // 照片相关 - v1.GET("/photos", getPhotosHandler) - v1.POST("/photos", createPhotoHandler) - v1.GET("/photos/:id", getPhotoHandler) - v1.PUT("/photos/:id", updatePhotoHandler) - v1.DELETE("/photos/:id", deletePhotoHandler) - - // 文件上传 - v1.POST("/upload", uploadFileHandler) - - // 分类相关 - v1.GET("/categories", getCategoriesHandler) - v1.POST("/categories", createCategoryHandler) - v1.GET("/categories/:id", getCategoryHandler) - v1.PUT("/categories/:id", updateCategoryHandler) - v1.DELETE("/categories/:id", deleteCategoryHandler) - - // 标签相关 - v1.GET("/tags", getTagsHandler) - v1.POST("/tags", createTagHandler) - v1.GET("/tags/:id", getTagHandler) - v1.PUT("/tags/:id", updateTagHandler) - v1.DELETE("/tags/:id", deleteTagHandler) - - // 用户相关 - v1.GET("/users", getUsersHandler) - v1.POST("/users", createUserHandler) - v1.GET("/users/:id", getUserHandler) - v1.PUT("/users/:id", updateUserHandler) - v1.DELETE("/users/:id", deleteUserHandler) - - // 仪表板统计 - v1.GET("/dashboard/stats", getDashboardStatsHandler) - } - - // 启动服务器 - log.Println("🚀 Backend server with SQLite database starting on :8080") - log.Fatal(r.Run(":8080")) -} - -// 初始化测试数据 -func initTestData() { - // 检查是否已有管理员用户 - var count int64 - db.Model(&User{}).Where("role = ?", "admin").Count(&count) - if count > 0 { - return // 已有管理员用户,跳过初始化 - } - - // 创建管理员用户 - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) - admin := User{ - Username: "admin", - Email: "admin@photography.com", - Password: string(hashedPassword), - Name: "管理员", - Role: "admin", - IsActive: true, - } - db.Create(&admin) - - // 创建编辑用户 - hashedPassword, _ = bcrypt.GenerateFromPassword([]byte("editor123"), bcrypt.DefaultCost) - editor := User{ - Username: "editor", - Email: "editor@photography.com", - Password: string(hashedPassword), - Name: "编辑员", - Role: "editor", - IsActive: true, - } - db.Create(&editor) - - // 创建分类 - categories := []Category{ - {Name: "风景", Slug: "landscape", Description: "自然风景摄影", Color: "#059669"}, - {Name: "人像", Slug: "portrait", Description: "人物肖像摄影", Color: "#dc2626"}, - {Name: "街拍", Slug: "street", Description: "街头摄影", Color: "#7c3aed"}, - {Name: "建筑", Slug: "architecture", Description: "建筑摄影", Color: "#ea580c"}, - {Name: "动物", Slug: "animal", Description: "动物摄影", Color: "#0891b2"}, - } - for _, category := range categories { - db.Create(&category) - } - - // 创建标签 - tags := []Tag{ - {Name: "日落", Slug: "sunset", Color: "#f59e0b"}, - {Name: "城市", Slug: "city", Color: "#6b7280"}, - {Name: "自然", Slug: "nature", Color: "#10b981"}, - {Name: "黑白", Slug: "black-white", Color: "#374151"}, - {Name: "夜景", Slug: "night", Color: "#1e40af"}, - {Name: "微距", Slug: "macro", Color: "#7c2d12"}, - {Name: "旅行", Slug: "travel", Color: "#be185d"}, - {Name: "艺术", Slug: "art", Color: "#9333ea"}, - } - for _, tag := range tags { - db.Create(&tag) - } - - // 创建示例照片 - photos := []Photo{ - { - Title: "金色日落", - Description: "美丽的金色日落景象", - URL: "https://picsum.photos/800/600?random=1", - ThumbnailURL: "https://picsum.photos/300/200?random=1", - OriginalFilename: "sunset.jpg", - FileSize: 1024000, - MimeType: "image/jpeg", - Width: 800, - Height: 600, - Status: "published", - UserID: admin.ID, - }, - { - Title: "城市夜景", - Description: "繁华的城市夜晚", - URL: "https://picsum.photos/800/600?random=2", - ThumbnailURL: "https://picsum.photos/300/200?random=2", - OriginalFilename: "citynight.jpg", - FileSize: 2048000, - MimeType: "image/jpeg", - Width: 800, - Height: 600, - Status: "published", - UserID: admin.ID, - }, - { - Title: "人像写真", - Description: "优雅的人像摄影", - URL: "https://picsum.photos/600/800?random=3", - ThumbnailURL: "https://picsum.photos/300/400?random=3", - OriginalFilename: "portrait.jpg", - FileSize: 1536000, - MimeType: "image/jpeg", - Width: 600, - Height: 800, - Status: "published", - UserID: editor.ID, - }, - { - Title: "建筑之美", - Description: "现代建筑的几何美学", - URL: "https://picsum.photos/800/600?random=4", - ThumbnailURL: "https://picsum.photos/300/200?random=4", - OriginalFilename: "architecture.jpg", - FileSize: 1792000, - MimeType: "image/jpeg", - Width: 800, - Height: 600, - Status: "draft", - UserID: editor.ID, - }, - } - - for _, photo := range photos { - db.Create(&photo) - } - - log.Println("✅ Test data initialized successfully") -} - -// 健康检查处理器 -func healthHandler(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "timestamp": time.Now().Unix(), - "version": "1.0.0", - "database": "connected", - }) -} - -// 登录处理器 -func loginHandler(c *gin.Context) { - var req LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - var user User - if err := db.Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) - return - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) - return - } - - // 更新最后登录时间 - now := time.Now() - user.LastLogin = &now - db.Save(&user) - - c.JSON(http.StatusOK, LoginResponse{ - User: user, - AccessToken: "mock-jwt-token-" + fmt.Sprintf("%d", user.ID), - ExpiresIn: 86400, - }) -} - -// 照片相关处理器 -func getPhotosHandler(c *gin.Context) { - var photos []Photo - query := db.Preload("User").Preload("Categories").Preload("Tags") - - // 分页参数 - page := 1 - limit := 10 - if p := c.Query("page"); p != "" { - fmt.Sscanf(p, "%d", &page) - } - if l := c.Query("limit"); l != "" { - fmt.Sscanf(l, "%d", &limit) - } - - offset := (page - 1) * limit - - // 搜索和过滤 - if search := c.Query("search"); search != "" { - query = query.Where("title LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%") - } - if status := c.Query("status"); status != "" { - query = query.Where("status = ?", status) - } - - var total int64 - query.Model(&Photo{}).Count(&total) - query.Offset(offset).Limit(limit).Find(&photos) - - c.JSON(http.StatusOK, gin.H{ - "data": photos, - "total": total, - "page": page, - "limit": limit, - "totalPages": (total + int64(limit) - 1) / int64(limit), - }) -} - -func createPhotoHandler(c *gin.Context) { - var photo Photo - if err := c.ShouldBindJSON(&photo); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Create(&photo).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建照片失败"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": photo}) -} - -func getPhotoHandler(c *gin.Context) { - id := c.Param("id") - var photo Photo - if err := db.Preload("User").Preload("Categories").Preload("Tags").First(&photo, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"}) - return - } - c.JSON(http.StatusOK, gin.H{"data": photo}) -} - -func updatePhotoHandler(c *gin.Context) { - id := c.Param("id") - var photo Photo - if err := db.First(&photo, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"}) - return - } - - if err := c.ShouldBindJSON(&photo); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&photo).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新照片失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": photo}) -} - -func deletePhotoHandler(c *gin.Context) { - id := c.Param("id") - if err := db.Delete(&Photo{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除照片失败"}) - return - } - c.JSON(http.StatusNoContent, nil) -} - -// 分类相关处理器 -func getCategoriesHandler(c *gin.Context) { - var categories []Category - var total int64 - - query := db.Model(&Category{}) - query.Count(&total) - query.Find(&categories) - - // 计算每个分类的照片数量 - for i := range categories { - var count int64 - db.Model(&Photo{}).Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). - Where("photo_categories.category_id = ?", categories[i].ID).Count(&count) - categories[i].PhotoCount = int(count) - } - - c.JSON(http.StatusOK, gin.H{ - "data": categories, - "total": total, - }) -} - -func createCategoryHandler(c *gin.Context) { - var category Category - if err := c.ShouldBindJSON(&category); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Create(&category).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建分类失败"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": category}) -} - -func getCategoryHandler(c *gin.Context) { - id := c.Param("id") - var category Category - if err := db.First(&category, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"}) - return - } - c.JSON(http.StatusOK, gin.H{"data": category}) -} - -func updateCategoryHandler(c *gin.Context) { - id := c.Param("id") - var category Category - if err := db.First(&category, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"}) - return - } - - if err := c.ShouldBindJSON(&category); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&category).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新分类失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": category}) -} - -func deleteCategoryHandler(c *gin.Context) { - id := c.Param("id") - if err := db.Delete(&Category{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除分类失败"}) - return - } - c.JSON(http.StatusNoContent, nil) -} - -// 标签相关处理器 -func getTagsHandler(c *gin.Context) { - var tags []Tag - var total int64 - - query := db.Model(&Tag{}) - query.Count(&total) - query.Find(&tags) - - // 计算每个标签的照片数量 - for i := range tags { - var count int64 - db.Model(&Photo{}).Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). - Where("photo_tags.tag_id = ?", tags[i].ID).Count(&count) - tags[i].PhotoCount = int(count) - } - - c.JSON(http.StatusOK, gin.H{ - "data": tags, - "total": total, - }) -} - -func createTagHandler(c *gin.Context) { - var tag Tag - if err := c.ShouldBindJSON(&tag); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Create(&tag).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建标签失败"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": tag}) -} - -func getTagHandler(c *gin.Context) { - id := c.Param("id") - var tag Tag - if err := db.First(&tag, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"}) - return - } - c.JSON(http.StatusOK, gin.H{"data": tag}) -} - -func updateTagHandler(c *gin.Context) { - id := c.Param("id") - var tag Tag - if err := db.First(&tag, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"}) - return - } - - if err := c.ShouldBindJSON(&tag); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&tag).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新标签失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": tag}) -} - -func deleteTagHandler(c *gin.Context) { - id := c.Param("id") - if err := db.Delete(&Tag{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除标签失败"}) - return - } - c.JSON(http.StatusNoContent, nil) -} - -// 用户相关处理器 -func getUsersHandler(c *gin.Context) { - var users []User - var total int64 - - query := db.Model(&User{}) - query.Count(&total) - query.Find(&users) - - c.JSON(http.StatusOK, gin.H{ - "data": users, - "total": total, - }) -} - -func createUserHandler(c *gin.Context) { - var user User - if err := c.ShouldBindJSON(&user); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) - return - } - user.Password = string(hashedPassword) - - if err := db.Create(&user).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": user}) -} - -func getUserHandler(c *gin.Context) { - id := c.Param("id") - var user User - if err := db.First(&user, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) - return - } - c.JSON(http.StatusOK, gin.H{"data": user}) -} - -func updateUserHandler(c *gin.Context) { - id := c.Param("id") - var user User - if err := db.First(&user, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) - return - } - - if err := c.ShouldBindJSON(&user); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := db.Save(&user).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": user}) -} - -func deleteUserHandler(c *gin.Context) { - id := c.Param("id") - if err := db.Delete(&User{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"}) - return - } - c.JSON(http.StatusNoContent, nil) -} - -// 仪表板统计处理器 -func getDashboardStatsHandler(c *gin.Context) { - var photoCount, categoryCount, tagCount, userCount int64 - var publishedCount, draftCount int64 - var todayCount, monthCount int64 - - // 基础统计 - db.Model(&Photo{}).Count(&photoCount) - db.Model(&Category{}).Count(&categoryCount) - db.Model(&Tag{}).Count(&tagCount) - db.Model(&User{}).Count(&userCount) - - // 照片状态统计 - db.Model(&Photo{}).Where("status = ?", "published").Count(&publishedCount) - db.Model(&Photo{}).Where("status = ?", "draft").Count(&draftCount) - - // 时间统计 - today := time.Now().Format("2006-01-02") - thisMonth := time.Now().Format("2006-01") - - db.Model(&Photo{}).Where("DATE(created_at) = ?", today).Count(&todayCount) - db.Model(&Photo{}).Where("strftime('%Y-%m', created_at) = ?", thisMonth).Count(&monthCount) - - c.JSON(http.StatusOK, gin.H{ - "photos": gin.H{ - "total": photoCount, - "published": publishedCount, - "draft": draftCount, - "thisMonth": monthCount, - "today": todayCount, - }, - "categories": gin.H{ - "total": categoryCount, - "active": categoryCount, // 简化统计 - }, - "tags": gin.H{ - "total": tagCount, - }, - "users": gin.H{ - "total": userCount, - "active": userCount, // 简化统计 - }, - }) -} - -// 文件上传处理器 -func uploadFileHandler(c *gin.Context) { - // 创建上传目录 - uploadDir := "uploads" - if err := os.MkdirAll(uploadDir, 0755); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败"}) - return - } - - // 获取上传的文件 - file, header, err := c.Request.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "获取上传文件失败"}) - return - } - defer file.Close() - - // 验证文件类型 - allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} - ext := strings.ToLower(filepath.Ext(header.Filename)) - isAllowed := false - for _, allowedType := range allowedTypes { - if ext == allowedType { - isAllowed = true - break - } - } - if !isAllowed { - c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"}) - return - } - - // 生成文件名 - filename := fmt.Sprintf("%d_%s", time.Now().Unix(), header.Filename) - filePath := filepath.Join(uploadDir, filename) - - // 保存文件 - out, err := os.Create(filePath) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文件失败"}) - return - } - defer out.Close() - - // 复制文件内容 - _, err = io.Copy(out, file) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"}) - return - } - - // 返回文件信息 - fileURL := fmt.Sprintf("http://localhost:8080/uploads/%s", filename) - c.JSON(http.StatusOK, gin.H{ - "message": "文件上传成功", - "filename": filename, - "url": fileURL, - "size": header.Size, - "type": header.Header.Get("Content-Type"), - }) -} \ No newline at end of file diff --git a/backend-old/cmd/server/simple_main.go b/backend-old/cmd/server/simple_main.go deleted file mode 100644 index 4e1e989..0000000 --- a/backend-old/cmd/server/simple_main.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "log" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/gin-contrib/cors" -) - -func main() { - // 创建 Gin 引擎 - r := gin.Default() - - // 配置 CORS - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"http://localhost:3000", "http://localhost:3002", "http://localhost:3003"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - AllowCredentials: true, - MaxAge: 12 * time.Hour, - })) - - // 健康检查 - r.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "timestamp": time.Now().Unix(), - "version": "1.0.0", - }) - }) - - // 模拟登录接口 - r.POST("/api/v1/auth/login", func(c *gin.Context) { - var req struct { - Username string `json:"username"` - Password string `json:"password"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 简单验证 - if req.Username == "admin" && req.Password == "admin123" { - c.JSON(http.StatusOK, gin.H{ - "user": gin.H{ - "id": "1", - "username": "admin", - "email": "admin@example.com", - "role": "admin", - "isActive": true, - }, - "access_token": "mock-jwt-token", - "expires_in": 86400, - }) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) - } - }) - - // 模拟照片列表接口 - r.GET("/api/v1/photos", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "data": []gin.H{ - { - "id": "1", - "title": "Sample Photo 1", - "description": "This is a sample photo", - "url": "https://picsum.photos/800/600?random=1", - "status": "published", - "createdAt": time.Now().Format(time.RFC3339), - }, - { - "id": "2", - "title": "Sample Photo 2", - "description": "This is another sample photo", - "url": "https://picsum.photos/800/600?random=2", - "status": "published", - "createdAt": time.Now().Format(time.RFC3339), - }, - }, - "total": 2, - "page": 1, - "limit": 10, - "totalPages": 1, - }) - }) - - // 模拟仪表板统计接口 - r.GET("/api/v1/dashboard/stats", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "photos": gin.H{ - "total": 156, - "thisMonth": 23, - "today": 5, - }, - "categories": gin.H{ - "total": 12, - "active": 10, - }, - "tags": gin.H{ - "total": 45, - }, - "users": gin.H{ - "total": 8, - "active": 7, - }, - }) - }) - - // 模拟分类列表接口 - r.GET("/api/v1/categories", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "data": []gin.H{ - { - "id": "1", - "name": "风景", - "slug": "landscape", - "description": "自然风景摄影", - "photoCount": 25, - "isActive": true, - }, - { - "id": "2", - "name": "人像", - "slug": "portrait", - "description": "人物肖像摄影", - "photoCount": 18, - "isActive": true, - }, - }, - "total": 2, - }) - }) - - // 模拟标签列表接口 - r.GET("/api/v1/tags", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "data": []gin.H{ - { - "id": "1", - "name": "日落", - "slug": "sunset", - "photoCount": 12, - }, - { - "id": "2", - "name": "城市", - "slug": "city", - "photoCount": 8, - }, - }, - "total": 2, - }) - }) - - // 模拟用户列表接口 - r.GET("/api/v1/users", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "data": []gin.H{ - { - "id": "1", - "username": "admin", - "email": "admin@example.com", - "role": "admin", - "isActive": true, - }, - { - "id": "2", - "username": "editor", - "email": "editor@example.com", - "role": "editor", - "isActive": true, - }, - }, - "total": 2, - }) - }) - - // 启动服务器 - log.Println("🚀 Simple backend server starting on :8080") - log.Fatal(r.Run(":8080")) -} \ No newline at end of file diff --git a/backend-old/configs/config.dev.yaml b/backend-old/configs/config.dev.yaml deleted file mode 100644 index 37fbcf8..0000000 --- a/backend-old/configs/config.dev.yaml +++ /dev/null @@ -1,105 +0,0 @@ -# 开发环境配置 -app: - name: "Photography Portfolio Dev" - version: "1.0.0" - environment: "development" - port: 8080 - debug: true - -# 数据库配置 -database: - host: "localhost" - port: 5432 - username: "postgres" - password: "password" - database: "photography_dev" - ssl_mode: "disable" - max_open_conns: 50 - max_idle_conns: 5 - conn_max_lifetime: 300 - -# Redis配置 -redis: - host: "localhost" - port: 6379 - password: "" - database: 1 - pool_size: 10 - min_idle_conns: 2 - -# JWT配置 -jwt: - secret: "dev-secret-key-not-for-production" - expires_in: "24h" - refresh_expires_in: "168h" - -# 存储配置 -storage: - type: "local" - local: - base_path: "./uploads" - base_url: "http://localhost:8080/uploads" - s3: - region: "us-east-1" - bucket: "photography-dev-bucket" - access_key: "" - secret_key: "" - endpoint: "" - -# 上传配置 -upload: - max_file_size: 104857600 # 100MB - allowed_types: - - "image/jpeg" - - "image/jpg" - - "image/png" - - "image/gif" - - "image/webp" - - "image/tiff" - - "image/bmp" - thumbnail_sizes: - - name: "thumbnail" - width: 200 - height: 200 - - name: "medium" - width: 800 - height: 600 - - name: "large" - width: 1920 - height: 1080 - -# 日志配置 -logger: - level: "debug" - format: "text" - output: "stdout" - filename: "logs/dev.log" - max_size: 50 - max_age: 7 - compress: false - -# CORS配置 -cors: - allowed_origins: - - "http://localhost:3000" - - "http://localhost:3001" - - "http://localhost:5173" - allowed_methods: - - "GET" - - "POST" - - "PUT" - - "DELETE" - - "PATCH" - - "OPTIONS" - allowed_headers: - - "Content-Type" - - "Authorization" - - "X-Requested-With" - - "Origin" - allow_credentials: true - -# 限流配置 -rate_limit: - enabled: false - requests_per_minute: 1000 - burst: 2000 \ No newline at end of file diff --git a/backend-old/configs/config.prod.yaml b/backend-old/configs/config.prod.yaml deleted file mode 100644 index 1718d1f..0000000 --- a/backend-old/configs/config.prod.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# 生产环境配置 -app: - name: "Photography Portfolio" - version: "1.0.0" - environment: "production" - port: 8080 - debug: false - -# 数据库配置 -database: - host: "localhost" - port: 5432 - username: "postgres" - password: "${DB_PASSWORD}" - database: "photography" - ssl_mode: "require" - max_open_conns: 200 - max_idle_conns: 20 - conn_max_lifetime: 3600 - -# Redis配置 -redis: - host: "localhost" - port: 6379 - password: "${REDIS_PASSWORD}" - database: 0 - pool_size: 50 - min_idle_conns: 10 - -# JWT配置 -jwt: - secret: "${JWT_SECRET}" - expires_in: "24h" - refresh_expires_in: "168h" - -# 存储配置 -storage: - type: "local" # 生产环境建议使用 s3 或 oss - local: - base_path: "/var/www/photography/uploads" - base_url: "https://photography.iriver.top/uploads" - s3: - region: "${AWS_REGION}" - bucket: "${AWS_BUCKET}" - access_key: "${AWS_ACCESS_KEY_ID}" - secret_key: "${AWS_SECRET_ACCESS_KEY}" - endpoint: "${AWS_ENDPOINT}" - -# 上传配置 -upload: - max_file_size: 104857600 # 100MB - allowed_types: - - "image/jpeg" - - "image/jpg" - - "image/png" - - "image/gif" - - "image/webp" - - "image/tiff" - thumbnail_sizes: - - name: "thumbnail" - width: 200 - height: 200 - - name: "medium" - width: 800 - height: 600 - - name: "large" - width: 1920 - height: 1080 - -# 日志配置 -logger: - level: "info" - format: "json" - output: "file" - filename: "/var/log/photography/app.log" - max_size: 100 - max_age: 30 - compress: true - -# CORS配置 -cors: - allowed_origins: - - "https://photography.iriver.top" - - "https://admin.photography.iriver.top" - allowed_methods: - - "GET" - - "POST" - - "PUT" - - "DELETE" - - "OPTIONS" - allowed_headers: - - "Content-Type" - - "Authorization" - - "X-Requested-With" - allow_credentials: true - -# 限流配置 -rate_limit: - enabled: true - requests_per_minute: 60 - burst: 120 \ No newline at end of file diff --git a/backend-old/configs/config.yaml b/backend-old/configs/config.yaml deleted file mode 100644 index a22f272..0000000 --- a/backend-old/configs/config.yaml +++ /dev/null @@ -1,76 +0,0 @@ -app: - name: "photography-backend" - version: "1.0.0" - environment: "development" - port: 8080 - debug: true - -database: - host: "localhost" - port: 5432 - username: "postgres" - password: "password" - database: "photography" - ssl_mode: "disable" - max_open_conns: 100 - max_idle_conns: 10 - conn_max_lifetime: 300 - -redis: - host: "localhost" - port: 6379 - password: "" - database: 0 - pool_size: 100 - min_idle_conns: 10 - -jwt: - secret: "your-secret-key-change-in-production" - expires_in: "24h" - refresh_expires_in: "168h" - -storage: - type: "local" # local, s3, minio - local: - base_path: "./uploads" - base_url: "http://localhost:8080/uploads" - s3: - region: "us-east-1" - bucket: "photography-uploads" - access_key: "" - secret_key: "" - endpoint: "" - -upload: - max_file_size: 52428800 # 50MB - allowed_types: ["image/jpeg", "image/png", "image/gif", "image/webp", "image/tiff"] - thumbnail_sizes: - - name: "small" - width: 300 - height: 300 - - name: "medium" - width: 800 - height: 600 - - name: "large" - width: 1200 - height: 900 - -logger: - level: "debug" - format: "json" - output: "file" - filename: "logs/app.log" - max_size: 100 - max_age: 7 - compress: true - -cors: - allowed_origins: ["http://localhost:3000", "https://photography.iriver.top"] - allowed_methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] - allowed_headers: ["Origin", "Content-Length", "Content-Type", "Authorization"] - allow_credentials: true - -rate_limit: - enabled: true - requests_per_minute: 100 - burst: 50 \ No newline at end of file diff --git a/backend-old/docker-compose.dev.yml b/backend-old/docker-compose.dev.yml deleted file mode 100644 index 9d156e4..0000000 --- a/backend-old/docker-compose.dev.yml +++ /dev/null @@ -1,123 +0,0 @@ -version: '3.8' - -services: - # PostgreSQL 数据库 (开发环境) - postgres: - image: postgres:15-alpine - container_name: photography_postgres_dev - environment: - POSTGRES_DB: photography_dev - POSTGRES_USER: postgres - POSTGRES_PASSWORD: dev_password - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" - ports: - - "5432:5432" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./migrations:/docker-entrypoint-initdb.d - networks: - - photography_dev_network - restart: unless-stopped - - # Redis 缓存 (开发环境) - redis: - image: redis:7-alpine - container_name: photography_redis_dev - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - networks: - - photography_dev_network - restart: unless-stopped - command: redis-server --appendonly yes - - # 后端 API 服务 (开发环境) - backend: - build: - context: . - dockerfile: Dockerfile - target: builder - container_name: photography_backend_dev - environment: - # 数据库配置 - DB_HOST: postgres - DB_PORT: 5432 - DB_USER: postgres - DB_PASSWORD: dev_password - DB_NAME: photography_dev - DB_SSL_MODE: disable - - # Redis 配置 - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: "" - REDIS_DB: 0 - - # JWT 配置 - JWT_SECRET: dev-jwt-secret-key - JWT_EXPIRES_IN: 24h - - # 服务器配置 - PORT: 8080 - GIN_MODE: debug - - # 文件上传配置 - UPLOAD_TYPE: local - UPLOAD_PATH: /app/uploads - UPLOAD_BASE_URL: http://localhost:8080/uploads - UPLOAD_MAX_SIZE: 10485760 # 10MB - - # 日志配置 - LOG_LEVEL: debug - LOG_FORMAT: console - ports: - - "8080:8080" - volumes: - - .:/app - - upload_dev_data:/app/uploads - - go_mod_cache:/go/pkg/mod - networks: - - photography_dev_network - depends_on: - - postgres - - redis - restart: unless-stopped - command: go run cmd/server/main.go - - # 管理后台前端 (开发环境) - admin: - build: - context: ../admin - dockerfile: Dockerfile.dev - container_name: photography_admin_dev - environment: - VITE_API_BASE_URL: http://localhost:8080/api - ports: - - "3000:3000" - volumes: - - ../admin:/app - - /app/node_modules - networks: - - photography_dev_network - depends_on: - - backend - restart: unless-stopped - profiles: - - admin - -# 数据卷 -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - upload_dev_data: - driver: local - go_mod_cache: - driver: local - -# 网络 -networks: - photography_dev_network: - driver: bridge \ No newline at end of file diff --git a/backend-old/docker-compose.yml b/backend-old/docker-compose.yml deleted file mode 100644 index d15ca83..0000000 --- a/backend-old/docker-compose.yml +++ /dev/null @@ -1,71 +0,0 @@ -version: '3.8' - -services: - # 后端 API 服务 - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: photography_backend - restart: unless-stopped - environment: - # 数据库配置 (连接到现有的 PostgreSQL) - DB_HOST: ${DB_HOST:-localhost} - DB_PORT: ${DB_PORT:-5432} - DB_NAME: ${DB_NAME:-photography} - DB_USER: ${DB_USER:-postgres} - DB_PASSWORD: ${DB_PASSWORD} - - # Redis 配置 (连接到现有的 Redis) - REDIS_HOST: ${REDIS_HOST:-localhost} - REDIS_PORT: ${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" - # 使用 host 网络模式以便访问宿主机的服务 - network_mode: host - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - - # 数据库备份服务 (可选,如果你想使用容器化备份) - backup: - image: postgres:16-alpine - container_name: photography_backup - restart: "no" - environment: - PGPASSWORD: ${DB_PASSWORD} - DB_HOST: ${DB_HOST:-localhost} - DB_USER: ${DB_USER:-postgres} - DB_NAME: ${DB_NAME:-photography} - volumes: - - ./backups:/backups - - ./scripts/backup.sh:/backup.sh - network_mode: host - entrypoint: ["/backup.sh"] - profiles: - - backup # 使用 profile 使这个服务可选 \ No newline at end of file diff --git a/backend-old/docs/CLAUDE.md b/backend-old/docs/CLAUDE.md deleted file mode 100644 index 327b67b..0000000 --- a/backend-old/docs/CLAUDE.md +++ /dev/null @@ -1,955 +0,0 @@ -# Documentation Module - CLAUDE.md - -本文件为 Claude Code 在文档模块中工作时提供指导。 - -## 🎯 模块概览 - -文档模块负责维护项目的技术文档、API 文档和架构设计文档,确保项目的可理解性和可维护性。 - -### 主要职责 -- 📚 API 接口文档 -- 🏗️ 架构设计文档 -- 🚀 部署运维文档 -- 📖 开发者指南 -- 📋 变更日志 - -## 📁 模块结构 - -``` -docs/ -├── CLAUDE.md # 📋 当前文件 - API 文档和接口设计指导 -├── api/ # 📚 API 接口文档 -│ ├── openapi.yaml # OpenAPI 3.0 规范 -│ ├── swagger.json # Swagger JSON 格式 -│ ├── postman/ # Postman 集合 -│ │ ├── photography.json # API 测试集合 -│ │ └── environment.json # 环境变量 -│ └── examples/ # 请求响应示例 -│ ├── user_examples.md # 用户接口示例 -│ ├── photo_examples.md # 照片接口示例 -│ └── auth_examples.md # 认证接口示例 -├── architecture/ # 🏗️ 架构设计文档 -│ ├── overview.md # 系统架构概览 -│ ├── database_design.md # 数据库设计 -│ ├── api_design.md # API 设计原则 -│ ├── security.md # 安全架构 -│ └── performance.md # 性能优化 -├── development/ # 👨‍💻 开发文档 -│ ├── getting_started.md # 快速开始 -│ ├── coding_standards.md # 编码规范 -│ ├── testing_guide.md # 测试指南 -│ ├── debugging.md # 调试指南 -│ └── contribution.md # 贡献指南 -├── deployment/ # 🚀 部署文档 -│ ├── local_deployment.md # 本地部署 -│ ├── docker_deployment.md # Docker 部署 -│ ├── production_deployment.md # 生产环境部署 -│ ├── monitoring.md # 监控配置 -│ └── troubleshooting.md # 故障排查 -├── guides/ # 📖 使用指南 -│ ├── user_management.md # 用户管理 -│ ├── photo_management.md # 照片管理 -│ ├── authentication.md # 认证授权 -│ └── file_upload.md # 文件上传 -├── changelog/ # 📋 变更日志 -│ ├── CHANGELOG.md # 版本变更记录 -│ └── migration_guides/ # 迁移指南 -│ ├── v1_to_v2.md # 版本迁移 -│ └── breaking_changes.md # 破坏性变更 -└── templates/ # 📄 文档模板 - ├── api_template.md # API 文档模板 - ├── guide_template.md # 指南模板 - └── adr_template.md # 架构决策记录模板 -``` - -## 📚 API 文档 - -### OpenAPI 3.0 规范 -```yaml -# api/openapi.yaml - OpenAPI 3.0 规范 -openapi: 3.0.3 -info: - title: Photography Portfolio API - description: 摄影作品集 REST API - version: 1.0.0 - contact: - name: API Support - email: support@photography.com - license: - name: MIT - url: https://opensource.org/licenses/MIT - -servers: - - url: https://api.photography.com/v1 - description: 生产环境 - - url: https://staging-api.photography.com/v1 - description: 测试环境 - - url: http://localhost:8080/api/v1 - description: 开发环境 - -paths: - /users: - get: - summary: 获取用户列表 - description: 获取系统中的用户列表,支持分页和过滤 - operationId: listUsers - tags: - - Users - parameters: - - name: page - in: query - description: 页码 - required: false - schema: - type: integer - minimum: 1 - default: 1 - - name: limit - in: query - description: 每页数量 - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: sort - in: query - description: 排序字段 - required: false - schema: - type: string - enum: [id, username, email, created_at] - default: created_at - - name: order - in: query - description: 排序方向 - required: false - schema: - type: string - enum: [asc, desc] - default: desc - responses: - '200': - description: 成功获取用户列表 - content: - application/json: - schema: - $ref: '#/components/schemas/UserListResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: 服务器内部错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - security: - - BearerAuth: [] - - post: - summary: 创建用户 - description: 创建新用户账户 - operationId: createUser - tags: - - Users - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserRequest' - responses: - '201': - description: 用户创建成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationErrorResponse' - '409': - description: 用户已存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /users/{id}: - get: - summary: 获取用户详情 - description: 根据用户ID获取用户详细信息 - operationId: getUserById - tags: - - Users - parameters: - - name: id - in: path - description: 用户ID - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: 成功获取用户信息 - content: - application/json: - schema: - $ref: '#/components/schemas/UserResponse' - '404': - description: 用户不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - security: - - BearerAuth: [] - -components: - schemas: - User: - type: object - properties: - id: - type: integer - format: int64 - description: 用户ID - example: 1 - username: - type: string - description: 用户名 - example: "john_doe" - email: - type: string - format: email - description: 邮箱地址 - example: "john@example.com" - first_name: - type: string - description: 名字 - example: "John" - last_name: - type: string - description: 姓氏 - example: "Doe" - avatar: - type: string - format: uri - description: 头像URL - example: "https://example.com/avatars/john.jpg" - role: - type: string - enum: [admin, editor, user] - description: 用户角色 - example: "user" - status: - type: string - enum: [active, inactive, banned] - description: 用户状态 - example: "active" - created_at: - type: string - format: date-time - description: 创建时间 - example: "2024-01-01T00:00:00Z" - updated_at: - type: string - format: date-time - description: 更新时间 - example: "2024-01-01T00:00:00Z" - required: - - id - - username - - email - - role - - status - - created_at - - updated_at - - CreateUserRequest: - type: object - properties: - username: - type: string - minLength: 3 - maxLength: 50 - pattern: '^[a-zA-Z0-9_-]+$' - description: 用户名 - example: "john_doe" - email: - type: string - format: email - description: 邮箱地址 - example: "john@example.com" - password: - type: string - minLength: 8 - description: 密码 - example: "SecurePassword123!" - first_name: - type: string - maxLength: 50 - description: 名字 - example: "John" - last_name: - type: string - maxLength: 50 - description: 姓氏 - example: "Doe" - required: - - username - - email - - password - - UserResponse: - type: object - properties: - success: - type: boolean - example: true - data: - $ref: '#/components/schemas/User' - message: - type: string - example: "User retrieved successfully" - timestamp: - type: integer - format: int64 - example: 1704067200 - request_id: - type: string - example: "req_123456789" - - UserListResponse: - type: object - properties: - success: - type: boolean - example: true - data: - type: array - items: - $ref: '#/components/schemas/User' - pagination: - $ref: '#/components/schemas/Pagination' - message: - type: string - example: "Users retrieved successfully" - timestamp: - type: integer - format: int64 - example: 1704067200 - request_id: - type: string - example: "req_123456789" - - Pagination: - type: object - properties: - page: - type: integer - example: 1 - limit: - type: integer - example: 20 - total: - type: integer - format: int64 - example: 100 - total_pages: - type: integer - example: 5 - has_next: - type: boolean - example: true - has_prev: - type: boolean - example: false - - ErrorResponse: - type: object - properties: - success: - type: boolean - example: false - error: - $ref: '#/components/schemas/Error' - timestamp: - type: integer - format: int64 - example: 1704067200 - request_id: - type: string - example: "req_123456789" - - Error: - type: object - properties: - code: - type: string - example: "USER_NOT_FOUND" - message: - type: string - example: "User not found" - details: - type: string - example: "User with ID 123 does not exist" - - ValidationErrorResponse: - type: object - properties: - success: - type: boolean - example: false - error: - type: object - properties: - code: - type: string - example: "VALIDATION_ERROR" - message: - type: string - example: "Validation failed" - details: - type: array - items: - $ref: '#/components/schemas/ValidationError' - timestamp: - type: integer - format: int64 - example: 1704067200 - request_id: - type: string - example: "req_123456789" - - ValidationError: - type: object - properties: - field: - type: string - example: "email" - value: - type: string - example: "invalid-email" - tag: - type: string - example: "email" - message: - type: string - example: "email must be a valid email address" - - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: JWT Bearer token - - parameters: - PageParam: - name: page - in: query - description: 页码 - required: false - schema: - type: integer - minimum: 1 - default: 1 - - LimitParam: - name: limit - in: query - description: 每页数量 - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - SortParam: - name: sort - in: query - description: 排序字段 - required: false - schema: - type: string - default: "created_at" - - OrderParam: - name: order - in: query - description: 排序方向 - required: false - schema: - type: string - enum: [asc, desc] - default: "desc" - -tags: - - name: Users - description: 用户管理接口 - - name: Photos - description: 照片管理接口 - - name: Authentication - description: 认证授权接口 - - name: Categories - description: 分类管理接口 - - name: Tags - description: 标签管理接口 -``` - -## 🏗️ 架构设计 - -### 系统架构概览 -```markdown -# architecture/overview.md - 系统架构概览 - -## 整体架构 - -Photography Portfolio 采用现代化的微服务架构,基于 Go 语言开发,使用 Clean Architecture 设计模式。 - -### 架构层次 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Frontend Layer │ -│ Next.js + React + TypeScript + Tailwind CSS │ -└─────────────────────────────────────────────────────────────┘ - │ - │ HTTP/HTTPS - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ API Gateway │ -│ Caddy + Load Balancer │ -└─────────────────────────────────────────────────────────────┘ - │ - │ HTTP - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Backend Services │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ -│ │ User Service │ │ Photo Service │ │ Auth Service │ │ -│ │ (Go + Gin) │ │ (Go + Gin) │ │ (Go + Gin) │ │ -│ └─────────────────┘ └─────────────────┘ └───────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ - │ - │ SQL/NoSQL - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ -│ │ PostgreSQL │ │ Redis │ │ File Storage │ │ -│ │ (主数据库) │ │ (缓存) │ │ (S3/Local) │ │ -│ └─────────────────┘ └─────────────────┘ └───────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 核心设计原则 - -1. **单一职责原则**: 每个服务只负责一个业务领域 -2. **依赖倒置原则**: 高层模块不依赖低层模块,都依赖抽象 -3. **接口隔离原则**: 使用接口定义契约,便于测试和替换 -4. **开闭原则**: 对扩展开放,对修改封闭 -5. **最小知识原则**: 模块间耦合度最小化 - -### 技术栈选择 - -#### 后端技术栈 -- **语言**: Go 1.23+ (性能优异,并发支持) -- **框架**: Gin (轻量级,高性能 HTTP 框架) -- **数据库**: PostgreSQL (关系型数据,事务支持) -- **缓存**: Redis (高性能内存缓存) -- **ORM**: GORM (功能丰富,性能优良) -- **认证**: JWT (无状态,扩展性好) -- **日志**: Uber Zap (结构化,高性能) -- **配置**: Viper (灵活的配置管理) - -#### 部署技术栈 -- **容器化**: Docker + Docker Compose -- **Web 服务器**: Caddy (自动 HTTPS,配置简单) -- **CI/CD**: Gitea Actions (自动化部署) -- **监控**: Prometheus + Grafana (可观测性) - -### 数据流向 - -1. **请求流程**: - Frontend → API Gateway → Backend Service → Repository → Database - -2. **响应流程**: - Database → Repository → Service → Handler → API Gateway → Frontend - -3. **认证流程**: - Client → Auth Service → JWT Token → Protected Resources - -4. **文件上传流程**: - Client → Upload Handler → Storage Service → File System/S3 -``` - -### 数据库设计 -```markdown -# architecture/database_design.md - 数据库设计 - -## 数据库选择 - -### PostgreSQL (主数据库) -- **优势**: ACID 事务支持,丰富的数据类型,成熟的生态 -- **用途**: 用户数据、照片元数据、分类标签等结构化数据 -- **版本**: PostgreSQL 14+ - -### Redis (缓存数据库) -- **优势**: 高性能内存存储,丰富的数据结构 -- **用途**: 会话缓存、频繁查询数据缓存、限流计数器 -- **版本**: Redis 6+ - -## 表结构设计 - -### 用户表 (users) -```sql -CREATE TABLE users ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - email VARCHAR(100) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - first_name VARCHAR(50), - last_name VARCHAR(50), - avatar VARCHAR(255), - bio TEXT, - role VARCHAR(20) DEFAULT 'user', - status VARCHAR(20) DEFAULT 'active', - last_login_at TIMESTAMP, - email_verified_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 索引 -CREATE INDEX idx_users_email ON users(email); -CREATE INDEX idx_users_username ON users(username); -CREATE INDEX idx_users_status ON users(status); -CREATE INDEX idx_users_created_at ON users(created_at); -``` - -### 照片表 (photos) -```sql -CREATE TABLE photos ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - description TEXT, - filename VARCHAR(255) NOT NULL, - file_path VARCHAR(500) NOT NULL, - file_size BIGINT NOT NULL, - mime_type VARCHAR(100) NOT NULL, - width INTEGER DEFAULT 0, - height INTEGER DEFAULT 0, - exif_data TEXT, - taken_at TIMESTAMP, - location VARCHAR(255), - camera VARCHAR(100), - lens VARCHAR(100), - status VARCHAR(20) DEFAULT 'active', - view_count INTEGER DEFAULT 0, - download_count INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 索引 -CREATE INDEX idx_photos_user_id ON photos(user_id); -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); -``` - -## 关系设计 - -### 实体关系图 -``` -Users ||--o{ Photos : owns -Photos }o--o{ Categories : categorized_by -Photos }o--o{ Tags : tagged_with -Users ||--o{ Albums : creates -Albums }o--o{ Photos : contains -``` - -### 外键约束 -- 所有外键使用 CASCADE 删除策略 -- 确保数据一致性和完整性 -- 使用索引优化查询性能 - -## 查询优化 - -### 索引策略 -1. **主键索引**: 自动创建 -2. **外键索引**: 手动创建,优化关联查询 -3. **查询索引**: 根据常用查询条件创建 -4. **复合索引**: 针对多字段查询优化 - -### 查询优化技巧 -1. **避免 N+1 查询**: 使用预加载 -2. **分页查询**: 使用 LIMIT 和 OFFSET -3. **条件筛选**: 在数据库层面过滤数据 -4. **结果缓存**: 缓存频繁查询的结果 -``` - -## 📖 开发指南 - -### 快速开始 -```markdown -# development/getting_started.md - 快速开始 - -## 环境要求 - -### 必需软件 -- Go 1.23+ -- PostgreSQL 14+ -- Redis 6+ -- Docker & Docker Compose -- Git - -### 推荐工具 -- GoLand 或 VS Code -- Postman 或 Insomnia -- DBeaver (数据库管理) -- Redis Desktop Manager - -## 本地开发环境设置 - -### 1. 克隆项目 -```bash -git clone -cd photography-backend -``` - -### 2. 安装依赖 -```bash -go mod download -``` - -### 3. 配置环境 -```bash -# 复制配置文件 -cp configs/config.dev.yaml.example configs/config.dev.yaml - -# 修改数据库配置 -vim configs/config.dev.yaml -``` - -### 4. 启动服务 -```bash -# 开发模式 (SQLite) -make dev - -# 完整模式 (PostgreSQL + Redis) -make dev-full - -# Mock 模式 (无数据库) -make dev-simple -``` - -### 5. 验证安装 -```bash -# 检查服务状态 -curl http://localhost:8080/health - -# 预期响应 -{ - "status": "healthy", - "timestamp": "2024-01-01T00:00:00Z", - "version": "1.0.0" -} -``` - -## 开发流程 - -### 1. 功能开发 -1. 创建功能分支: `git checkout -b feature/new-feature` -2. 编写代码和测试 -3. 运行测试: `make test` -4. 代码检查: `make lint` -5. 提交代码: `git commit -m "feat: add new feature"` - -### 2. 测试验证 -```bash -# 运行单元测试 -make test-unit - -# 运行集成测试 -make test-integration - -# 运行所有测试 -make test - -# 查看测试覆盖率 -make test-coverage -``` - -### 3. 提交代码 -```bash -# 格式化代码 -make fmt - -# 代码检查 -make lint - -# 构建项目 -make build - -# 提交到远程 -git push origin feature/new-feature -``` -``` - -## 📋 变更日志 - -### 版本管理 -```markdown -# changelog/CHANGELOG.md - 版本变更记录 - -# 变更日志 - -本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范。 - -## [未发布] - -### 新增 -- 新功能开发中... - -### 修改 -- 改进功能... - -### 修复 -- 修复问题... - -### 删除 -- 移除废弃功能... - -## [1.2.0] - 2024-01-15 - -### 新增 -- 添加照片批量上传功能 -- 支持照片 EXIF 信息提取 -- 新增照片搜索 API -- 添加用户头像上传功能 -- 支持照片分类管理 -- 新增 API 限流中间件 - -### 修改 -- 优化照片列表查询性能 -- 改进错误处理机制 -- 更新 API 响应格式 -- 优化数据库查询索引 -- 改进日志记录格式 - -### 修复 -- 修复文件上传大小限制问题 -- 解决并发操作的数据竞争问题 -- 修复 JWT Token 过期处理 -- 解决照片删除时的缓存清理问题 -- 修复分页查询的边界条件 - -### 安全 -- 加强文件上传安全验证 -- 改进密码强度要求 -- 添加请求频率限制 -- 优化 SQL 注入防护 - -## [1.1.0] - 2024-01-01 - -### 新增 -- 用户注册和登录功能 -- JWT 认证机制 -- 用户权限管理 -- 照片上传和管理 -- RESTful API 接口 -- 数据库迁移系统 - -### 修改 -- 重构代码架构 -- 优化性能表现 -- 改进错误处理 - -## [1.0.0] - 2023-12-01 - -### 新增 -- 项目初始版本 -- 基础框架搭建 -- 核心功能实现 -``` - -## 🛠️ 文档维护 - -### 文档编写规范 -1. **Markdown 格式**: 使用标准 Markdown 语法 -2. **结构清晰**: 合理使用标题层级 -3. **代码示例**: 提供完整可运行的示例 -4. **版本同步**: 文档与代码版本保持同步 -5. **定期更新**: 定期检查和更新文档内容 - -### 文档生成工具 -```bash -# 生成 API 文档 -make docs-api - -# 生成 Swagger UI -make docs-swagger - -# 生成完整文档站点 -make docs-build - -# 启动文档服务器 -make docs-serve -``` - -### 文档发布流程 -1. **编写文档**: 按照模板编写文档 -2. **本地预览**: 使用工具预览效果 -3. **代码审查**: 提交 PR 进行审查 -4. **自动部署**: 合并后自动发布到文档站点 - -## 💡 最佳实践 - -### API 文档编写 -1. **完整性**: 包含所有接口的详细说明 -2. **示例丰富**: 提供请求和响应示例 -3. **错误说明**: 详细说明各种错误情况 -4. **版本控制**: 明确 API 版本兼容性 -5. **交互式**: 支持在线测试功能 - -### 架构文档维护 -1. **及时更新**: 架构变更后立即更新文档 -2. **图文并茂**: 使用图表说明复杂概念 -3. **决策记录**: 记录重要的架构决策过程 -4. **影响分析**: 说明变更对系统的影响 -5. **迁移指南**: 提供版本迁移的详细步骤 - -### 开发文档规范 -1. **步骤详细**: 提供详细的操作步骤 -2. **环境说明**: 明确开发环境要求 -3. **故障排查**: 包含常见问题的解决方案 -4. **最佳实践**: 分享开发经验和技巧 -5. **持续改进**: 根据反馈不断完善文档 - -本模块确保项目文档的完整性和准确性,为开发者和用户提供清晰的指导。 \ No newline at end of file diff --git a/backend-old/go.mod b/backend-old/go.mod deleted file mode 100644 index ab688ea..0000000 --- a/backend-old/go.mod +++ /dev/null @@ -1,67 +0,0 @@ -module photography-backend - -go 1.23.0 - -toolchain go1.24.4 - -require ( - github.com/gin-contrib/cors v1.7.6 - github.com/gin-gonic/gin v1.10.1 - github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/spf13/viper v1.18.2 - go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.39.0 - golang.org/x/text v0.26.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 - gorm.io/driver/postgres v1.5.4 - gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.30.0 -) - -require ( - github.com/bytedance/sonic v1.13.3 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.18.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/backend-old/go.sum b/backend-old/go.sum deleted file mode 100644 index 6c724fe..0000000 --- a/backend-old/go.sum +++ /dev/null @@ -1,155 +0,0 @@ -github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= -github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= -github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= -github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= -gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= -gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend-old/internal/CLAUDE.md b/backend-old/internal/CLAUDE.md deleted file mode 100644 index ae80a16..0000000 --- a/backend-old/internal/CLAUDE.md +++ /dev/null @@ -1,313 +0,0 @@ -# Internal 包 - CLAUDE.md - -此文件为 Claude Code 在 internal 包中工作时提供指导。internal 包包含了应用程序的核心业务逻辑,不对外暴露。 - -## 🎯 模块概览 - -internal 包采用分层架构设计,包含以下核心层次: - -- **API Layer** (`api/`): HTTP 接口层,处理请求和响应 -- **Domain Layer** (`domain/`): 领域模型层,定义业务实体和规则 -- **Application Layer** (`application/`): 应用服务层,编排业务逻辑 -- **Infrastructure Layer** (`infrastructure/`): 基础设施层,外部依赖 -- **Shared Layer** (`shared/`): 共享工具和常量 - -## 📁 目录结构 - -``` -internal/ -├── CLAUDE.md # 🔍 当前文件 - Internal 包指南 -├── api/ # 🌐 API 接口层 -│ ├── CLAUDE.md # API 层开发指南 -│ ├── handlers/ # HTTP 处理器 -│ ├── middleware/ # 中间件 -│ ├── routes/ # 路由配置 -│ └── validators/ # 输入验证 -├── domain/ # 🏗️ 领域模型层 -│ ├── CLAUDE.md # 领域层开发指南 -│ ├── entities/ # 业务实体 -│ ├── repositories/ # 仓储接口 -│ └── services/ # 领域服务接口 -├── application/ # 🔧 应用服务层 -│ ├── CLAUDE.md # 应用层开发指南 -│ ├── dto/ # 数据传输对象 -│ └── services/ # 应用服务实现 -├── infrastructure/ # 🏭 基础设施层 -│ ├── CLAUDE.md # 基础设施指南 -│ ├── config/ # 配置管理 -│ ├── database/ # 数据库操作 -│ ├── cache/ # 缓存服务 -│ ├── storage/ # 文件存储 -│ └── repositories/ # 仓储实现 -└── shared/ # 🔗 共享组件 - ├── CLAUDE.md # 共享组件指南 - ├── constants/ # 常量定义 - ├── errors/ # 错误处理 - └── utils/ # 工具函数 -``` - -## 🏗️ 分层架构原则 - -### 依赖方向 -``` -API Layer ──→ Application Layer ──→ Domain Layer - ↓ ↓ ↑ -Infrastructure Layer ──────────────────┘ -``` - -### 层次职责 - -#### 1. API Layer (api/) -- **职责**: 处理 HTTP 请求和响应 -- **依赖**: Application Layer -- **不允许**: 直接调用 Infrastructure Layer 或包含业务逻辑 - -#### 2. Application Layer (application/) -- **职责**: 编排业务逻辑,协调 Domain Services -- **依赖**: Domain Layer -- **不允许**: 包含具体的基础设施实现 - -#### 3. Domain Layer (domain/) -- **职责**: 定义业务实体、规则和接口 -- **依赖**: 无(最核心层) -- **不允许**: 依赖外部框架或基础设施 - -#### 4. Infrastructure Layer (infrastructure/) -- **职责**: 实现外部依赖,如数据库、缓存、文件存储 -- **依赖**: Domain Layer (实现接口) -- **不允许**: 包含业务逻辑 - -#### 5. Shared Layer (shared/) -- **职责**: 提供跨层共享的工具和常量 -- **依赖**: 最小化依赖 -- **原则**: 保持稳定,避免频繁变更 - -## 🔧 开发规范 - -### 包导入规则 - -#### 标准导入顺序 -```go -import ( - // 1. 标准库 - "context" - "fmt" - "time" - - // 2. 第三方库 - "github.com/gin-gonic/gin" - "gorm.io/gorm" - - // 3. 项目内部包 - 按依赖层次 - "photography-backend/internal/domain/entities" - "photography-backend/internal/application/dto" - "photography-backend/internal/shared/errors" -) -``` - -#### 禁止的依赖 -- Domain Layer 不能导入 Infrastructure Layer -- Application Layer 不能导入 API Layer -- 下层不能导入上层 - -### 接口设计原则 - -#### 1. 依赖倒置 -```go -// ✅ 正确:在 domain/repositories 定义接口 -type UserRepository interface { - FindByID(id uint) (*entities.User, error) - Save(user *entities.User) error -} - -// ✅ 在 infrastructure/repositories 实现接口 -type userRepository struct { - db *gorm.DB -} - -func (r *userRepository) FindByID(id uint) (*entities.User, error) { - // 实现细节 -} -``` - -#### 2. 接口隔离 -```go -// ✅ 正确:小而专注的接口 -type UserReader interface { - FindByID(id uint) (*entities.User, error) - FindByEmail(email string) (*entities.User, error) -} - -type UserWriter interface { - Save(user *entities.User) error - Delete(id uint) error -} - -// ❌ 错误:过大的接口 -type UserRepository interface { - // 包含太多方法... -} -``` - -### 错误处理规范 - -#### 1. 错误传播 -```go -// Domain Layer: 定义业务错误 -var ErrUserNotFound = errors.New("user not found") - -// Application Layer: 处理和转换错误 -func (s *UserService) GetUser(id uint) (*dto.UserResponse, error) { - user, err := s.repo.FindByID(id) - if err != nil { - if errors.Is(err, domain.ErrUserNotFound) { - return nil, shared.ErrUserNotFound - } - return nil, fmt.Errorf("failed to get user: %w", err) - } - return s.toDTO(user), nil -} - -// API Layer: 转换为 HTTP 响应 -func (h *UserHandler) GetUser(c *gin.Context) { - user, err := h.service.GetUser(id) - if err != nil { - response.HandleError(c, err) - return - } - response.Success(c, user) -} -``` - -## 🚀 开发工作流 - -### 1. 新功能开发流程 - -#### Step 1: Domain Layer -```bash -# 1. 定义实体 -vim internal/domain/entities/new_entity.go - -# 2. 定义仓储接口 -vim internal/domain/repositories/new_repository.go - -# 3. 定义领域服务接口(如需要) -vim internal/domain/services/new_service.go -``` - -#### Step 2: Application Layer -```bash -# 1. 定义 DTO -vim internal/application/dto/request/new_request.go -vim internal/application/dto/response/new_response.go - -# 2. 实现应用服务 -vim internal/application/services/new_service.go -``` - -#### Step 3: Infrastructure Layer -```bash -# 1. 实现仓储 -vim internal/infrastructure/repositories/new_repository.go - -# 2. 实现其他基础设施(如需要) -``` - -#### Step 4: API Layer -```bash -# 1. 实现处理器 -vim internal/api/handlers/new_handler.go - -# 2. 添加路由 -vim internal/api/routes/api_v1.go - -# 3. 添加验证器(如需要) -vim internal/api/validators/new_validator.go -``` - -### 2. 测试策略 - -#### 单元测试 -- Domain Layer: 测试业务逻辑 -- Application Layer: 测试服务编排 -- Infrastructure Layer: 测试数据访问 - -#### 集成测试 -- API Layer: 端到端测试 -- Repository Layer: 数据库集成测试 - -#### 测试隔离 -```go -// 使用依赖注入便于测试 -type UserService struct { - repo domain.UserRepository -} - -// 测试时注入 Mock -func TestUserService_GetUser(t *testing.T) { - mockRepo := &MockUserRepository{} - service := NewUserService(mockRepo) - // 测试逻辑 -} -``` - -## 📋 代码审查清单 - -### 架构合规性 -- [ ] 是否遵循分层架构原则 -- [ ] 是否违反依赖方向 -- [ ] 接口设计是否合理 -- [ ] 错误处理是否完善 - -### 代码质量 -- [ ] 命名是否清晰明确 -- [ ] 函数是否单一职责 -- [ ] 是否有适当的注释 -- [ ] 是否有相应的测试 - -### 性能考虑 -- [ ] 是否有 N+1 查询问题 -- [ ] 是否需要添加缓存 -- [ ] 数据库操作是否高效 - -## 🎯 最佳实践 - -### 1. 保持层次清晰 -- 每层只关注自己的职责 -- 避免跨层直接调用 -- 使用接口解耦 - -### 2. 依赖注入 -- 使用构造函数注入 -- 避免全局变量 -- 便于测试和替换 - -### 3. 错误处理 -- 使用带类型的错误 -- 适当包装错误信息 -- 保持错误处理一致性 - -### 4. 性能优化 -- 合理使用缓存 -- 数据库查询优化 -- 避免过度设计 - -## 🔍 故障排查 - -### 常见问题 - -1. **循环依赖**: 检查包导入关系 -2. **接口未实现**: 确认所有方法都已实现 -3. **依赖注入失败**: 检查构造函数和依赖配置 -4. **测试失败**: 确认 Mock 对象配置正确 - -### 调试技巧 -```go -// 使用结构化日志 -logger.Debug("Processing request", - logger.String("handler", "UserHandler.GetUser"), - logger.Uint("user_id", userID), -) -``` - -这个架构设计确保了代码的可维护性、可测试性和可扩展性。遵循这些指导原则,可以构建出高质量的后端服务。 \ No newline at end of file diff --git a/backend-old/internal/api/CLAUDE.md b/backend-old/internal/api/CLAUDE.md deleted file mode 100644 index ab39277..0000000 --- a/backend-old/internal/api/CLAUDE.md +++ /dev/null @@ -1,565 +0,0 @@ -# HTTP 接口层 - CLAUDE.md - -本文件为 Claude Code 在 HTTP 接口层模块中工作时提供指导。 - -## 🎯 模块概览 - -HTTP 接口层负责处理所有 HTTP 请求,包括路由定义、请求处理、响应格式化、中间件管理等。 - -### 职责范围 -- 🌐 HTTP 路由定义和管理 -- 📝 请求处理和响应格式化 -- 🛡️ 中间件管理和应用 -- ✅ 请求参数验证 -- 📊 HTTP 状态码管理 - -### 文件结构 -``` -internal/api/ -├── CLAUDE.md # 📋 当前文件 - API 接口指导 -├── handlers/ # 🎯 HTTP 处理器 -│ ├── user.go # 用户相关处理器 -│ ├── photo.go # 照片相关处理器 -│ ├── category.go # 分类相关处理器 -│ ├── tag.go # 标签相关处理器 -│ ├── auth.go # 认证相关处理器 -│ ├── upload.go # 上传相关处理器 -│ └── health.go # 健康检查处理器 -├── middleware/ # 🛡️ 中间件 -│ ├── auth.go # 认证中间件 -│ ├── cors.go # CORS 中间件 -│ ├── logger.go # 日志中间件 -│ ├── rate_limit.go # 限流中间件 -│ ├── recovery.go # 错误恢复中间件 -│ └── validator.go # 验证中间件 -├── routes/ # 🗺️ 路由定义 -│ ├── v1.go # API v1 路由 -│ ├── auth.go # 认证路由 -│ ├── public.go # 公共路由 -│ └── admin.go # 管理员路由 -└── validators/ # ✅ 请求验证器 - ├── user.go # 用户验证器 - ├── photo.go # 照片验证器 - ├── category.go # 分类验证器 - └── common.go # 通用验证器 -``` - -## 🎯 处理器模式 - -### 标准处理器结构 -```go -type UserHandler struct { - userService service.UserServiceInterface - logger *zap.Logger -} - -func NewUserHandler(userService service.UserServiceInterface, logger *zap.Logger) *UserHandler { - return &UserHandler{ - userService: userService, - logger: logger, - } -} - -// 创建用户 -func (h *UserHandler) Create(c *gin.Context) { - var req validators.CreateUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "请求参数无效", err) - return - } - - user, err := h.userService.Create(c.Request.Context(), req) - if err != nil { - h.logger.Error("创建用户失败", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "CREATE_USER_FAILED", "创建用户失败", err) - return - } - - response.Success(c, user, "用户创建成功") -} -``` - -### 处理器职责 -1. **请求绑定**: 解析 HTTP 请求参数 -2. **参数验证**: 验证请求参数的有效性 -3. **服务调用**: 调用应用服务层处理业务逻辑 -4. **响应格式化**: 格式化响应数据 -5. **错误处理**: 处理和记录错误信息 - -## 🗺️ 路由设计 - -### API 版本管理 -```go -// v1 路由组 -v1 := router.Group("/api/v1") -{ - // 公共路由 (无需认证) - v1.POST("/auth/login", authHandler.Login) - v1.POST("/auth/register", authHandler.Register) - v1.GET("/photos/public", photoHandler.GetPublicPhotos) - - // 需要认证的路由 - authenticated := v1.Group("") - authenticated.Use(middleware.AuthRequired()) - { - authenticated.GET("/users/profile", userHandler.GetProfile) - authenticated.PUT("/users/profile", userHandler.UpdateProfile) - authenticated.POST("/photos", photoHandler.Create) - authenticated.GET("/photos", photoHandler.List) - } - - // 管理员路由 - admin := v1.Group("/admin") - admin.Use(middleware.AuthRequired(), middleware.AdminRequired()) - { - admin.GET("/users", userHandler.ListUsers) - admin.DELETE("/users/:id", userHandler.DeleteUser) - admin.GET("/photos/all", photoHandler.ListAllPhotos) - } -} -``` - -### RESTful API 设计 -```go -// 用户资源 -GET /api/v1/users # 获取用户列表 -POST /api/v1/users # 创建用户 -GET /api/v1/users/:id # 获取用户详情 -PUT /api/v1/users/:id # 更新用户 -DELETE /api/v1/users/:id # 删除用户 - -// 照片资源 -GET /api/v1/photos # 获取照片列表 -POST /api/v1/photos # 创建照片 -GET /api/v1/photos/:id # 获取照片详情 -PUT /api/v1/photos/:id # 更新照片 -DELETE /api/v1/photos/:id # 删除照片 - -// 分类资源 -GET /api/v1/categories # 获取分类列表 -POST /api/v1/categories # 创建分类 -GET /api/v1/categories/:id # 获取分类详情 -PUT /api/v1/categories/:id # 更新分类 -DELETE /api/v1/categories/:id # 删除分类 -``` - -## 🛡️ 中间件管理 - -### 认证中间件 -```go -func AuthRequired() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") - if token == "" { - response.Error(c, http.StatusUnauthorized, "MISSING_TOKEN", "缺少认证令牌", nil) - c.Abort() - return - } - - // 验证 JWT Token - claims, err := jwt.VerifyToken(token) - if err != nil { - response.Error(c, http.StatusUnauthorized, "INVALID_TOKEN", "无效的认证令牌", err) - c.Abort() - return - } - - // 设置用户信息到上下文 - c.Set("user_id", claims.UserID) - c.Set("user_role", claims.Role) - c.Next() - } -} -``` - -### 日志中间件 -```go -func Logger(logger *zap.Logger) gin.HandlerFunc { - return func(c *gin.Context) { - start := time.Now() - c.Next() - duration := time.Since(start) - - logger.Info("HTTP Request", - zap.String("method", c.Request.Method), - zap.String("path", c.Request.URL.Path), - zap.String("query", c.Request.URL.RawQuery), - zap.Int("status", c.Writer.Status()), - zap.Duration("duration", duration), - zap.String("user_agent", c.Request.UserAgent()), - zap.String("ip", c.ClientIP()), - ) - } -} -``` - -### CORS 中间件 -```go -func CORS() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") - c.Header("Access-Control-Max-Age", "86400") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } -} -``` - -### 限流中间件 -```go -func RateLimit(limit int, window time.Duration) gin.HandlerFunc { - limiter := rate.NewLimiter(rate.Every(window), limit) - - return func(c *gin.Context) { - if !limiter.Allow() { - response.Error(c, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "请求过于频繁", nil) - c.Abort() - return - } - c.Next() - } -} -``` - -## ✅ 请求验证 - -### 验证器结构 -```go -type CreateUserRequest struct { - Username string `json:"username" binding:"required,min=3,max=20"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - Role string `json:"role" binding:"oneof=user editor admin"` -} - -type UpdateUserRequest struct { - Username string `json:"username,omitempty" binding:"omitempty,min=3,max=20"` - Email string `json:"email,omitempty" binding:"omitempty,email"` - Role string `json:"role,omitempty" binding:"omitempty,oneof=user editor admin"` -} -``` - -### 自定义验证器 -```go -func ValidateUsername(fl validator.FieldLevel) bool { - username := fl.Field().String() - // 用户名只能包含字母、数字和下划线 - matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username) - return matched -} - -func ValidatePhotoFormat(fl validator.FieldLevel) bool { - format := fl.Field().String() - allowedFormats := []string{"jpg", "jpeg", "png", "gif", "webp"} - for _, allowed := range allowedFormats { - if format == allowed { - return true - } - } - return false -} -``` - -### 验证错误处理 -```go -func FormatValidationErrors(err error) map[string]string { - errors := make(map[string]string) - - if validationErrors, ok := err.(validator.ValidationErrors); ok { - for _, e := range validationErrors { - field := strings.ToLower(e.Field()) - switch e.Tag() { - case "required": - errors[field] = "此字段为必填项" - case "email": - errors[field] = "请输入有效的邮箱地址" - case "min": - errors[field] = fmt.Sprintf("最小长度为 %s", e.Param()) - case "max": - errors[field] = fmt.Sprintf("最大长度为 %s", e.Param()) - default: - errors[field] = "字段值无效" - } - } - } - - return errors -} -``` - -## 📊 响应格式 - -### 统一响应结构 -```go -type Response struct { - Success bool `json:"success"` - Data interface{} `json:"data,omitempty"` - Message string `json:"message"` - Error *ErrorInfo `json:"error,omitempty"` - Timestamp time.Time `json:"timestamp"` -} - -type ErrorInfo struct { - Code string `json:"code"` - Message string `json:"message"` - Details interface{} `json:"details,omitempty"` -} -``` - -### 响应帮助函数 -```go -func Success(c *gin.Context, data interface{}, message string) { - c.JSON(http.StatusOK, Response{ - Success: true, - Data: data, - Message: message, - Timestamp: time.Now(), - }) -} - -func Error(c *gin.Context, statusCode int, errorCode, message string, details interface{}) { - c.JSON(statusCode, Response{ - Success: false, - Message: message, - Error: &ErrorInfo{ - Code: errorCode, - Message: message, - Details: details, - }, - Timestamp: time.Now(), - }) -} -``` - -### 分页响应 -```go -type PaginatedResponse struct { - Data interface{} `json:"data"` - Pagination Pagination `json:"pagination"` -} - -type Pagination struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - Total int64 `json:"total"` - TotalPages int `json:"total_pages"` - HasNext bool `json:"has_next"` - HasPrevious bool `json:"has_previous"` -} -``` - -## 🔍 错误处理 - -### 错误分类 -```go -const ( - // 请求错误 - ErrInvalidRequest = "INVALID_REQUEST" - ErrMissingParameter = "MISSING_PARAMETER" - ErrInvalidParameter = "INVALID_PARAMETER" - - // 认证错误 - ErrMissingToken = "MISSING_TOKEN" - ErrInvalidToken = "INVALID_TOKEN" - ErrTokenExpired = "TOKEN_EXPIRED" - ErrInsufficientPermission = "INSUFFICIENT_PERMISSION" - - // 业务错误 - ErrUserNotFound = "USER_NOT_FOUND" - ErrUserAlreadyExists = "USER_ALREADY_EXISTS" - ErrPhotoNotFound = "PHOTO_NOT_FOUND" - ErrCategoryNotFound = "CATEGORY_NOT_FOUND" - - // 系统错误 - ErrInternalServer = "INTERNAL_SERVER_ERROR" - ErrDatabaseError = "DATABASE_ERROR" - ErrStorageError = "STORAGE_ERROR" -) -``` - -### 错误恢复中间件 -```go -func Recovery(logger *zap.Logger) gin.HandlerFunc { - return func(c *gin.Context) { - defer func() { - if err := recover(); err != nil { - logger.Error("Panic recovered", - zap.Any("error", err), - zap.String("path", c.Request.URL.Path), - zap.String("method", c.Request.Method), - ) - - response.Error(c, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "服务器内部错误", nil) - } - }() - c.Next() - } -} -``` - -## 📊 性能优化 - -### 响应压缩 -```go -func Compression() gin.HandlerFunc { - return gzip.Gzip(gzip.DefaultCompression) -} -``` - -### 缓存控制 -```go -func CacheControl(maxAge int) gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("Cache-Control", fmt.Sprintf("max-age=%d", maxAge)) - c.Next() - } -} -``` - -### 请求大小限制 -```go -func LimitRequestSize(maxSize int64) gin.HandlerFunc { - return func(c *gin.Context) { - c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize) - c.Next() - } -} -``` - -## 🧪 测试策略 - -### 处理器测试 -```go -func TestUserHandler_Create(t *testing.T) { - // 模拟服务 - mockService := &MockUserService{} - handler := NewUserHandler(mockService, zap.NewNop()) - - // 创建测试请求 - reqBody := `{"username":"test","email":"test@example.com","password":"123456"}` - req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(reqBody)) - req.Header.Set("Content-Type", "application/json") - - // 执行请求 - w := httptest.NewRecorder() - router := gin.New() - router.POST("/api/v1/users", handler.Create) - router.ServeHTTP(w, req) - - // 验证结果 - assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, w.Body.String(), "success") -} -``` - -### 中间件测试 -```go -func TestAuthMiddleware(t *testing.T) { - middleware := AuthRequired() - - // 测试缺少 token - req, _ := http.NewRequest("GET", "/api/v1/users", nil) - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - middleware(c) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Contains(t, w.Body.String(), "MISSING_TOKEN") -} -``` - -## 📚 API 文档 - -### Swagger 注释 -```go -// CreateUser 创建用户 -// @Summary 创建用户 -// @Description 创建新用户账户 -// @Tags 用户管理 -// @Accept json -// @Produce json -// @Param user body validators.CreateUserRequest true "用户信息" -// @Success 200 {object} response.Response{data=models.User} "成功创建用户" -// @Failure 400 {object} response.Response "请求参数错误" -// @Failure 500 {object} response.Response "服务器内部错误" -// @Router /api/v1/users [post] -func (h *UserHandler) Create(c *gin.Context) { - // 处理器实现 -} -``` - -### API 文档生成 -```bash -# 安装 swag -go install github.com/swaggo/swag/cmd/swag@latest - -# 生成文档 -swag init -g cmd/server/main.go - -# 启动时访问文档 -# http://localhost:8080/swagger/index.html -``` - -## 🔧 开发工具 - -### 路由调试 -```go -func PrintRoutes(router *gin.Engine) { - routes := router.Routes() - for _, route := range routes { - fmt.Printf("[%s] %s -> %s\n", route.Method, route.Path, route.Handler) - } -} -``` - -### 请求日志 -```go -func RequestLogger() gin.HandlerFunc { - return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - return fmt.Sprintf("[%s] %s %s %d %s\n", - param.TimeStamp.Format("2006-01-02 15:04:05"), - param.Method, - param.Path, - param.StatusCode, - param.Latency, - ) - }) -} -``` - -## 💡 最佳实践 - -### 处理器设计 -- 保持处理器轻量,业务逻辑放在服务层 -- 统一错误处理和响应格式 -- 完善的参数验证和错误提示 -- 适当的日志记录 - -### 中间件使用 -- 按需应用中间件,避免过度使用 -- 注意中间件的执行顺序 -- 错误处理中间件应该最先应用 -- 认证中间件应该在业务中间件之前 - -### 路由设计 -- 遵循 RESTful 设计原则 -- 合理的路由分组和版本管理 -- 清晰的路由命名和结构 -- 适当的权限控制 - -### 性能考虑 -- 使用响应压缩减少传输大小 -- 适当的缓存控制 -- 限制请求大小和频率 -- 异步处理耗时操作 - -本模块是整个 API 的入口和门面,确保接口设计合理、响应格式统一、错误处理完善是项目成功的关键。 \ No newline at end of file diff --git a/backend-old/internal/api/handlers/auth_handler.go b/backend-old/internal/api/handlers/auth_handler.go deleted file mode 100644 index 7f4d8bd..0000000 --- a/backend-old/internal/api/handlers/auth_handler.go +++ /dev/null @@ -1,119 +0,0 @@ -package handlers - -import ( - "net/http" - "github.com/gin-gonic/gin" - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service/auth" - "photography-backend/internal/api/middleware" - "photography-backend/pkg/response" -) - -// AuthHandler 认证处理器 -type AuthHandler struct { - authService *auth.AuthService -} - -// NewAuthHandler 创建认证处理器 -func NewAuthHandler(authService *auth.AuthService) *AuthHandler { - return &AuthHandler{ - authService: authService, - } -} - -// Login 用户登录 -func (h *AuthHandler) Login(c *gin.Context) { - var req dto.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - loginResp, err := h.authService.Login(&req) - if err != nil { - c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, err.Error())) - return - } - - c.JSON(http.StatusOK, response.Success(loginResp)) -} - -// Register 用户注册 -func (h *AuthHandler) Register(c *gin.Context) { - var req dto.CreateUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - user, err := h.authService.Register(&req) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - c.JSON(http.StatusCreated, response.Success(user)) -} - -// RefreshToken 刷新令牌 -func (h *AuthHandler) RefreshToken(c *gin.Context) { - var req dto.RefreshTokenRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - loginResp, err := h.authService.RefreshToken(&req) - if err != nil { - c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, err.Error())) - return - } - - c.JSON(http.StatusOK, response.Success(loginResp)) -} - -// GetProfile 获取用户资料 -func (h *AuthHandler) GetProfile(c *gin.Context) { - userID, exists := middleware.GetCurrentUser(c) - if !exists { - c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, "User not authenticated")) - return - } - - user, err := h.authService.GetUserByID(userID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.Error(http.StatusInternalServerError, err.Error())) - return - } - - c.JSON(http.StatusOK, response.Success(user)) -} - -// UpdatePassword 更新密码 -func (h *AuthHandler) UpdatePassword(c *gin.Context) { - userID, exists := middleware.GetCurrentUser(c) - if !exists { - c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, "User not authenticated")) - return - } - - var req dto.ChangePasswordRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - if err := h.authService.UpdatePassword(userID, &req); err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error())) - return - } - - c.JSON(http.StatusOK, response.Success(gin.H{"message": "Password updated successfully"})) -} - -// Logout 用户登出 -func (h *AuthHandler) Logout(c *gin.Context) { - // 简单实现,实际应用中可能需要将token加入黑名单 - c.JSON(http.StatusOK, response.Success(gin.H{"message": "Logged out successfully"})) -} \ No newline at end of file diff --git a/backend-old/internal/api/handlers/category_handler.go b/backend-old/internal/api/handlers/category_handler.go deleted file mode 100644 index 100cf8c..0000000 --- a/backend-old/internal/api/handlers/category_handler.go +++ /dev/null @@ -1,430 +0,0 @@ -package handlers - -import ( - "errors" - "net/http" - "strconv" - - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service" - "photography-backend/pkg/response" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -type CategoryHandler struct { - categoryService *service.CategoryService - logger *zap.Logger -} - -func NewCategoryHandler(categoryService *service.CategoryService, logger *zap.Logger) *CategoryHandler { - return &CategoryHandler{ - categoryService: categoryService, - logger: logger, - } -} - -// GetCategories 获取分类列表 -// @Summary 获取分类列表 -// @Description 获取分类列表,可指定父分类 -// @Tags categories -// @Accept json -// @Produce json -// @Param parent_id query int false "父分类ID" -// @Success 200 {array} models.Category -// @Failure 500 {object} response.Error -// @Router /categories [get] -func (h *CategoryHandler) GetCategories(c *gin.Context) { - var parentID *uint - if parentIDStr := c.Query("parent_id"); parentIDStr != "" { - id, err := strconv.ParseUint(parentIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, "Parent ID must be a valid number")) - return - } - parentIDUint := uint(id) - parentID = &parentIDUint - } - - categories, err := h.categoryService.GetCategories(c.Request.Context(), parentID) - if err != nil { - h.logger.Error("Failed to get categories", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error(http.StatusInternalServerError, err.Error())) - return - } - - c.JSON(http.StatusOK, categories) -} - -// GetCategoryTree 获取分类树 -// @Summary 获取分类树 -// @Description 获取完整的分类树结构 -// @Tags categories -// @Accept json -// @Produce json -// @Success 200 {array} models.CategoryTree -// @Failure 500 {object} response.Error -// @Router /categories/tree [get] -func (h *CategoryHandler) GetCategoryTree(c *gin.Context) { - tree, err := h.categoryService.GetCategoryTree(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get category tree", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get category tree", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tree) -} - -// GetCategory 获取分类详情 -// @Summary 获取分类详情 -// @Description 根据ID获取分类详情 -// @Tags categories -// @Accept json -// @Produce json -// @Param id path int true "分类ID" -// @Success 200 {object} models.Category -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/{id} [get] -func (h *CategoryHandler) GetCategory(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid category ID", - Message: "Category ID must be a valid number", - }) - return - } - - category, err := h.categoryService.GetCategoryByID(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "category not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Category not found", - Message: "The requested category does not exist", - }) - return - } - - h.logger.Error("Failed to get category", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get category", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, category) -} - -// GetCategoryBySlug 根据slug获取分类 -// @Summary 根据slug获取分类 -// @Description 根据slug获取分类详情 -// @Tags categories -// @Accept json -// @Produce json -// @Param slug path string true "分类slug" -// @Success 200 {object} models.Category -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/slug/{slug} [get] -func (h *CategoryHandler) GetCategoryBySlug(c *gin.Context) { - slug := c.Param("slug") - - category, err := h.categoryService.GetCategoryBySlug(c.Request.Context(), slug) - if err != nil { - if err.Error() == "category not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Category not found", - Message: "The requested category does not exist", - }) - return - } - - h.logger.Error("Failed to get category by slug", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get category", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, category) -} - -// CreateCategory 创建分类 -// @Summary 创建分类 -// @Description 创建新的分类 -// @Tags categories -// @Accept json -// @Produce json -// @Param category body models.CreateCategoryRequest true "分类信息" -// @Success 201 {object} models.Category -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories [post] -func (h *CategoryHandler) CreateCategory(c *gin.Context) { - var req entity.CreateCategoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - // 验证请求数据 - if err := h.validateCreateCategoryRequest(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request data", - Message: err.Error(), - }) - return - } - - category, err := h.categoryService.CreateCategory(c.Request.Context(), &req) - if err != nil { - h.logger.Error("Failed to create category", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to create category", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusCreated, category) -} - -// UpdateCategory 更新分类 -// @Summary 更新分类 -// @Description 更新分类信息 -// @Tags categories -// @Accept json -// @Produce json -// @Param id path int true "分类ID" -// @Param category body models.UpdateCategoryRequest true "分类信息" -// @Success 200 {object} models.Category -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/{id} [put] -func (h *CategoryHandler) UpdateCategory(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid category ID", - Message: "Category ID must be a valid number", - }) - return - } - - var req entity.UpdateCategoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - category, err := h.categoryService.UpdateCategory(c.Request.Context(), uint(id), &req) - if err != nil { - if err.Error() == "category not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Category not found", - Message: "The requested category does not exist", - }) - return - } - - h.logger.Error("Failed to update category", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to update category", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, category) -} - -// DeleteCategory 删除分类 -// @Summary 删除分类 -// @Description 删除分类 -// @Tags categories -// @Accept json -// @Produce json -// @Param id path int true "分类ID" -// @Success 204 "No Content" -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/{id} [delete] -func (h *CategoryHandler) DeleteCategory(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid category ID", - Message: "Category ID must be a valid number", - }) - return - } - - err = h.categoryService.DeleteCategory(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "category not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Category not found", - Message: "The requested category does not exist", - }) - return - } - - h.logger.Error("Failed to delete category", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to delete category", - Message: err.Error(), - }) - return - } - - c.Status(http.StatusNoContent) -} - -// ReorderCategories 重新排序分类 -// @Summary 重新排序分类 -// @Description 重新排序分类 -// @Tags categories -// @Accept json -// @Produce json -// @Param request body models.ReorderCategoriesRequest true "排序请求" -// @Success 200 {object} models.SuccessResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/reorder [post] -func (h *CategoryHandler) ReorderCategories(c *gin.Context) { - var req entity.ReorderCategoriesRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if len(req.CategoryIDs) == 0 { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "No category IDs provided", - }) - return - } - - err := h.categoryService.ReorderCategories(c.Request.Context(), req.ParentID, req.CategoryIDs) - if err != nil { - h.logger.Error("Failed to reorder categories", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to reorder categories", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, entity.SuccessResponse{ - Message: "Categories reordered successfully", - }) -} - -// GetCategoryStats 获取分类统计信息 -// @Summary 获取分类统计信息 -// @Description 获取分类统计信息 -// @Tags categories -// @Accept json -// @Produce json -// @Success 200 {object} models.CategoryStats -// @Failure 500 {object} response.Error -// @Router /categories/stats [get] -func (h *CategoryHandler) GetCategoryStats(c *gin.Context) { - stats, err := h.categoryService.GetCategoryStats(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get category stats", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get category stats", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, stats) -} - -// GenerateSlug 生成分类slug -// @Summary 生成分类slug -// @Description 根据分类名称生成唯一的slug -// @Tags categories -// @Accept json -// @Produce json -// @Param request body models.GenerateSlugRequest true "生成slug请求" -// @Success 200 {object} models.GenerateSlugResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /categories/generate-slug [post] -func (h *CategoryHandler) GenerateSlug(c *gin.Context) { - var req entity.GenerateSlugRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if req.Name == "" { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "Name is required", - }) - return - } - - slug, err := h.categoryService.GenerateSlug(c.Request.Context(), req.Name) - if err != nil { - h.logger.Error("Failed to generate slug", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to generate slug", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, entity.GenerateSlugResponse{ - Slug: slug, - }) -} - -// validateCreateCategoryRequest 验证创建分类请求 -func (h *CategoryHandler) validateCreateCategoryRequest(req *models.CreateCategoryRequest) error { - if req.Name == "" { - return errors.New("name is required") - } - - if req.Slug == "" { - return errors.New("slug is required") - } - - return nil -} \ No newline at end of file diff --git a/backend-old/internal/api/handlers/photo_handler.go b/backend-old/internal/api/handlers/photo_handler.go deleted file mode 100644 index 5c64498..0000000 --- a/backend-old/internal/api/handlers/photo_handler.go +++ /dev/null @@ -1,480 +0,0 @@ -package handlers - -import ( - "errors" - "net/http" - "strconv" - "strings" - - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -type PhotoHandler struct { - photoService *service.PhotoService - logger *zap.Logger -} - -func NewPhotoHandler(photoService *service.PhotoService, logger *zap.Logger) *PhotoHandler { - return &PhotoHandler{ - photoService: photoService, - logger: logger, - } -} - -// GetPhotos 获取照片列表 -// @Summary 获取照片列表 -// @Description 获取照片列表,支持分页、搜索、过滤 -// @Tags photos -// @Accept json -// @Produce json -// @Param page query int false "页码" -// @Param limit query int false "每页数量" -// @Param search query string false "搜索关键词" -// @Param status query string false "状态筛选" -// @Param category_id query int false "分类ID" -// @Param tags query string false "标签列表(逗号分隔)" -// @Param start_date query string false "开始日期" -// @Param end_date query string false "结束日期" -// @Param sort_by query string false "排序字段" -// @Param sort_order query string false "排序方向" -// @Success 200 {object} service.PhotoListResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos [get] -func (h *PhotoHandler) GetPhotos(c *gin.Context) { - var params service.PhotoListParams - - // 解析查询参数 - if err := c.ShouldBindQuery(¶ms); err != nil { - h.logger.Error("Failed to bind query params", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid query parameters", - Message: err.Error(), - }) - return - } - - // 解析标签参数 - if tagsStr := c.Query("tags"); tagsStr != "" { - params.Tags = strings.Split(tagsStr, ",") - } - - // 调用服务层 - result, err := h.photoService.GetPhotos(c.Request.Context(), params) - if err != nil { - h.logger.Error("Failed to get photos", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get photos", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, result) -} - -// GetPhoto 获取照片详情 -// @Summary 获取照片详情 -// @Description 根据ID获取照片详情 -// @Tags photos -// @Accept json -// @Produce json -// @Param id path int true "照片ID" -// @Success 200 {object} models.Photo -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/{id} [get] -func (h *PhotoHandler) GetPhoto(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid photo ID", - Message: "Photo ID must be a valid number", - }) - return - } - - photo, err := h.photoService.GetPhotoByID(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "photo not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Photo not found", - Message: "The requested photo does not exist", - }) - return - } - - h.logger.Error("Failed to get photo", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get photo", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, photo) -} - -// CreatePhoto 创建照片 -// @Summary 创建照片 -// @Description 创建新的照片记录 -// @Tags photos -// @Accept json -// @Produce json -// @Param photo body models.CreatePhotoRequest true "照片信息" -// @Success 201 {object} models.Photo -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos [post] -func (h *PhotoHandler) CreatePhoto(c *gin.Context) { - var req models.CreatePhotoRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - // 验证请求数据 - if err := h.validateCreatePhotoRequest(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request data", - Message: err.Error(), - }) - return - } - - photo, err := h.photoService.CreatePhoto(c.Request.Context(), &req) - if err != nil { - h.logger.Error("Failed to create photo", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to create photo", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusCreated, photo) -} - -// UpdatePhoto 更新照片 -// @Summary 更新照片 -// @Description 更新照片信息 -// @Tags photos -// @Accept json -// @Produce json -// @Param id path int true "照片ID" -// @Param photo body models.UpdatePhotoRequest true "照片信息" -// @Success 200 {object} models.Photo -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/{id} [put] -func (h *PhotoHandler) UpdatePhoto(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid photo ID", - Message: "Photo ID must be a valid number", - }) - return - } - - var req models.UpdatePhotoRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - photo, err := h.photoService.UpdatePhoto(c.Request.Context(), uint(id), &req) - if err != nil { - if err.Error() == "photo not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Photo not found", - Message: "The requested photo does not exist", - }) - return - } - - h.logger.Error("Failed to update photo", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to update photo", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, photo) -} - -// DeletePhoto 删除照片 -// @Summary 删除照片 -// @Description 删除照片 -// @Tags photos -// @Accept json -// @Produce json -// @Param id path int true "照片ID" -// @Success 204 "No Content" -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/{id} [delete] -func (h *PhotoHandler) DeletePhoto(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid photo ID", - Message: "Photo ID must be a valid number", - }) - return - } - - err = h.photoService.DeletePhoto(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "photo not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Photo not found", - Message: "The requested photo does not exist", - }) - return - } - - h.logger.Error("Failed to delete photo", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to delete photo", - Message: err.Error(), - }) - return - } - - c.Status(http.StatusNoContent) -} - -// UploadPhoto 上传照片 -// @Summary 上传照片 -// @Description 上传照片文件并创建记录 -// @Tags photos -// @Accept multipart/form-data -// @Produce json -// @Param file formData file true "照片文件" -// @Param title formData string false "标题" -// @Param description formData string false "描述" -// @Param status formData string false "状态" -// @Param category_ids formData string false "分类ID列表(逗号分隔)" -// @Param tag_ids formData string false "标签ID列表(逗号分隔)" -// @Success 201 {object} models.Photo -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/upload [post] -func (h *PhotoHandler) UploadPhoto(c *gin.Context) { - // 获取上传的文件 - file, header, err := c.Request.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "No file uploaded", - Message: "Please select a file to upload", - }) - return - } - defer file.Close() - - // 构建创建请求 - req := &models.CreatePhotoRequest{ - Title: c.PostForm("title"), - Description: c.PostForm("description"), - Status: c.PostForm("status"), - } - - // 如果未指定状态,默认为草稿 - if req.Status == "" { - req.Status = "draft" - } - - // 解析分类ID - if categoryIDsStr := c.PostForm("category_ids"); categoryIDsStr != "" { - categoryIDStrs := strings.Split(categoryIDsStr, ",") - for _, idStr := range categoryIDStrs { - if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil { - req.CategoryIDs = append(req.CategoryIDs, uint(id)) - } - } - } - - // 解析标签ID - if tagIDsStr := c.PostForm("tag_ids"); tagIDsStr != "" { - tagIDStrs := strings.Split(tagIDsStr, ",") - for _, idStr := range tagIDStrs { - if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil { - req.TagIDs = append(req.TagIDs, uint(id)) - } - } - } - - // 上传照片 - photo, err := h.photoService.UploadPhoto(c.Request.Context(), file, header, req) - if err != nil { - h.logger.Error("Failed to upload photo", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to upload photo", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusCreated, photo) -} - -// BatchUpdatePhotos 批量更新照片 -// @Summary 批量更新照片 -// @Description 批量更新照片信息 -// @Tags photos -// @Accept json -// @Produce json -// @Param request body models.BatchUpdatePhotosRequest true "批量更新请求" -// @Success 200 {object} models.SuccessResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/batch/update [post] -func (h *PhotoHandler) BatchUpdatePhotos(c *gin.Context) { - var req models.BatchUpdatePhotosRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if len(req.IDs) == 0 { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "No photo IDs provided", - }) - return - } - - err := h.photoService.BatchUpdatePhotos(c.Request.Context(), req.IDs, &req) - if err != nil { - h.logger.Error("Failed to batch update photos", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to batch update photos", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, models.SuccessResponse{ - Message: "Photos updated successfully", - }) -} - -// BatchDeletePhotos 批量删除照片 -// @Summary 批量删除照片 -// @Description 批量删除照片 -// @Tags photos -// @Accept json -// @Produce json -// @Param request body models.BatchDeleteRequest true "批量删除请求" -// @Success 200 {object} models.SuccessResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /photos/batch/delete [post] -func (h *PhotoHandler) BatchDeletePhotos(c *gin.Context) { - var req models.BatchDeleteRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if len(req.IDs) == 0 { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "No photo IDs provided", - }) - return - } - - err := h.photoService.BatchDeletePhotos(c.Request.Context(), req.IDs) - if err != nil { - h.logger.Error("Failed to batch delete photos", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to batch delete photos", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, models.SuccessResponse{ - Message: "Photos deleted successfully", - }) -} - -// GetPhotoStats 获取照片统计信息 -// @Summary 获取照片统计信息 -// @Description 获取照片统计信息 -// @Tags photos -// @Accept json -// @Produce json -// @Success 200 {object} models.PhotoStats -// @Failure 500 {object} response.Error -// @Router /photos/stats [get] -func (h *PhotoHandler) GetPhotoStats(c *gin.Context) { - stats, err := h.photoService.GetPhotoStats(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get photo stats", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get photo stats", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, stats) -} - -// validateCreatePhotoRequest 验证创建照片请求 -func (h *PhotoHandler) validateCreatePhotoRequest(req *models.CreatePhotoRequest) error { - if req.Title == "" { - return errors.New("title is required") - } - - if req.Status == "" { - req.Status = "draft" - } - - // 验证状态值 - validStatuses := []string{"draft", "published", "archived", "processing"} - isValidStatus := false - for _, status := range validStatuses { - if req.Status == status { - isValidStatus = true - break - } - } - - if !isValidStatus { - return errors.New("invalid status value") - } - - return nil -} \ No newline at end of file diff --git a/backend-old/internal/api/handlers/tag_handler.go b/backend-old/internal/api/handlers/tag_handler.go deleted file mode 100644 index 58be4d4..0000000 --- a/backend-old/internal/api/handlers/tag_handler.go +++ /dev/null @@ -1,536 +0,0 @@ -package handlers - -import ( - "errors" - "net/http" - "strconv" - - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -type TagHandler struct { - tagService *service.TagService - logger *zap.Logger -} - -func NewTagHandler(tagService *service.TagService, logger *zap.Logger) *TagHandler { - return &TagHandler{ - tagService: tagService, - logger: logger, - } -} - -// GetTags 获取标签列表 -// @Summary 获取标签列表 -// @Description 获取标签列表,支持分页、搜索、过滤 -// @Tags tags -// @Accept json -// @Produce json -// @Param page query int false "页码" -// @Param limit query int false "每页数量" -// @Param search query string false "搜索关键词" -// @Param is_active query bool false "是否激活" -// @Param sort_by query string false "排序字段" -// @Param sort_order query string false "排序方向" -// @Success 200 {object} service.TagListResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags [get] -func (h *TagHandler) GetTags(c *gin.Context) { - var params service.TagListParams - - // 解析查询参数 - if err := c.ShouldBindQuery(¶ms); err != nil { - h.logger.Error("Failed to bind query params", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid query parameters", - Message: err.Error(), - }) - return - } - - // 调用服务层 - result, err := h.tagService.GetTags(c.Request.Context(), params) - if err != nil { - h.logger.Error("Failed to get tags", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get tags", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, result) -} - -// GetAllTags 获取所有活跃标签 -// @Summary 获取所有活跃标签 -// @Description 获取所有活跃标签(用于选择器) -// @Tags tags -// @Accept json -// @Produce json -// @Success 200 {array} models.Tag -// @Failure 500 {object} response.Error -// @Router /tags/all [get] -func (h *TagHandler) GetAllTags(c *gin.Context) { - tags, err := h.tagService.GetAllTags(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get all tags", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get all tags", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tags) -} - -// GetTag 获取标签详情 -// @Summary 获取标签详情 -// @Description 根据ID获取标签详情 -// @Tags tags -// @Accept json -// @Produce json -// @Param id path int true "标签ID" -// @Success 200 {object} models.Tag -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/{id} [get] -func (h *TagHandler) GetTag(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid tag ID", - Message: "Tag ID must be a valid number", - }) - return - } - - tag, err := h.tagService.GetTagByID(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "tag not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Tag not found", - Message: "The requested tag does not exist", - }) - return - } - - h.logger.Error("Failed to get tag", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get tag", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tag) -} - -// GetTagBySlug 根据slug获取标签 -// @Summary 根据slug获取标签 -// @Description 根据slug获取标签详情 -// @Tags tags -// @Accept json -// @Produce json -// @Param slug path string true "标签slug" -// @Success 200 {object} models.Tag -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/slug/{slug} [get] -func (h *TagHandler) GetTagBySlug(c *gin.Context) { - slug := c.Param("slug") - - tag, err := h.tagService.GetTagBySlug(c.Request.Context(), slug) - if err != nil { - if err.Error() == "tag not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Tag not found", - Message: "The requested tag does not exist", - }) - return - } - - h.logger.Error("Failed to get tag by slug", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get tag", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tag) -} - -// CreateTag 创建标签 -// @Summary 创建标签 -// @Description 创建新的标签 -// @Tags tags -// @Accept json -// @Produce json -// @Param tag body models.CreateTagRequest true "标签信息" -// @Success 201 {object} models.Tag -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags [post] -func (h *TagHandler) CreateTag(c *gin.Context) { - var req models.CreateTagRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - // 验证请求数据 - if err := h.validateCreateTagRequest(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request data", - Message: err.Error(), - }) - return - } - - tag, err := h.tagService.CreateTag(c.Request.Context(), &req) - if err != nil { - h.logger.Error("Failed to create tag", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to create tag", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusCreated, tag) -} - -// UpdateTag 更新标签 -// @Summary 更新标签 -// @Description 更新标签信息 -// @Tags tags -// @Accept json -// @Produce json -// @Param id path int true "标签ID" -// @Param tag body models.UpdateTagRequest true "标签信息" -// @Success 200 {object} models.Tag -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/{id} [put] -func (h *TagHandler) UpdateTag(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid tag ID", - Message: "Tag ID must be a valid number", - }) - return - } - - var req models.UpdateTagRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - tag, err := h.tagService.UpdateTag(c.Request.Context(), uint(id), &req) - if err != nil { - if err.Error() == "tag not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Tag not found", - Message: "The requested tag does not exist", - }) - return - } - - h.logger.Error("Failed to update tag", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to update tag", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tag) -} - -// DeleteTag 删除标签 -// @Summary 删除标签 -// @Description 删除标签 -// @Tags tags -// @Accept json -// @Produce json -// @Param id path int true "标签ID" -// @Success 204 "No Content" -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/{id} [delete] -func (h *TagHandler) DeleteTag(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid tag ID", - Message: "Tag ID must be a valid number", - }) - return - } - - err = h.tagService.DeleteTag(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "tag not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "Tag not found", - Message: "The requested tag does not exist", - }) - return - } - - h.logger.Error("Failed to delete tag", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to delete tag", - Message: err.Error(), - }) - return - } - - c.Status(http.StatusNoContent) -} - -// BatchDeleteTags 批量删除标签 -// @Summary 批量删除标签 -// @Description 批量删除标签 -// @Tags tags -// @Accept json -// @Produce json -// @Param request body models.BatchDeleteRequest true "批量删除请求" -// @Success 200 {object} models.SuccessResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/batch/delete [post] -func (h *TagHandler) BatchDeleteTags(c *gin.Context) { - var req models.BatchDeleteRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if len(req.IDs) == 0 { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "No tag IDs provided", - }) - return - } - - err := h.tagService.BatchDeleteTags(c.Request.Context(), req.IDs) - if err != nil { - h.logger.Error("Failed to batch delete tags", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to batch delete tags", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, models.SuccessResponse{ - Message: "Tags deleted successfully", - }) -} - -// GetPopularTags 获取热门标签 -// @Summary 获取热门标签 -// @Description 获取热门标签(按使用次数排序) -// @Tags tags -// @Accept json -// @Produce json -// @Param limit query int false "限制数量" -// @Success 200 {array} models.TagWithCount -// @Failure 500 {object} response.Error -// @Router /tags/popular [get] -func (h *TagHandler) GetPopularTags(c *gin.Context) { - limit := 10 - if limitStr := c.Query("limit"); limitStr != "" { - if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - - tags, err := h.tagService.GetPopularTags(c.Request.Context(), limit) - if err != nil { - h.logger.Error("Failed to get popular tags", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get popular tags", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tags) -} - -// GetTagCloud 获取标签云 -// @Summary 获取标签云 -// @Description 获取标签云数据 -// @Tags tags -// @Accept json -// @Produce json -// @Success 200 {array} models.TagCloudItem -// @Failure 500 {object} response.Error -// @Router /tags/cloud [get] -func (h *TagHandler) GetTagCloud(c *gin.Context) { - cloud, err := h.tagService.GetTagCloud(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get tag cloud", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get tag cloud", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, cloud) -} - -// GetTagStats 获取标签统计信息 -// @Summary 获取标签统计信息 -// @Description 获取标签统计信息 -// @Tags tags -// @Accept json -// @Produce json -// @Success 200 {object} models.TagStats -// @Failure 500 {object} response.Error -// @Router /tags/stats [get] -func (h *TagHandler) GetTagStats(c *gin.Context) { - stats, err := h.tagService.GetTagStats(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get tag stats", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get tag stats", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, stats) -} - -// SearchTags 搜索标签 -// @Summary 搜索标签 -// @Description 搜索标签(用于自动完成) -// @Tags tags -// @Accept json -// @Produce json -// @Param q query string true "搜索关键词" -// @Param limit query int false "限制数量" -// @Success 200 {array} models.Tag -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/search [get] -func (h *TagHandler) SearchTags(c *gin.Context) { - query := c.Query("q") - if query == "" { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid query", - Message: "Search query is required", - }) - return - } - - limit := 10 - if limitStr := c.Query("limit"); limitStr != "" { - if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - - tags, err := h.tagService.SearchTags(c.Request.Context(), query, limit) - if err != nil { - h.logger.Error("Failed to search tags", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to search tags", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, tags) -} - -// GenerateSlug 生成标签slug -// @Summary 生成标签slug -// @Description 根据标签名称生成唯一的slug -// @Tags tags -// @Accept json -// @Produce json -// @Param request body models.GenerateSlugRequest true "生成slug请求" -// @Success 200 {object} models.GenerateSlugResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /tags/generate-slug [post] -func (h *TagHandler) GenerateSlug(c *gin.Context) { - var req models.GenerateSlugRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - if req.Name == "" { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request", - Message: "Name is required", - }) - return - } - - slug, err := h.tagService.GenerateSlug(c.Request.Context(), req.Name) - if err != nil { - h.logger.Error("Failed to generate slug", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to generate slug", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, models.GenerateSlugResponse{ - Slug: slug, - }) -} - -// validateCreateTagRequest 验证创建标签请求 -func (h *TagHandler) validateCreateTagRequest(req *models.CreateTagRequest) error { - if req.Name == "" { - return errors.New("name is required") - } - - if req.Slug == "" { - return errors.New("slug is required") - } - - return nil -} \ No newline at end of file diff --git a/backend-old/internal/api/handlers/user_handler.go b/backend-old/internal/api/handlers/user_handler.go deleted file mode 100644 index 65dd131..0000000 --- a/backend-old/internal/api/handlers/user_handler.go +++ /dev/null @@ -1,410 +0,0 @@ -package handlers - -import ( - "errors" - "net/http" - "strconv" - - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/service" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -type UserHandler struct { - userService *service.UserService - logger *zap.Logger -} - -func NewUserHandler(userService *service.UserService, logger *zap.Logger) *UserHandler { - return &UserHandler{ - userService: userService, - logger: logger, - } -} - -// GetCurrentUser 获取当前用户信息 -// @Summary 获取当前用户信息 -// @Description 获取当前登录用户的详细信息 -// @Tags users -// @Accept json -// @Produce json -// @Success 200 {object} models.UserResponse -// @Failure 401 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /me [get] -func (h *UserHandler) GetCurrentUser(c *gin.Context) { - userID := c.GetUint("user_id") - - user, err := h.userService.GetUserByID(c.Request.Context(), userID) - if err != nil { - h.logger.Error("Failed to get current user", zap.Error(err), zap.Uint("user_id", userID)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get user information", - Message: err.Error(), - }) - return - } - - userResponse := &models.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Role: user.Role, - IsActive: user.IsActive, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } - - c.JSON(http.StatusOK, userResponse) -} - -// UpdateCurrentUser 更新当前用户信息 -// @Summary 更新当前用户信息 -// @Description 更新当前登录用户的个人信息 -// @Tags users -// @Accept json -// @Produce json -// @Param user body models.UpdateCurrentUserRequest true "用户信息" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /me [put] -func (h *UserHandler) UpdateCurrentUser(c *gin.Context) { - userID := c.GetUint("user_id") - - var req models.UpdateCurrentUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - user, err := h.userService.UpdateCurrentUser(c.Request.Context(), userID, &req) - if err != nil { - h.logger.Error("Failed to update current user", zap.Error(err), zap.Uint("user_id", userID)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to update user information", - Message: err.Error(), - }) - return - } - - userResponse := &models.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Role: user.Role, - IsActive: user.IsActive, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } - - c.JSON(http.StatusOK, userResponse) -} - -// GetUsers 获取用户列表 (管理员功能) -// @Summary 获取用户列表 -// @Description 获取系统中所有用户列表 -// @Tags admin -// @Accept json -// @Produce json -// @Param page query int false "页码" -// @Param limit query int false "每页数量" -// @Param search query string false "搜索关键词" -// @Success 200 {object} service.UserListResponse -// @Failure 403 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /admin/users [get] -func (h *UserHandler) GetUsers(c *gin.Context) { - var params service.UserListParams - - // 解析查询参数 - if err := c.ShouldBindQuery(¶ms); err != nil { - h.logger.Error("Failed to bind query params", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid query parameters", - Message: err.Error(), - }) - return - } - - result, err := h.userService.GetUsers(c.Request.Context(), params) - if err != nil { - h.logger.Error("Failed to get users", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get users", - Message: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, result) -} - -// GetUser 获取用户详情 (管理员功能) -// @Summary 获取用户详情 -// @Description 根据ID获取用户详情 -// @Tags admin -// @Accept json -// @Produce json -// @Param id path int true "用户ID" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /admin/users/{id} [get] -func (h *UserHandler) GetUser(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid user ID", - Message: "User ID must be a valid number", - }) - return - } - - user, err := h.userService.GetUserByID(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "user not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "User not found", - Message: "The requested user does not exist", - }) - return - } - - h.logger.Error("Failed to get user", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to get user", - Message: err.Error(), - }) - return - } - - userResponse := &models.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Role: user.Role, - IsActive: user.IsActive, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } - - c.JSON(http.StatusOK, userResponse) -} - -// CreateUser 创建用户 (管理员功能) -// @Summary 创建用户 -// @Description 创建新用户 -// @Tags admin -// @Accept json -// @Produce json -// @Param user body models.CreateUserRequest true "用户信息" -// @Success 201 {object} models.UserResponse -// @Failure 400 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /admin/users [post] -func (h *UserHandler) CreateUser(c *gin.Context) { - var req models.CreateUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - // 验证请求数据 - if err := h.validateCreateUserRequest(&req); err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request data", - Message: err.Error(), - }) - return - } - - user, err := h.userService.CreateUser(c.Request.Context(), &req) - if err != nil { - h.logger.Error("Failed to create user", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to create user", - Message: err.Error(), - }) - return - } - - userResponse := &models.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Role: user.Role, - IsActive: user.IsActive, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } - - c.JSON(http.StatusCreated, userResponse) -} - -// UpdateUser 更新用户 (管理员功能) -// @Summary 更新用户 -// @Description 更新用户信息 -// @Tags admin -// @Accept json -// @Produce json -// @Param id path int true "用户ID" -// @Param user body models.UpdateUserRequest true "用户信息" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /admin/users/{id} [put] -func (h *UserHandler) UpdateUser(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid user ID", - Message: "User ID must be a valid number", - }) - return - } - - var req models.UpdateUserRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Error("Failed to bind JSON", zap.Error(err)) - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid request body", - Message: err.Error(), - }) - return - } - - user, err := h.userService.UpdateUser(c.Request.Context(), uint(id), &req) - if err != nil { - if err.Error() == "user not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "User not found", - Message: "The requested user does not exist", - }) - return - } - - h.logger.Error("Failed to update user", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to update user", - Message: err.Error(), - }) - return - } - - userResponse := &models.UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Role: user.Role, - IsActive: user.IsActive, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } - - c.JSON(http.StatusOK, userResponse) -} - -// DeleteUser 删除用户 (管理员功能) -// @Summary 删除用户 -// @Description 删除用户 -// @Tags admin -// @Accept json -// @Produce json -// @Param id path int true "用户ID" -// @Success 204 "No Content" -// @Failure 400 {object} response.Error -// @Failure 404 {object} response.Error -// @Failure 500 {object} response.Error -// @Router /admin/users/{id} [delete] -func (h *UserHandler) DeleteUser(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Invalid user ID", - Message: "User ID must be a valid number", - }) - return - } - - // 防止删除自己 - currentUserID := c.GetUint("user_id") - if uint(id) == currentUserID { - c.JSON(http.StatusBadRequest, response.Error{ - Error: "Cannot delete yourself", - Message: "You cannot delete your own account", - }) - return - } - - err = h.userService.DeleteUser(c.Request.Context(), uint(id)) - if err != nil { - if err.Error() == "user not found" { - c.JSON(http.StatusNotFound, response.Error{ - Error: "User not found", - Message: "The requested user does not exist", - }) - return - } - - h.logger.Error("Failed to delete user", zap.Error(err)) - c.JSON(http.StatusInternalServerError, response.Error{ - Error: "Failed to delete user", - Message: err.Error(), - }) - return - } - - c.Status(http.StatusNoContent) -} - -// validateCreateUserRequest 验证创建用户请求 -func (h *UserHandler) validateCreateUserRequest(req *models.CreateUserRequest) error { - if req.Username == "" { - return errors.New("username is required") - } - - if req.Email == "" { - return errors.New("email is required") - } - - if req.Password == "" { - return errors.New("password is required") - } - - if req.Role == "" { - req.Role = "user" - } - - // 验证角色 - validRoles := []string{"user", "editor", "admin"} - isValidRole := false - for _, role := range validRoles { - if req.Role == role { - isValidRole = true - break - } - } - - if !isValidRole { - return errors.New("invalid role value") - } - - return nil -} \ No newline at end of file diff --git a/backend-old/internal/api/middleware/auth.go b/backend-old/internal/api/middleware/auth.go deleted file mode 100644 index 7433a67..0000000 --- a/backend-old/internal/api/middleware/auth.go +++ /dev/null @@ -1,217 +0,0 @@ -package middleware - -import ( - "net/http" - "strings" - "github.com/gin-gonic/gin" - "photography-backend/internal/service/auth" - "photography-backend/internal/model/entity" -) - -// AuthMiddleware 认证中间件 -type AuthMiddleware struct { - jwtService *auth.JWTService -} - -// NewAuthMiddleware 创建认证中间件 -func NewAuthMiddleware(jwtService *auth.JWTService) *AuthMiddleware { - return &AuthMiddleware{ - jwtService: jwtService, - } -} - -// RequireAuth 需要认证的中间件 -func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc { - return func(c *gin.Context) { - // 从Header中获取Authorization - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Authorization header is required", - }) - c.Abort() - return - } - - // 检查Bearer前缀 - if !strings.HasPrefix(authHeader, "Bearer ") { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Invalid authorization header format", - }) - c.Abort() - return - } - - // 提取token - token := strings.TrimPrefix(authHeader, "Bearer ") - - // 验证token - claims, err := m.jwtService.ValidateToken(token) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Invalid or expired token", - }) - c.Abort() - return - } - - // 将用户信息存入上下文 - c.Set("user_id", claims.UserID) - c.Set("username", claims.Username) - c.Set("user_role", claims.Role) - - c.Next() - } -} - -// RequireRole 需要特定角色的中间件 -func (m *AuthMiddleware) RequireRole(requiredRole string) gin.HandlerFunc { - return func(c *gin.Context) { - userRole, exists := c.Get("user_role") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "User role not found in context", - }) - c.Abort() - return - } - - roleStr, ok := userRole.(string) - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Invalid user role", - }) - c.Abort() - return - } - - // 检查角色权限 - if !m.hasPermission(roleStr, requiredRole) { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Insufficient permissions", - }) - c.Abort() - return - } - - c.Next() - } -} - -// RequireAdmin 需要管理员权限的中间件 -func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc { - return m.RequireRole(string(entity.UserRoleAdmin)) -} - -// RequirePhotographer 需要摄影师权限的中间件 -func (m *AuthMiddleware) RequirePhotographer() gin.HandlerFunc { - return m.RequireRole(string(entity.UserRolePhotographer)) -} - -// OptionalAuth 可选认证中间件 -func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.Next() - return - } - - if !strings.HasPrefix(authHeader, "Bearer ") { - c.Next() - return - } - - token := strings.TrimPrefix(authHeader, "Bearer ") - claims, err := m.jwtService.ValidateToken(token) - if err != nil { - c.Next() - return - } - - // 将用户信息存入上下文 - c.Set("user_id", claims.UserID) - c.Set("username", claims.Username) - c.Set("user_role", claims.Role) - - c.Next() - } -} - -// GetCurrentUser 获取当前用户ID -func GetCurrentUser(c *gin.Context) (uint, bool) { - userID, exists := c.Get("user_id") - if !exists { - return 0, false - } - - id, ok := userID.(uint) - return id, ok -} - -// GetCurrentUserRole 获取当前用户角色 -func GetCurrentUserRole(c *gin.Context) (string, bool) { - userRole, exists := c.Get("user_role") - if !exists { - return "", false - } - - role, ok := userRole.(string) - return role, ok -} - -// GetCurrentUsername 获取当前用户名 -func GetCurrentUsername(c *gin.Context) (string, bool) { - username, exists := c.Get("username") - if !exists { - return "", false - } - - name, ok := username.(string) - return name, ok -} - -// IsAuthenticated 检查是否已认证 -func IsAuthenticated(c *gin.Context) bool { - _, exists := c.Get("user_id") - return exists -} - -// IsAdmin 检查是否为管理员 -func IsAdmin(c *gin.Context) bool { - role, exists := GetCurrentUserRole(c) - if !exists { - return false - } - return role == string(entity.UserRoleAdmin) -} - -// IsPhotographer 检查是否为摄影师或以上 -func IsPhotographer(c *gin.Context) bool { - role, exists := GetCurrentUserRole(c) - if !exists { - return false - } - return role == string(entity.UserRolePhotographer) || role == string(entity.UserRoleAdmin) -} - -// hasPermission 检查权限 -func (m *AuthMiddleware) hasPermission(userRole, requiredRole string) bool { - roleLevel := map[string]int{ - string(entity.UserRoleUser): 1, - string(entity.UserRolePhotographer): 2, - string(entity.UserRoleAdmin): 3, - } - - userLevel, exists := roleLevel[userRole] - if !exists { - return false - } - - requiredLevel, exists := roleLevel[requiredRole] - if !exists { - return false - } - - return userLevel >= requiredLevel -} \ No newline at end of file diff --git a/backend-old/internal/api/middleware/cors.go b/backend-old/internal/api/middleware/cors.go deleted file mode 100644 index 75fb46d..0000000 --- a/backend-old/internal/api/middleware/cors.go +++ /dev/null @@ -1,58 +0,0 @@ -package middleware - -import ( - "net/http" - "github.com/gin-gonic/gin" - "photography-backend/internal/config" -) - -// CORSMiddleware CORS中间件 -func CORSMiddleware(cfg *config.CORSConfig) gin.HandlerFunc { - return func(c *gin.Context) { - origin := c.GetHeader("Origin") - - // 检查是否允许的来源 - allowed := false - for _, allowedOrigin := range cfg.AllowedOrigins { - if allowedOrigin == "*" || allowedOrigin == origin { - allowed = true - break - } - } - - if allowed { - c.Header("Access-Control-Allow-Origin", origin) - } - - // 设置其他CORS头 - c.Header("Access-Control-Allow-Methods", joinStrings(cfg.AllowedMethods, ", ")) - c.Header("Access-Control-Allow-Headers", joinStrings(cfg.AllowedHeaders, ", ")) - c.Header("Access-Control-Max-Age", "86400") - - if cfg.AllowCredentials { - c.Header("Access-Control-Allow-Credentials", "true") - } - - // 处理预检请求 - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(http.StatusNoContent) - return - } - - c.Next() - } -} - -// joinStrings 连接字符串数组 -func joinStrings(strs []string, sep string) string { - if len(strs) == 0 { - return "" - } - - result := strs[0] - for i := 1; i < len(strs); i++ { - result += sep + strs[i] - } - - return result -} \ No newline at end of file diff --git a/backend-old/internal/api/middleware/logger.go b/backend-old/internal/api/middleware/logger.go deleted file mode 100644 index 50b6db7..0000000 --- a/backend-old/internal/api/middleware/logger.go +++ /dev/null @@ -1,74 +0,0 @@ -package middleware - -import ( - "time" - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -// LoggerMiddleware 日志中间件 -func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc { - return func(c *gin.Context) { - start := time.Now() - path := c.Request.URL.Path - raw := c.Request.URL.RawQuery - - // 处理请求 - c.Next() - - // 计算延迟 - latency := time.Since(start) - - // 获取请求信息 - clientIP := c.ClientIP() - method := c.Request.Method - statusCode := c.Writer.Status() - bodySize := c.Writer.Size() - - if raw != "" { - path = path + "?" + raw - } - - // 记录日志 - logger.Info("HTTP Request", - zap.String("method", method), - zap.String("path", path), - zap.String("client_ip", clientIP), - zap.Int("status_code", statusCode), - zap.Int("body_size", bodySize), - zap.Duration("latency", latency), - zap.String("user_agent", c.Request.UserAgent()), - ) - } -} - -// RequestIDMiddleware 请求ID中间件 -func RequestIDMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - requestID := c.GetHeader("X-Request-ID") - if requestID == "" { - requestID = generateRequestID() - } - - c.Set("request_id", requestID) - c.Header("X-Request-ID", requestID) - - c.Next() - } -} - -// generateRequestID 生成请求ID -func generateRequestID() string { - // 简单实现,实际应用中可能需要更复杂的ID生成逻辑 - return time.Now().Format("20060102150405") + "-" + randomString(8) -} - -// randomString 生成随机字符串 -func randomString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, length) - for i := range b { - b[i] = charset[time.Now().UnixNano()%int64(len(charset))] - } - return string(b) -} \ No newline at end of file diff --git a/backend-old/internal/api/routes/routes.go b/backend-old/internal/api/routes/routes.go deleted file mode 100644 index f2f518c..0000000 --- a/backend-old/internal/api/routes/routes.go +++ /dev/null @@ -1,131 +0,0 @@ -package routes - -import ( - "photography-backend/internal/api/handlers" - "photography-backend/internal/api/middleware" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -type Handlers struct { - AuthHandler *handlers.AuthHandler - UserHandler *handlers.UserHandler - PhotoHandler *handlers.PhotoHandler - CategoryHandler *handlers.CategoryHandler - TagHandler *handlers.TagHandler -} - -func SetupRoutes(r *gin.Engine, h *Handlers, authMiddleware *middleware.AuthMiddleware, logger *zap.Logger) { - // 健康检查 - r.GET("/health", func(c *gin.Context) { - c.JSON(200, gin.H{"status": "ok"}) - }) - - // API 路由组 - api := r.Group("/api") - { - // 公开路由 - public := api.Group("") - { - // 认证相关 - auth := public.Group("/auth") - { - auth.POST("/login", h.AuthHandler.Login) - auth.POST("/refresh", h.AuthHandler.RefreshToken) - } - } - - // 需要认证的路由 - protected := api.Group("") - protected.Use(authMiddleware.RequireAuth()) - { - // 当前用户信息 - protected.GET("/me", h.UserHandler.GetCurrentUser) - protected.PUT("/me", h.UserHandler.UpdateCurrentUser) - protected.POST("/auth/logout", h.AuthHandler.Logout) - - // 照片管理 - photos := protected.Group("/photos") - { - photos.GET("", h.PhotoHandler.GetPhotos) - photos.POST("", h.PhotoHandler.CreatePhoto) - photos.GET("/stats", h.PhotoHandler.GetPhotoStats) - photos.POST("/upload", h.PhotoHandler.UploadPhoto) - photos.POST("/batch/update", h.PhotoHandler.BatchUpdatePhotos) - photos.POST("/batch/delete", h.PhotoHandler.BatchDeletePhotos) - photos.GET("/:id", h.PhotoHandler.GetPhoto) - photos.PUT("/:id", h.PhotoHandler.UpdatePhoto) - photos.DELETE("/:id", h.PhotoHandler.DeletePhoto) - } - - // 分类管理 - categories := protected.Group("/categories") - { - categories.GET("", h.CategoryHandler.GetCategories) - categories.POST("", h.CategoryHandler.CreateCategory) - categories.GET("/tree", h.CategoryHandler.GetCategoryTree) - categories.GET("/stats", h.CategoryHandler.GetCategoryStats) - categories.POST("/reorder", h.CategoryHandler.ReorderCategories) - categories.POST("/generate-slug", h.CategoryHandler.GenerateSlug) - categories.GET("/:id", h.CategoryHandler.GetCategory) - categories.PUT("/:id", h.CategoryHandler.UpdateCategory) - categories.DELETE("/:id", h.CategoryHandler.DeleteCategory) - categories.GET("/slug/:slug", h.CategoryHandler.GetCategoryBySlug) - } - - // 标签管理 - tags := protected.Group("/tags") - { - tags.GET("", h.TagHandler.GetTags) - tags.POST("", h.TagHandler.CreateTag) - tags.GET("/all", h.TagHandler.GetAllTags) - tags.GET("/popular", h.TagHandler.GetPopularTags) - tags.GET("/cloud", h.TagHandler.GetTagCloud) - tags.GET("/stats", h.TagHandler.GetTagStats) - tags.GET("/search", h.TagHandler.SearchTags) - tags.POST("/batch/delete", h.TagHandler.BatchDeleteTags) - tags.POST("/generate-slug", h.TagHandler.GenerateSlug) - tags.GET("/:id", h.TagHandler.GetTag) - tags.PUT("/:id", h.TagHandler.UpdateTag) - tags.DELETE("/:id", h.TagHandler.DeleteTag) - tags.GET("/slug/:slug", h.TagHandler.GetTagBySlug) - } - - // 用户管理 (需要管理员权限) - admin := protected.Group("/admin") - admin.Use(authMiddleware.RequireRole("admin")) - { - users := admin.Group("/users") - { - users.GET("", h.UserHandler.GetUsers) - users.POST("", h.UserHandler.CreateUser) - users.GET("/:id", h.UserHandler.GetUser) - users.PUT("/:id", h.UserHandler.UpdateUser) - users.DELETE("/:id", h.UserHandler.DeleteUser) - } - } - } - } - - // 前端公共 API (无需认证) - frontend := api.Group("/public") - { - // 公开的照片接口 - frontend.GET("/photos", h.PhotoHandler.GetPhotos) - frontend.GET("/photos/:id", h.PhotoHandler.GetPhoto) - - // 公开的分类接口 - frontend.GET("/categories", h.CategoryHandler.GetCategories) - frontend.GET("/categories/tree", h.CategoryHandler.GetCategoryTree) - frontend.GET("/categories/:id", h.CategoryHandler.GetCategory) - frontend.GET("/categories/slug/:slug", h.CategoryHandler.GetCategoryBySlug) - - // 公开的标签接口 - frontend.GET("/tags", h.TagHandler.GetTags) - frontend.GET("/tags/popular", h.TagHandler.GetPopularTags) - frontend.GET("/tags/cloud", h.TagHandler.GetTagCloud) - frontend.GET("/tags/:id", h.TagHandler.GetTag) - frontend.GET("/tags/slug/:slug", h.TagHandler.GetTagBySlug) - } -} \ No newline at end of file diff --git a/backend-old/internal/config/config.go b/backend-old/internal/config/config.go deleted file mode 100644 index 247787b..0000000 --- a/backend-old/internal/config/config.go +++ /dev/null @@ -1,229 +0,0 @@ -package config - -import ( - "fmt" - "time" - "github.com/spf13/viper" -) - -// Config 应用配置 -type Config struct { - App AppConfig `mapstructure:"app"` - Database DatabaseConfig `mapstructure:"database"` - Redis RedisConfig `mapstructure:"redis"` - JWT JWTConfig `mapstructure:"jwt"` - Storage StorageConfig `mapstructure:"storage"` - Upload UploadConfig `mapstructure:"upload"` - Logger LoggerConfig `mapstructure:"logger"` - CORS CORSConfig `mapstructure:"cors"` - RateLimit RateLimitConfig `mapstructure:"rate_limit"` -} - -// AppConfig 应用配置 -type AppConfig struct { - Name string `mapstructure:"name"` - Version string `mapstructure:"version"` - Environment string `mapstructure:"environment"` - Port int `mapstructure:"port"` - Debug bool `mapstructure:"debug"` -} - -// DatabaseConfig 数据库配置 -type DatabaseConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Username string `mapstructure:"username"` - 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 int `mapstructure:"conn_max_lifetime"` -} - -// RedisConfig Redis配置 -type RedisConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Password string `mapstructure:"password"` - Database int `mapstructure:"database"` - PoolSize int `mapstructure:"pool_size"` - MinIdleConns int `mapstructure:"min_idle_conns"` -} - -// JWTConfig JWT配置 -type JWTConfig struct { - Secret string `mapstructure:"secret"` - ExpiresIn string `mapstructure:"expires_in"` - RefreshExpiresIn string `mapstructure:"refresh_expires_in"` -} - -// StorageConfig 存储配置 -type StorageConfig struct { - Type string `mapstructure:"type"` - Local LocalConfig `mapstructure:"local"` - S3 S3Config `mapstructure:"s3"` -} - -// LocalConfig 本地存储配置 -type LocalConfig struct { - BasePath string `mapstructure:"base_path"` - BaseURL string `mapstructure:"base_url"` -} - -// S3Config S3存储配置 -type S3Config struct { - Region string `mapstructure:"region"` - Bucket string `mapstructure:"bucket"` - AccessKey string `mapstructure:"access_key"` - SecretKey string `mapstructure:"secret_key"` - Endpoint string `mapstructure:"endpoint"` -} - -// UploadConfig 上传配置 -type UploadConfig struct { - MaxFileSize int64 `mapstructure:"max_file_size"` - AllowedTypes []string `mapstructure:"allowed_types"` - ThumbnailSizes []ThumbnailSize `mapstructure:"thumbnail_sizes"` -} - -// ThumbnailSize 缩略图尺寸 -type ThumbnailSize struct { - Name string `mapstructure:"name"` - Width int `mapstructure:"width"` - Height int `mapstructure:"height"` -} - -// LoggerConfig 日志配置 -type LoggerConfig struct { - Level string `mapstructure:"level"` - Format string `mapstructure:"format"` - Output string `mapstructure:"output"` - Filename string `mapstructure:"filename"` - MaxSize int `mapstructure:"max_size"` - MaxAge int `mapstructure:"max_age"` - Compress bool `mapstructure:"compress"` -} - -// CORSConfig CORS配置 -type CORSConfig struct { - AllowedOrigins []string `mapstructure:"allowed_origins"` - AllowedMethods []string `mapstructure:"allowed_methods"` - AllowedHeaders []string `mapstructure:"allowed_headers"` - AllowCredentials bool `mapstructure:"allow_credentials"` -} - -// RateLimitConfig 限流配置 -type RateLimitConfig struct { - Enabled bool `mapstructure:"enabled"` - RequestsPerMinute int `mapstructure:"requests_per_minute"` - Burst int `mapstructure:"burst"` -} - - -// LoadConfig 加载配置 -func LoadConfig(configPath string) (*Config, error) { - viper.SetConfigFile(configPath) - viper.SetConfigType("yaml") - - // 设置环境变量前缀 - viper.SetEnvPrefix("PHOTOGRAPHY") - viper.AutomaticEnv() - - // 环境变量替换配置 - viper.BindEnv("database.host", "DB_HOST") - viper.BindEnv("database.port", "DB_PORT") - viper.BindEnv("database.username", "DB_USER") - viper.BindEnv("database.password", "DB_PASSWORD") - viper.BindEnv("database.database", "DB_NAME") - viper.BindEnv("redis.host", "REDIS_HOST") - viper.BindEnv("redis.port", "REDIS_PORT") - viper.BindEnv("redis.password", "REDIS_PASSWORD") - viper.BindEnv("jwt.secret", "JWT_SECRET") - viper.BindEnv("storage.s3.access_key", "AWS_ACCESS_KEY_ID") - viper.BindEnv("storage.s3.secret_key", "AWS_SECRET_ACCESS_KEY") - viper.BindEnv("app.port", "PORT") - - 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("config validation failed: %w", err) - } - - return &config, nil -} - -// validateConfig 验证配置 -func validateConfig(config *Config) error { - if config.App.Name == "" { - return fmt.Errorf("app name is required") - } - - if config.Database.Host == "" { - return fmt.Errorf("database host is required") - } - - if config.JWT.Secret == "" { - return fmt.Errorf("jwt secret is required") - } - - return nil -} - -// GetJWTExpiration 获取JWT过期时间 -func (c *Config) GetJWTExpiration() time.Duration { - duration, err := time.ParseDuration(c.JWT.ExpiresIn) - if err != nil { - return 24 * time.Hour // 默认24小时 - } - return duration -} - -// GetJWTRefreshExpiration 获取JWT刷新过期时间 -func (c *Config) GetJWTRefreshExpiration() time.Duration { - duration, err := time.ParseDuration(c.JWT.RefreshExpiresIn) - if err != nil { - return 7 * 24 * time.Hour // 默认7天 - } - return duration -} - -// GetDatabaseDSN 获取数据库DSN -func (c *Config) GetDatabaseDSN() string { - return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", - c.Database.Host, - c.Database.Port, - c.Database.Username, - c.Database.Password, - c.Database.Database, - c.Database.SSLMode, - ) -} - -// GetRedisAddr 获取Redis地址 -func (c *Config) GetRedisAddr() string { - return fmt.Sprintf("%s:%d", c.Redis.Host, c.Redis.Port) -} - -// GetServerAddr 获取服务器地址 -func (c *Config) GetServerAddr() string { - return fmt.Sprintf(":%d", c.App.Port) -} - -// IsDevelopment 是否为开发环境 -func (c *Config) IsDevelopment() bool { - return c.App.Environment == "development" -} - -// IsProduction 是否为生产环境 -func (c *Config) IsProduction() bool { - return c.App.Environment == "production" -} \ No newline at end of file diff --git a/backend-old/internal/database/database.go b/backend-old/internal/database/database.go deleted file mode 100644 index 62e06b9..0000000 --- a/backend-old/internal/database/database.go +++ /dev/null @@ -1,341 +0,0 @@ -package database - -import ( - "fmt" - "log" - "time" - - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - - "photography-backend/internal/config" - "photography-backend/internal/model/entity" -) - -// Database 数据库连接管理器 -type Database struct { - db *gorm.DB - config *config.DatabaseConfig -} - -// New 创建新的数据库连接 -func New(cfg *config.Config) (*Database, error) { - var db *gorm.DB - var err error - - // 配置 GORM 日志 - gormConfig := &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), - } - - if cfg.App.Environment == "production" { - gormConfig.Logger = logger.Default.LogMode(logger.Error) - } - - // 根据环境选择数据库 - if cfg.App.Environment == "test" || cfg.Database.Host == "" { - // 使用 SQLite 进行测试或开发 - db, err = gorm.Open(sqlite.Open("photography_dev.db"), gormConfig) - } else { - // 使用 PostgreSQL 进行生产 - dsn := cfg.GetDatabaseDSN() - db, err = gorm.Open(postgres.Open(dsn), gormConfig) - } - - if err != nil { - return nil, fmt.Errorf("failed to connect to database: %w", err) - } - - // 配置连接池 - sqlDB, err := db.DB() - if err != nil { - return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) - } - - // 设置连接池参数 - sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns) - sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns) - sqlDB.SetConnMaxLifetime(time.Duration(cfg.Database.ConnMaxLifetime) * time.Minute) - - // 测试连接 - if err := sqlDB.Ping(); err != nil { - return nil, fmt.Errorf("failed to ping database: %w", err) - } - - return &Database{ - db: db, - config: &cfg.Database, - }, nil -} - -// GetDB 获取数据库连接 -func (d *Database) GetDB() *gorm.DB { - return d.db -} - -// Close 关闭数据库连接 -func (d *Database) Close() error { - sqlDB, err := d.db.DB() - if err != nil { - return err - } - return sqlDB.Close() -} - -// AutoMigrate 自动迁移数据库表 -func (d *Database) AutoMigrate() error { - // 按依赖关系顺序迁移表 - entities := []interface{}{ - &entity.User{}, - &entity.Category{}, - &entity.Tag{}, - &entity.Album{}, - &entity.Photo{}, - &entity.PhotoTag{}, - } - - for _, entity := range entities { - if err := d.db.AutoMigrate(entity); err != nil { - return fmt.Errorf("failed to migrate %T: %w", entity, err) - } - } - - log.Println("Database migration completed successfully") - return nil -} - -// Seed 填充种子数据 -func (d *Database) Seed() error { - // 检查是否已有数据 - var userCount int64 - if err := d.db.Model(&entity.User{}).Count(&userCount).Error; err != nil { - return fmt.Errorf("failed to count users: %w", err) - } - - if userCount > 0 { - log.Println("Database already has data, skipping seed") - return nil - } - - // 创建事务 - tx := d.db.Begin() - if tx.Error != nil { - return fmt.Errorf("failed to begin transaction: %w", tx.Error) - } - defer tx.Rollback() - - // 创建默认用户 - if err := d.seedUsers(tx); err != nil { - return fmt.Errorf("failed to seed users: %w", err) - } - - // 创建默认分类 - if err := d.seedCategories(tx); err != nil { - return fmt.Errorf("failed to seed categories: %w", err) - } - - // 创建默认标签 - if err := d.seedTags(tx); err != nil { - return fmt.Errorf("failed to seed tags: %w", err) - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - - log.Println("Database seeding completed successfully") - return nil -} - -// seedUsers 创建默认用户 -func (d *Database) seedUsers(tx *gorm.DB) error { - users := []entity.User{ - { - Username: "admin", - Email: "admin@photography.com", - Password: "$2a$10$D4Zz6m3j1YJzp8Y7zW4l2OXcQ5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", // admin123 - Name: "管理员", - Role: entity.UserRoleAdmin, - IsActive: true, - IsPublic: true, - EmailVerified: true, - }, - { - Username: "photographer", - Email: "photographer@photography.com", - Password: "$2a$10$D4Zz6m3j1YJzp8Y7zW4l2OXcQ5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", // admin123 - Name: "摄影师", - Role: entity.UserRolePhotographer, - IsActive: true, - IsPublic: true, - EmailVerified: true, - }, - { - Username: "demo", - Email: "demo@photography.com", - Password: "$2a$10$D4Zz6m3j1YJzp8Y7zW4l2OXcQ5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", // admin123 - Name: "演示用户", - Role: entity.UserRoleUser, - IsActive: true, - IsPublic: true, - EmailVerified: true, - }, - } - - for _, user := range users { - if err := tx.Create(&user).Error; err != nil { - return fmt.Errorf("failed to create user %s: %w", user.Username, err) - } - } - - return nil -} - -// seedCategories 创建默认分类 -func (d *Database) seedCategories(tx *gorm.DB) error { - categories := []entity.Category{ - { - Name: "风景摄影", - Description: "自然风景摄影作品", - Color: "#10b981", - Sort: 1, - IsActive: true, - }, - { - Name: "人像摄影", - Description: "人物肖像摄影作品", - Color: "#f59e0b", - Sort: 2, - IsActive: true, - }, - { - Name: "街头摄影", - Description: "街头纪实摄影作品", - Color: "#ef4444", - Sort: 3, - IsActive: true, - }, - { - Name: "建筑摄影", - Description: "建筑和城市摄影作品", - Color: "#3b82f6", - Sort: 4, - IsActive: true, - }, - { - Name: "抽象摄影", - Description: "抽象艺术摄影作品", - Color: "#8b5cf6", - Sort: 5, - IsActive: true, - }, - } - - for _, category := range categories { - if err := tx.Create(&category).Error; err != nil { - return fmt.Errorf("failed to create category %s: %w", category.Name, err) - } - } - - return nil -} - -// seedTags 创建默认标签 -func (d *Database) seedTags(tx *gorm.DB) error { - tags := []entity.Tag{ - {Name: "自然", Color: "#10b981"}, - {Name: "人物", Color: "#f59e0b"}, - {Name: "城市", Color: "#3b82f6"}, - {Name: "夜景", Color: "#1f2937"}, - {Name: "黑白", Color: "#6b7280"}, - {Name: "色彩", Color: "#ec4899"}, - {Name: "构图", Color: "#8b5cf6"}, - {Name: "光影", Color: "#f97316"}, - {Name: "街头", Color: "#ef4444"}, - {Name: "建筑", Color: "#0891b2"}, - {Name: "风景", Color: "#10b981"}, - {Name: "抽象", Color: "#8b5cf6"}, - {Name: "微距", Color: "#84cc16"}, - {Name: "运动", Color: "#f97316"}, - {Name: "动物", Color: "#8b5cf6"}, - } - - for _, tag := range tags { - if err := tx.Create(&tag).Error; err != nil { - return fmt.Errorf("failed to create tag %s: %w", tag.Name, err) - } - } - - return nil -} - -// HealthCheck 健康检查 -func (d *Database) HealthCheck() error { - sqlDB, err := d.db.DB() - if err != nil { - return fmt.Errorf("failed to get underlying sql.DB: %w", err) - } - - if err := sqlDB.Ping(); err != nil { - return fmt.Errorf("database ping failed: %w", err) - } - - return nil -} - -// GetStats 获取数据库统计信息 -func (d *Database) GetStats() (map[string]interface{}, error) { - sqlDB, err := d.db.DB() - if err != nil { - return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) - } - - stats := sqlDB.Stats() - - // 获取表记录数 - var userCount, photoCount, albumCount, categoryCount, tagCount int64 - - d.db.Model(&entity.User{}).Count(&userCount) - d.db.Model(&entity.Photo{}).Count(&photoCount) - d.db.Model(&entity.Album{}).Count(&albumCount) - d.db.Model(&entity.Category{}).Count(&categoryCount) - d.db.Model(&entity.Tag{}).Count(&tagCount) - - return map[string]interface{}{ - "connection_stats": map[string]interface{}{ - "max_open_connections": stats.MaxOpenConnections, - "open_connections": stats.OpenConnections, - "in_use": stats.InUse, - "idle": stats.Idle, - }, - "table_counts": map[string]interface{}{ - "users": userCount, - "photos": photoCount, - "albums": albumCount, - "categories": categoryCount, - "tags": tagCount, - }, - }, nil -} - -// Transaction 执行事务 -func (d *Database) Transaction(fn func(*gorm.DB) error) error { - tx := d.db.Begin() - if tx.Error != nil { - return fmt.Errorf("failed to begin transaction: %w", tx.Error) - } - defer tx.Rollback() - - if err := fn(tx); err != nil { - return err - } - - if err := tx.Commit().Error; err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - - return nil -} \ No newline at end of file diff --git a/backend-old/internal/model/CLAUDE.md b/backend-old/internal/model/CLAUDE.md deleted file mode 100644 index 4ce63d4..0000000 --- a/backend-old/internal/model/CLAUDE.md +++ /dev/null @@ -1,684 +0,0 @@ -# Model Layer - CLAUDE.md - -本文件为 Claude Code 在数据模型层中工作时提供指导。 - -## 🎯 模块概览 - -Model 层负责定义数据结构、实体模型和数据传输对象,是整个应用的数据基础。 - -### 主要职责 -- 📦 定义数据库实体模型 -- 🔄 定义数据传输对象(DTO) -- 📝 定义请求和响应结构 -- 🔗 实现数据转换和验证 -- 📊 定义枚举和常量 - -## 📁 模块结构 - -``` -internal/model/ -├── CLAUDE.md # 📋 当前文件 - 数据模型设计指导 -├── entity/ # 📦 实体模型 -│ ├── user.go # 用户实体 -│ ├── photo.go # 照片实体 -│ ├── category.go # 分类实体 -│ ├── tag.go # 标签实体 -│ ├── album.go # 相册实体 -│ └── base.go # 基础实体 -├── dto/ # 🔄 数据传输对象 -│ ├── user_dto.go # 用户 DTO -│ ├── photo_dto.go # 照片 DTO -│ ├── category_dto.go # 分类 DTO -│ ├── auth_dto.go # 认证 DTO -│ └── common_dto.go # 通用 DTO -├── request/ # 📝 请求模型 -│ ├── user_request.go # 用户请求 -│ ├── photo_request.go # 照片请求 -│ ├── category_request.go # 分类请求 -│ └── auth_request.go # 认证请求 -├── response/ # 📤 响应模型 -│ ├── user_response.go # 用户响应 -│ ├── photo_response.go # 照片响应 -│ ├── category_response.go # 分类响应 -│ └── common_response.go # 通用响应 -└── types/ # 📊 类型定义 - ├── enums.go # 枚举类型 - ├── constants.go # 常量定义 - └── custom_types.go # 自定义类型 -``` - -## 🏗️ 实体模型设计 - -### 基础实体 -```go -// entity/base.go - 基础实体模型 -package entity - -import ( - "time" - "gorm.io/gorm" -) - -// BaseEntity 基础实体,包含通用字段 -type BaseEntity struct { - ID uint `gorm:"primaryKey" json:"id"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"` -} - -// CreatedBy 创建者字段 -type CreatedBy struct { - CreatedBy uint `gorm:"column:created_by;index" json:"created_by"` -} - -// UpdatedBy 更新者字段 -type UpdatedBy struct { - UpdatedBy uint `gorm:"column:updated_by;index" json:"updated_by"` -} - -// SoftDelete 软删除字段 -type SoftDelete struct { - DeletedBy uint `gorm:"column:deleted_by;index" json:"deleted_by,omitempty"` -} - -// TableName 实现 gorm.Tabler 接口 -func (BaseEntity) TableName() string { - return "" -} -``` - -### 用户实体 -```go -// entity/user.go - 用户实体 -package entity - -import ( - "time" - "gorm.io/gorm" -) - -// User 用户实体 -type User struct { - BaseEntity - - // 基本信息 - Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"` - Email string `gorm:"column:email;type:varchar(100);uniqueIndex;not null" json:"email"` - Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` - - // 个人信息 - FirstName string `gorm:"column:first_name;type:varchar(50)" json:"first_name"` - LastName string `gorm:"column:last_name;type:varchar(50)" json:"last_name"` - Avatar string `gorm:"column:avatar;type:varchar(255)" json:"avatar"` - Bio string `gorm:"column:bio;type:text" json:"bio"` - - // 系统字段 - Role UserRole `gorm:"column:role;type:varchar(20);default:'user'" json:"role"` - Status UserStatus `gorm:"column:status;type:varchar(20);default:'active'" json:"status"` - - // 时间字段 - LastLoginAt *time.Time `gorm:"column:last_login_at" json:"last_login_at"` - EmailVerifiedAt *time.Time `gorm:"column:email_verified_at" json:"email_verified_at"` - - // 关联关系 - Photos []Photo `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"photos,omitempty"` - Albums []Album `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"albums,omitempty"` - Categories []Category `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"categories,omitempty"` -} - -// UserRole 用户角色枚举 -type UserRole string - -const ( - UserRoleAdmin UserRole = "admin" - UserRoleEditor UserRole = "editor" - UserRoleUser UserRole = "user" -) - -// UserStatus 用户状态枚举 -type UserStatus string - -const ( - UserStatusActive UserStatus = "active" - UserStatusInactive UserStatus = "inactive" - UserStatusBanned UserStatus = "banned" -) - -// TableName 指定表名 -func (User) TableName() string { - return "users" -} - -// IsAdmin 检查是否为管理员 -func (u *User) IsAdmin() bool { - return u.Role == UserRoleAdmin -} - -// IsActive 检查是否为活跃用户 -func (u *User) IsActive() bool { - return u.Status == UserStatusActive -} - -// GetFullName 获取完整姓名 -func (u *User) GetFullName() string { - if u.FirstName == "" && u.LastName == "" { - return u.Username - } - return u.FirstName + " " + u.LastName -} -``` - -### 照片实体 -```go -// entity/photo.go - 照片实体 -package entity - -import ( - "time" -) - -// Photo 照片实体 -type Photo struct { - BaseEntity - - // 基本信息 - Title string `gorm:"column:title;type:varchar(255);not null" json:"title"` - Description string `gorm:"column:description;type:text" json:"description"` - - // 文件信息 - Filename string `gorm:"column:filename;type:varchar(255);not null" json:"filename"` - FilePath string `gorm:"column:file_path;type:varchar(500);not null" json:"file_path"` - FileSize int64 `gorm:"column:file_size;not null" json:"file_size"` - MimeType string `gorm:"column:mime_type;type:varchar(100);not null" json:"mime_type"` - - // 图片属性 - Width int `gorm:"column:width;default:0" json:"width"` - Height int `gorm:"column:height;default:0" json:"height"` - - // 元数据 - ExifData string `gorm:"column:exif_data;type:text" json:"exif_data,omitempty"` - TakenAt *time.Time `gorm:"column:taken_at" json:"taken_at"` - Location string `gorm:"column:location;type:varchar(255)" json:"location"` - Camera string `gorm:"column:camera;type:varchar(100)" json:"camera"` - Lens string `gorm:"column:lens;type:varchar(100)" json:"lens"` - - // 系统字段 - UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` - Status PhotoStatus `gorm:"column:status;type:varchar(20);default:'active'" json:"status"` - - // 统计字段 - ViewCount int `gorm:"column:view_count;default:0" json:"view_count"` - DownloadCount int `gorm:"column:download_count;default:0" json:"download_count"` - - // 关联关系 - User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"user,omitempty"` - Categories []Category `gorm:"many2many:photo_categories;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"categories,omitempty"` - Tags []Tag `gorm:"many2many:photo_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"tags,omitempty"` - Albums []Album `gorm:"many2many:album_photos;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"albums,omitempty"` -} - -// PhotoStatus 照片状态枚举 -type PhotoStatus string - -const ( - PhotoStatusActive PhotoStatus = "active" - PhotoStatusInactive PhotoStatus = "inactive" - PhotoStatusPrivate PhotoStatus = "private" - PhotoStatusDeleted PhotoStatus = "deleted" -) - -// TableName 指定表名 -func (Photo) TableName() string { - return "photos" -} - -// IsActive 检查照片是否可见 -func (p *Photo) IsActive() bool { - return p.Status == PhotoStatusActive -} - -// GetURL 获取照片URL -func (p *Photo) GetURL(baseURL string) string { - return baseURL + "/" + p.FilePath -} - -// GetThumbnailURL 获取缩略图URL -func (p *Photo) GetThumbnailURL(baseURL string) string { - return baseURL + "/thumbnails/" + p.FilePath -} -``` - -## 🔄 数据传输对象 - -### 用户 DTO -```go -// dto/user_dto.go - 用户数据传输对象 -package dto - -import ( - "time" - "photography-backend/internal/model/entity" -) - -// CreateUserRequest 创建用户请求 -type CreateUserRequest struct { - Username string `json:"username" binding:"required,min=3,max=50"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - FirstName string `json:"first_name" binding:"max=50"` - LastName string `json:"last_name" binding:"max=50"` -} - -// UpdateUserRequest 更新用户请求 -type UpdateUserRequest struct { - Username string `json:"username" binding:"omitempty,min=3,max=50"` - Email string `json:"email" binding:"omitempty,email"` - FirstName string `json:"first_name" binding:"max=50"` - LastName string `json:"last_name" binding:"max=50"` - Avatar string `json:"avatar" binding:"max=255"` - Bio string `json:"bio" binding:"max=500"` -} - -// ChangePasswordRequest 修改密码请求 -type ChangePasswordRequest struct { - CurrentPassword string `json:"current_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=6"` -} - -// UserResponse 用户响应 -type UserResponse struct { - ID uint `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Avatar string `json:"avatar"` - Bio string `json:"bio"` - Role string `json:"role"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// UserListResponse 用户列表响应 -type UserListResponse struct { - Users []UserResponse `json:"users"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// ListUsersOptions 用户列表选项 -type ListUsersOptions struct { - Page int `form:"page" binding:"min=1"` - Limit int `form:"limit" binding:"min=1,max=100"` - Sort string `form:"sort" binding:"oneof=id username email created_at"` - Order string `form:"order" binding:"oneof=asc desc"` - Status string `form:"status" binding:"oneof=active inactive banned"` - Role string `form:"role" binding:"oneof=admin editor user"` - Search string `form:"search" binding:"max=100"` -} - -// ToUserResponse 转换为用户响应 -func ToUserResponse(user *entity.User) *UserResponse { - return &UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - Avatar: user.Avatar, - Bio: user.Bio, - Role: string(user.Role), - Status: string(user.Status), - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } -} -``` - -### 照片 DTO -```go -// dto/photo_dto.go - 照片数据传输对象 -package dto - -import ( - "time" - "photography-backend/internal/model/entity" -) - -// CreatePhotoRequest 创建照片请求 -type CreatePhotoRequest struct { - Title string `json:"title" binding:"required,max=255"` - Description string `json:"description" binding:"max=1000"` - CategoryIDs []uint `json:"category_ids"` - TagIDs []uint `json:"tag_ids"` - AlbumIDs []uint `json:"album_ids"` - Location string `json:"location" binding:"max=255"` - TakenAt *time.Time `json:"taken_at"` -} - -// UpdatePhotoRequest 更新照片请求 -type UpdatePhotoRequest struct { - Title string `json:"title" binding:"omitempty,max=255"` - Description string `json:"description" binding:"max=1000"` - CategoryIDs []uint `json:"category_ids"` - TagIDs []uint `json:"tag_ids"` - AlbumIDs []uint `json:"album_ids"` - Location string `json:"location" binding:"max=255"` - TakenAt *time.Time `json:"taken_at"` - Status string `json:"status" binding:"oneof=active inactive private"` -} - -// PhotoResponse 照片响应 -type PhotoResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Filename string `json:"filename"` - FilePath string `json:"file_path"` - FileSize int64 `json:"file_size"` - MimeType string `json:"mime_type"` - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - ThumbnailURL string `json:"thumbnail_url"` - Location string `json:"location"` - TakenAt *time.Time `json:"taken_at"` - Status string `json:"status"` - ViewCount int `json:"view_count"` - UserID uint `json:"user_id"` - User *UserResponse `json:"user,omitempty"` - Categories []CategoryResponse `json:"categories,omitempty"` - Tags []TagResponse `json:"tags,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PhotoListResponse 照片列表响应 -type PhotoListResponse struct { - Photos []PhotoResponse `json:"photos"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// ListPhotosOptions 照片列表选项 -type ListPhotosOptions struct { - Page int `form:"page" binding:"min=1"` - Limit int `form:"limit" binding:"min=1,max=100"` - Sort string `form:"sort" binding:"oneof=id title created_at taken_at view_count"` - Order string `form:"order" binding:"oneof=asc desc"` - Status string `form:"status" binding:"oneof=active inactive private"` - CategoryID uint `form:"category_id"` - TagID uint `form:"tag_id"` - AlbumID uint `form:"album_id"` - UserID uint `form:"user_id"` - Search string `form:"search" binding:"max=100"` -} - -// SearchPhotosOptions 搜索照片选项 -type SearchPhotosOptions struct { - ListPhotosOptions - Query string `form:"query" binding:"required,min=1,max=100"` - Fields []string `form:"fields" binding:"dive,oneof=title description location"` - DateFrom *time.Time `form:"date_from"` - DateTo *time.Time `form:"date_to"` -} - -// ProcessPhotoOptions 处理照片选项 -type ProcessPhotoOptions struct { - Resize bool `json:"resize"` - Width int `json:"width"` - Height int `json:"height"` - Quality int `json:"quality"` - Watermark bool `json:"watermark"` - Thumbnail bool `json:"thumbnail"` -} - -// ToPhotoResponse 转换为照片响应 -func ToPhotoResponse(photo *entity.Photo, baseURL string) *PhotoResponse { - resp := &PhotoResponse{ - ID: photo.ID, - Title: photo.Title, - Description: photo.Description, - Filename: photo.Filename, - FilePath: photo.FilePath, - FileSize: photo.FileSize, - MimeType: photo.MimeType, - Width: photo.Width, - Height: photo.Height, - URL: photo.GetURL(baseURL), - ThumbnailURL: photo.GetThumbnailURL(baseURL), - Location: photo.Location, - TakenAt: photo.TakenAt, - Status: string(photo.Status), - ViewCount: photo.ViewCount, - UserID: photo.UserID, - CreatedAt: photo.CreatedAt, - UpdatedAt: photo.UpdatedAt, - } - - // 加载关联数据 - if photo.User.ID != 0 { - resp.User = ToUserResponse(&photo.User) - } - - if len(photo.Categories) > 0 { - resp.Categories = make([]CategoryResponse, len(photo.Categories)) - for i, category := range photo.Categories { - resp.Categories[i] = *ToCategoryResponse(&category) - } - } - - if len(photo.Tags) > 0 { - resp.Tags = make([]TagResponse, len(photo.Tags)) - for i, tag := range photo.Tags { - resp.Tags[i] = *ToTagResponse(&tag) - } - } - - return resp -} -``` - -## 📊 类型定义 - -### 枚举和常量 -```go -// types/enums.go - 枚举类型定义 -package types - -// SortOrder 排序方向 -type SortOrder string - -const ( - SortOrderAsc SortOrder = "asc" - SortOrderDesc SortOrder = "desc" -) - -// FileType 文件类型 -type FileType string - -const ( - FileTypeImage FileType = "image" - FileTypeVideo FileType = "video" - FileTypeAudio FileType = "audio" - FileTypeDocument FileType = "document" -) - -// MimeType 媒体类型 -const ( - MimeTypeJPEG = "image/jpeg" - MimeTypePNG = "image/png" - MimeTypeGIF = "image/gif" - MimeTypeWebP = "image/webp" -) - -// 分页常量 -const ( - DefaultPage = 1 - DefaultLimit = 20 - MaxLimit = 100 - DefaultSort = "created_at" - DefaultOrder = "desc" -) - -// 文件大小常量 -const ( - KB = 1024 - MB = KB * 1024 - GB = MB * 1024 - - MaxFileSize = 10 * MB // 10MB -) -``` - -### 自定义类型 -```go -// types/custom_types.go - 自定义类型 -package types - -import ( - "database/sql/driver" - "encoding/json" - "errors" -) - -// JSON 自定义JSON类型 -type JSON map[string]interface{} - -// Value 实现 driver.Valuer 接口 -func (j JSON) Value() (driver.Value, error) { - if j == nil { - return nil, nil - } - return json.Marshal(j) -} - -// Scan 实现 sql.Scanner 接口 -func (j *JSON) Scan(value interface{}) error { - if value == nil { - *j = nil - return nil - } - - bytes, ok := value.([]byte) - if !ok { - return errors.New("failed to scan JSON value") - } - - return json.Unmarshal(bytes, j) -} - -// StringArray 字符串数组类型 -type StringArray []string - -// Value 实现 driver.Valuer 接口 -func (s StringArray) Value() (driver.Value, error) { - if s == nil { - return nil, nil - } - return json.Marshal(s) -} - -// Scan 实现 sql.Scanner 接口 -func (s *StringArray) Scan(value interface{}) error { - if value == nil { - *s = nil - return nil - } - - bytes, ok := value.([]byte) - if !ok { - return errors.New("failed to scan StringArray value") - } - - return json.Unmarshal(bytes, s) -} -``` - -## 🎯 验证和转换 - -### 数据验证 -```go -// validation/validators.go - 自定义验证器 -package validation - -import ( - "regexp" - "strings" - - "github.com/go-playground/validator/v10" -) - -// RegisterCustomValidators 注册自定义验证器 -func RegisterCustomValidators(v *validator.Validate) { - v.RegisterValidation("username", validateUsername) - v.RegisterValidation("password", validatePassword) - v.RegisterValidation("phone", validatePhone) - v.RegisterValidation("slug", validateSlug) -} - -// validateUsername 验证用户名 -func validateUsername(fl validator.FieldLevel) bool { - username := fl.Field().String() - - // 3-50个字符,只允许字母、数字、下划线 - matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]{3,50}$`, username) - return matched -} - -// validatePassword 验证密码强度 -func validatePassword(fl validator.FieldLevel) bool { - password := fl.Field().String() - - // 至少6个字符,包含字母和数字 - if len(password) < 6 { - return false - } - - hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password) - hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password) - - return hasLetter && hasNumber -} - -// validatePhone 验证手机号 -func validatePhone(fl validator.FieldLevel) bool { - phone := fl.Field().String() - matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone) - return matched -} - -// validateSlug 验证 URL 友好字符串 -func validateSlug(fl validator.FieldLevel) bool { - slug := fl.Field().String() - matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug) - return matched -} -``` - -## 💡 最佳实践 - -### 模型设计原则 -1. **职责分离**: 实体、DTO、请求、响应分别定义 -2. **数据验证**: 使用标签进行数据验证 -3. **关联关系**: 合理定义实体间的关联关系 -4. **索引优化**: 为常用查询字段添加索引 -5. **软删除**: 重要数据使用软删除 - -### 性能优化 -1. **延迟加载**: 避免不必要的关联查询 -2. **选择性字段**: 只查询需要的字段 -3. **批量操作**: 使用批量插入和更新 -4. **缓存策略**: 缓存频繁访问的数据 - -### 安全考虑 -1. **敏感字段**: 密码等敏感字段不参与序列化 -2. **输入验证**: 严格验证所有输入数据 -3. **权限控制**: 在模型层实现基础权限检查 -4. **SQL 注入**: 使用 ORM 防止 SQL 注入 - -本模块是数据层的基础,确保模型设计的合理性和一致性是关键。 \ No newline at end of file diff --git a/backend-old/internal/model/dto/album_dto.go b/backend-old/internal/model/dto/album_dto.go deleted file mode 100644 index 9ec0626..0000000 --- a/backend-old/internal/model/dto/album_dto.go +++ /dev/null @@ -1,196 +0,0 @@ -package dto - -import ( - "time" - - "photography-backend/internal/model/entity" -) - -// CreateAlbumRequest 创建相册请求 -type CreateAlbumRequest struct { - Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"` - Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Slug string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"` - CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - IsPublic bool `json:"is_public" binding:"omitempty"` - IsFeatured bool `json:"is_featured" binding:"omitempty"` - Password string `json:"password" binding:"omitempty,min=6" validate:"omitempty,min=6"` -} - -// UpdateAlbumRequest 更新相册请求 -type UpdateAlbumRequest struct { - Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"` - Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Slug *string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"` - CoverPhotoID *uint `json:"cover_photo_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - IsPublic *bool `json:"is_public" binding:"omitempty"` - IsFeatured *bool `json:"is_featured" binding:"omitempty"` - Password *string `json:"password" binding:"omitempty,min=0" validate:"omitempty,min=0"` // 空字符串表示移除密码 - SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"` -} - -// AlbumResponse 相册响应 -type AlbumResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Slug string `json:"slug"` - CoverPhotoID *uint `json:"cover_photo_id"` - UserID uint `json:"user_id"` - CategoryID *uint `json:"category_id"` - IsPublic bool `json:"is_public"` - IsFeatured bool `json:"is_featured"` - HasPassword bool `json:"has_password"` - ViewCount int `json:"view_count"` - LikeCount int `json:"like_count"` - PhotoCount int `json:"photo_count"` - SortOrder int `json:"sort_order"` - User *UserResponse `json:"user,omitempty"` - Category *CategoryResponse `json:"category,omitempty"` - CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"` - RecentPhotos []PhotoListItem `json:"recent_photos,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// AlbumListItem 相册列表项(简化版) -type AlbumListItem struct { - ID uint `json:"id"` - Title string `json:"title"` - Slug string `json:"slug"` - IsPublic bool `json:"is_public"` - IsFeatured bool `json:"is_featured"` - HasPassword bool `json:"has_password"` - ViewCount int `json:"view_count"` - LikeCount int `json:"like_count"` - PhotoCount int `json:"photo_count"` - CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ListAlbumsOptions 相册列表查询选项 -type ListAlbumsOptions struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count" validate:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"` - IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"` - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` -} - -// AlbumListResponse 相册列表响应 -type AlbumListResponse struct { - Albums []AlbumListItem `json:"albums"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// AddPhotosToAlbumRequest 向相册添加照片请求 -type AddPhotosToAlbumRequest struct { - PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"` -} - -// RemovePhotosFromAlbumRequest 从相册移除照片请求 -type RemovePhotosFromAlbumRequest struct { - PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"` -} - -// AlbumPasswordRequest 相册密码验证请求 -type AlbumPasswordRequest struct { - Password string `json:"password" binding:"required" validate:"required"` -} - -// AlbumStatsResponse 相册统计响应 -type AlbumStatsResponse struct { - Total int64 `json:"total"` - Published int64 `json:"published"` - Private int64 `json:"private"` - Featured int64 `json:"featured"` - WithPassword int64 `json:"with_password"` - TotalViews int64 `json:"total_views"` - TotalLikes int64 `json:"total_likes"` - TotalPhotos int64 `json:"total_photos"` - CategoryCounts map[string]int64 `json:"category_counts"` - Recent []AlbumListItem `json:"recent"` - Popular []AlbumListItem `json:"popular"` -} - -// ConvertToAlbumResponse 将相册实体转换为响应DTO -func ConvertToAlbumResponse(album *entity.Album) *AlbumResponse { - if album == nil { - return nil - } - - response := &AlbumResponse{ - ID: album.ID, - Title: album.Title, - Description: album.Description, - Slug: album.Slug, - CoverPhotoID: album.CoverPhotoID, - UserID: album.UserID, - CategoryID: album.CategoryID, - IsPublic: album.IsPublic, - IsFeatured: album.IsFeatured, - HasPassword: album.HasPassword(), - ViewCount: album.ViewCount, - LikeCount: album.LikeCount, - PhotoCount: album.PhotoCount, - SortOrder: album.SortOrder, - CreatedAt: album.CreatedAt, - UpdatedAt: album.UpdatedAt, - } - - // 转换关联对象 - if album.User.ID != 0 { - response.User = ConvertToUserResponse(&album.User) - } - if album.Category != nil { - response.Category = ConvertToCategoryResponse(album.Category) - } - if album.CoverPhoto != nil { - coverPhoto := ConvertToPhotoListItem(album.CoverPhoto) - response.CoverPhoto = &coverPhoto - } - - // 转换最近照片 - if len(album.Photos) > 0 { - recentPhotos := make([]PhotoListItem, 0, len(album.Photos)) - for _, photo := range album.Photos { - recentPhotos = append(recentPhotos, ConvertToPhotoListItem(&photo)) - } - response.RecentPhotos = recentPhotos - } - - return response -} - -// ConvertToAlbumListItem 将相册实体转换为列表项DTO -func ConvertToAlbumListItem(album *entity.Album) AlbumListItem { - item := AlbumListItem{ - ID: album.ID, - Title: album.Title, - Slug: album.Slug, - IsPublic: album.IsPublic, - IsFeatured: album.IsFeatured, - HasPassword: album.HasPassword(), - ViewCount: album.ViewCount, - LikeCount: album.LikeCount, - PhotoCount: album.PhotoCount, - CreatedAt: album.CreatedAt, - UpdatedAt: album.UpdatedAt, - } - - // 转换封面照片 - if album.CoverPhoto != nil { - coverPhoto := ConvertToPhotoListItem(album.CoverPhoto) - item.CoverPhoto = &coverPhoto - } - - return item -} \ No newline at end of file diff --git a/backend-old/internal/model/dto/auth_dto.go b/backend-old/internal/model/dto/auth_dto.go deleted file mode 100644 index c5cde7c..0000000 --- a/backend-old/internal/model/dto/auth_dto.go +++ /dev/null @@ -1,107 +0,0 @@ -package dto - -import ( - "time" - - "photography-backend/internal/model/entity" -) - -// LoginRequest 登录请求 -type LoginRequest struct { - Email string `json:"email" binding:"required,email" validate:"required,email"` - Password string `json:"password" binding:"required" validate:"required"` -} - -// RegisterRequest 注册请求 -type RegisterRequest struct { - Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"` - Email string `json:"email" binding:"required,email" validate:"required,email"` - Password string `json:"password" binding:"required,min=6" validate:"required,min=6"` - Name string `json:"name" binding:"max=100" validate:"max=100"` -} - -// RefreshTokenRequest 刷新令牌请求 -type RefreshTokenRequest struct { - RefreshToken string `json:"refresh_token" binding:"required" validate:"required"` -} - -// TokenResponse 令牌响应 -type TokenResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - ExpiresAt time.Time `json:"expires_at"` -} - -// LoginResponse 登录响应 -type LoginResponse struct { - Token TokenResponse `json:"token"` - User UserResponse `json:"user"` -} - -// RegisterResponse 注册响应 -type RegisterResponse struct { - Token TokenResponse `json:"token"` - User UserResponse `json:"user"` -} - -// TokenClaims JWT 令牌声明 -type TokenClaims struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Email string `json:"email"` - Role entity.UserRole `json:"role"` - TokenID string `json:"token_id"` - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` -} - -// ResetPasswordRequest 重置密码请求 -type ResetPasswordRequest struct { - Email string `json:"email" binding:"required,email" validate:"required,email"` -} - -// ConfirmResetPasswordRequest 确认重置密码请求 -type ConfirmResetPasswordRequest struct { - Token string `json:"token" binding:"required" validate:"required"` - NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"` -} - -// VerifyEmailRequest 验证邮箱请求 -type VerifyEmailRequest struct { - Token string `json:"token" binding:"required" validate:"required"` -} - -// LogoutRequest 登出请求 -type LogoutRequest struct { - Token string `json:"token" binding:"required" validate:"required"` -} - -// AuthStatsResponse 认证统计响应 -type AuthStatsResponse struct { - TotalLogins int64 `json:"total_logins"` - ActiveSessions int64 `json:"active_sessions"` - FailedAttempts int64 `json:"failed_attempts"` - RecentLogins []LoginInfo `json:"recent_logins"` - LoginsByHour map[string]int64 `json:"logins_by_hour"` - LoginsByDevice map[string]int64 `json:"logins_by_device"` -} - -// LoginInfo 登录信息 -type LoginInfo struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Email string `json:"email"` - LoginTime time.Time `json:"login_time"` - IPAddress string `json:"ip_address"` - UserAgent string `json:"user_agent"` - Success bool `json:"success"` -} - -// ValidateTokenResponse 验证令牌响应 -type ValidateTokenResponse struct { - Valid bool `json:"valid"` - Claims *TokenClaims `json:"claims,omitempty"` - Error string `json:"error,omitempty"` -} \ No newline at end of file diff --git a/backend-old/internal/model/dto/category_dto.go b/backend-old/internal/model/dto/category_dto.go deleted file mode 100644 index 46f60e2..0000000 --- a/backend-old/internal/model/dto/category_dto.go +++ /dev/null @@ -1,143 +0,0 @@ -package dto - -import ( - "time" - - "photography-backend/internal/model/entity" -) - -// CreateCategoryRequest 创建分类请求 -type CreateCategoryRequest struct { - Name string `json:"name" binding:"required,min=1,max=100" validate:"required,min=1,max=100"` - Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Slug string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - ParentID *uint `json:"parent_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"` - CoverImage string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"` - SortOrder int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"` -} - -// UpdateCategoryRequest 更新分类请求 -type UpdateCategoryRequest struct { - Name *string `json:"name" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Slug *string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"` - CoverImage *string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"` - SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"` - IsActive *bool `json:"is_active" binding:"omitempty"` -} - -// CategoryResponse 分类响应 -type CategoryResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Slug string `json:"slug"` - ParentID *uint `json:"parent_id"` - Color string `json:"color"` - CoverImage string `json:"cover_image"` - Sort int `json:"sort"` - IsActive bool `json:"is_active"` - PhotoCount int64 `json:"photo_count"` - AlbumCount int64 `json:"album_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// CategoryTreeResponse 分类树响应 -type CategoryTreeResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Slug string `json:"slug"` - ParentID *uint `json:"parent_id"` - Color string `json:"color"` - CoverImage string `json:"cover_image"` - Sort int `json:"sort"` - IsActive bool `json:"is_active"` - PhotoCount int64 `json:"photo_count"` - AlbumCount int64 `json:"album_count"` - Children []CategoryTreeResponse `json:"children"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ListCategoriesOptions 分类列表查询选项 -type ListCategoriesOptions struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name sort created_at updated_at" validate:"omitempty,oneof=id name sort created_at updated_at"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - ParentID *uint `json:"parent_id" form:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"` - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` - WithCount bool `json:"with_count" form:"with_count" binding:"omitempty"` -} - -// CategoryListResponse 分类列表响应 -type CategoryListResponse struct { - Categories []CategoryResponse `json:"categories"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// ReorderCategoriesRequest 重新排序分类请求 -type ReorderCategoriesRequest struct { - ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - CategoryIDs []uint `json:"category_ids" binding:"required,min=1" validate:"required,min=1"` -} - -// CategoryStatsResponse 分类统计响应 -type CategoryStatsResponse struct { - Total int64 `json:"total"` - Active int64 `json:"active"` - TopLevel int64 `json:"top_level"` - PhotoCounts map[string]int64 `json:"photo_counts"` - Popular []CategoryResponse `json:"popular"` -} - -// ConvertToCategoryResponse 将分类实体转换为响应DTO -func ConvertToCategoryResponse(category *entity.Category) *CategoryResponse { - if category == nil { - return nil - } - - return &CategoryResponse{ - ID: category.ID, - Name: category.Name, - Description: category.Description, - ParentID: category.ParentID, - Color: category.Color, - CoverImage: category.CoverImage, - Sort: category.Sort, - IsActive: category.IsActive, - PhotoCount: category.PhotoCount, - CreatedAt: category.CreatedAt, - UpdatedAt: category.UpdatedAt, - } -} - -// ConvertToCategoryTreeResponse 将分类树转换为响应DTO -func ConvertToCategoryTreeResponse(tree []entity.CategoryTree) []CategoryTreeResponse { - result := make([]CategoryTreeResponse, len(tree)) - for i, category := range tree { - result[i] = CategoryTreeResponse{ - ID: category.ID, - Name: category.Name, - Description: category.Description, - ParentID: category.ParentID, - Color: category.Color, - CoverImage: category.CoverImage, - Sort: category.Sort, - IsActive: category.IsActive, - PhotoCount: category.PhotoCount, - Children: ConvertToCategoryTreeResponse(category.Children), - CreatedAt: category.CreatedAt, - UpdatedAt: category.UpdatedAt, - } - } - return result -} \ No newline at end of file diff --git a/backend-old/internal/model/dto/photo_dto.go b/backend-old/internal/model/dto/photo_dto.go deleted file mode 100644 index e00dee6..0000000 --- a/backend-old/internal/model/dto/photo_dto.go +++ /dev/null @@ -1,264 +0,0 @@ -package dto - -import ( - "mime/multipart" - "time" - - "photography-backend/internal/model/entity" -) - -// CreatePhotoRequest 创建照片请求 -type CreatePhotoRequest struct { - Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"` - Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Filename string `json:"filename" binding:"required" validate:"required"` - OriginalURL string `json:"original_url" binding:"required,url" validate:"required,url"` - FileSize int64 `json:"file_size" binding:"omitempty,min=0" validate:"omitempty,min=0"` - MimeType string `json:"mime_type" binding:"omitempty" validate:"omitempty"` - Width int `json:"width" binding:"omitempty,min=0" validate:"omitempty,min=0"` - Height int `json:"height" binding:"omitempty,min=0" validate:"omitempty,min=0"` - UserID uint `json:"user_id" binding:"required,min=1" validate:"required,min=1"` - AlbumID *uint `json:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"` - IsPublic bool `json:"is_public" binding:"omitempty"` - IsFeatured bool `json:"is_featured" binding:"omitempty"` -} - -// UpdatePhotoRequest 更新照片请求 -type UpdatePhotoRequest struct { - Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"` - Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - AlbumID *uint `json:"album_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"` - TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` - IsFeatured *bool `json:"is_featured" binding:"omitempty"` - LocationName *string `json:"location_name" binding:"omitempty,max=200" validate:"omitempty,max=200"` - Latitude *float64 `json:"latitude" binding:"omitempty,min=-90,max=90" validate:"omitempty,min=-90,max=90"` - Longitude *float64 `json:"longitude" binding:"omitempty,min=-180,max=180" validate:"omitempty,min=-180,max=180"` -} - -// UploadPhotoRequest 上传照片请求 -type UploadPhotoRequest struct { - File *multipart.FileHeader `form:"photo" binding:"required" validate:"required"` - Title string `form:"title" binding:"omitempty,max=200" validate:"omitempty,max=200"` - Description string `form:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - AlbumID *uint `form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - CategoryID *uint `form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - TagNames []string `form:"tag_names" binding:"omitempty" validate:"omitempty"` - IsPublic bool `form:"is_public" binding:"omitempty"` - IsFeatured bool `form:"is_featured" binding:"omitempty"` -} - -// PhotoResponse 照片响应 -type PhotoResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Filename string `json:"filename"` - OriginalURL string `json:"original_url"` - ThumbnailURL string `json:"thumbnail_url"` - MediumURL string `json:"medium_url"` - LargeURL string `json:"large_url"` - FileSize int64 `json:"file_size"` - MimeType string `json:"mime_type"` - Width int `json:"width"` - Height int `json:"height"` - AspectRatio float64 `json:"aspect_ratio"` - - // EXIF 信息 - CameraMake string `json:"camera_make"` - CameraModel string `json:"camera_model"` - LensModel string `json:"lens_model"` - FocalLength *float64 `json:"focal_length"` - Aperture *float64 `json:"aperture"` - ShutterSpeed string `json:"shutter_speed"` - ISO *int `json:"iso"` - TakenAt *time.Time `json:"taken_at"` - - // 地理位置 - LocationName string `json:"location_name"` - Latitude *float64 `json:"latitude"` - Longitude *float64 `json:"longitude"` - - // 关联信息 - UserID uint `json:"user_id"` - AlbumID *uint `json:"album_id"` - CategoryID *uint `json:"category_id"` - User *UserResponse `json:"user,omitempty"` - Album *AlbumResponse `json:"album,omitempty"` - Category *CategoryResponse `json:"category,omitempty"` - Tags []TagResponse `json:"tags,omitempty"` - - // 状态和统计 - IsPublic bool `json:"is_public"` - IsFeatured bool `json:"is_featured"` - ViewCount int `json:"view_count"` - LikeCount int `json:"like_count"` - DownloadCount int `json:"download_count"` - SortOrder int `json:"sort_order"` - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PhotoListItem 照片列表项(简化版) -type PhotoListItem struct { - ID uint `json:"id"` - Title string `json:"title"` - ThumbnailURL string `json:"thumbnail_url"` - Width int `json:"width"` - Height int `json:"height"` - AspectRatio float64 `json:"aspect_ratio"` - IsPublic bool `json:"is_public"` - IsFeatured bool `json:"is_featured"` - ViewCount int `json:"view_count"` - LikeCount int `json:"like_count"` - CreatedAt time.Time `json:"created_at"` -} - -// ListPhotosOptions 照片列表查询选项 -type ListPhotosOptions struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count" validate:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - AlbumID *uint `json:"album_id" form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"` - IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"` - IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"` - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` - Year *int `json:"year" form:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"` - Month *int `json:"month" form:"month" binding:"omitempty,min=1,max=12" validate:"omitempty,min=1,max=12"` -} - -// SearchPhotosOptions 照片搜索选项 -type SearchPhotosOptions struct { - Query string `json:"query" form:"query" binding:"required,min=1" validate:"required,min=1"` - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=relevance created_at view_count like_count" validate:"omitempty,oneof=relevance created_at view_count like_count"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"` - UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"` - IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"` -} - -// PhotoListResponse 照片列表响应 -type PhotoListResponse struct { - Photos []PhotoListItem `json:"photos"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// ProcessPhotoOptions 照片处理选项 -type ProcessPhotoOptions struct { - GenerateThumbnails bool `json:"generate_thumbnails"` - ThumbnailSizes []string `json:"thumbnail_sizes"` - ExtractEXIF bool `json:"extract_exif"` - GenerateHash bool `json:"generate_hash"` - OptimizeSize bool `json:"optimize_size"` - WatermarkEnabled bool `json:"watermark_enabled"` -} - -// PhotoStatsResponse 照片统计响应 -type PhotoStatsResponse struct { - Total int64 `json:"total"` - Published int64 `json:"published"` - Private int64 `json:"private"` - Featured int64 `json:"featured"` - TotalViews int64 `json:"total_views"` - TotalLikes int64 `json:"total_likes"` - TotalDownloads int64 `json:"total_downloads"` - FileSize int64 `json:"file_size"` - CategoryCounts map[string]int64 `json:"category_counts"` - TagCounts map[string]int64 `json:"tag_counts"` - Recent []PhotoListItem `json:"recent"` - Popular []PhotoListItem `json:"popular"` -} - -// ConvertToPhotoResponse 将照片实体转换为响应DTO -func ConvertToPhotoResponse(photo *entity.Photo) *PhotoResponse { - if photo == nil { - return nil - } - - response := &PhotoResponse{ - ID: photo.ID, - Title: photo.Title, - Description: photo.Description, - Filename: photo.Filename, - OriginalURL: photo.OriginalURL, - ThumbnailURL: photo.ThumbnailURL, - MediumURL: photo.MediumURL, - LargeURL: photo.LargeURL, - FileSize: photo.FileSize, - MimeType: photo.MimeType, - Width: photo.Width, - Height: photo.Height, - AspectRatio: photo.GetAspectRatio(), - - // EXIF - CameraMake: photo.CameraMake, - CameraModel: photo.CameraModel, - LensModel: photo.LensModel, - FocalLength: photo.FocalLength, - Aperture: photo.Aperture, - ShutterSpeed: photo.ShutterSpeed, - ISO: photo.ISO, - TakenAt: photo.TakenAt, - - // 地理位置 - LocationName: photo.LocationName, - Latitude: photo.Latitude, - Longitude: photo.Longitude, - - // 关联 - UserID: photo.UserID, - AlbumID: photo.AlbumID, - CategoryID: photo.CategoryID, - - // 状态 - IsPublic: photo.IsPublic, - IsFeatured: photo.IsFeatured, - ViewCount: photo.ViewCount, - LikeCount: photo.LikeCount, - DownloadCount: photo.DownloadCount, - SortOrder: photo.SortOrder, - - CreatedAt: photo.CreatedAt, - UpdatedAt: photo.UpdatedAt, - } - - // 转换关联对象 - if photo.User.ID != 0 { - response.User = ConvertToUserResponse(&photo.User) - } - if photo.Category != nil { - response.Category = ConvertToCategoryResponse(photo.Category) - } - - return response -} - -// ConvertToPhotoListItem 将照片实体转换为列表项DTO -func ConvertToPhotoListItem(photo *entity.Photo) PhotoListItem { - return PhotoListItem{ - ID: photo.ID, - Title: photo.Title, - ThumbnailURL: photo.ThumbnailURL, - Width: photo.Width, - Height: photo.Height, - AspectRatio: photo.GetAspectRatio(), - IsPublic: photo.IsPublic, - IsFeatured: photo.IsFeatured, - ViewCount: photo.ViewCount, - LikeCount: photo.LikeCount, - CreatedAt: photo.CreatedAt, - } -} \ No newline at end of file diff --git a/backend-old/internal/model/dto/tag_dto.go b/backend-old/internal/model/dto/tag_dto.go deleted file mode 100644 index f9c1a07..0000000 --- a/backend-old/internal/model/dto/tag_dto.go +++ /dev/null @@ -1,135 +0,0 @@ -package dto - -import ( - "time" - - "photography-backend/internal/model/entity" -) - -// CreateTagRequest 创建标签请求 -type CreateTagRequest struct { - Name string `json:"name" binding:"required,min=1,max=50" validate:"required,min=1,max=50"` - Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"` -} - -// UpdateTagRequest 更新标签请求 -type UpdateTagRequest struct { - Name *string `json:"name" binding:"omitempty,min=1,max=50" validate:"omitempty,min=1,max=50"` - Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"` - IsActive *bool `json:"is_active" binding:"omitempty"` -} - -// TagResponse 标签响应 -type TagResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Color string `json:"color"` - UseCount int `json:"use_count"` - IsActive bool `json:"is_active"` - IsPopular bool `json:"is_popular"` - PhotoCount int64 `json:"photo_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TagListItem 标签列表项(简化版) -type TagListItem struct { - ID uint `json:"id"` - Name string `json:"name"` - Color string `json:"color"` - UseCount int `json:"use_count"` - IsActive bool `json:"is_active"` - IsPopular bool `json:"is_popular"` -} - -// ListTagsOptions 标签列表查询选项 -type ListTagsOptions struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name use_count created_at updated_at" validate:"omitempty,oneof=id name use_count created_at updated_at"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"` - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` - Popular bool `json:"popular" form:"popular" binding:"omitempty"` -} - -// TagListResponse 标签列表响应 -type TagListResponse struct { - Tags []TagResponse `json:"tags"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// TagCloudResponse 标签云响应 -type TagCloudResponse struct { - Tags []TagCloudItem `json:"tags"` -} - -// TagCloudItem 标签云项 -type TagCloudItem struct { - ID uint `json:"id"` - Name string `json:"name"` - Color string `json:"color"` - UseCount int `json:"use_count"` - Weight int `json:"weight"` // 1-10 的权重,用于控制标签大小 -} - -// TagStatsResponse 标签统计响应 -type TagStatsResponse struct { - Total int64 `json:"total"` - Active int64 `json:"active"` - Popular []TagResponse `json:"popular"` - PhotoCounts map[string]int64 `json:"photo_counts"` - Recent []TagResponse `json:"recent"` -} - -// ConvertToTagResponse 将标签实体转换为响应DTO -func ConvertToTagResponse(tag *entity.Tag) *TagResponse { - if tag == nil { - return nil - } - - return &TagResponse{ - ID: tag.ID, - Name: tag.Name, - Color: tag.Color, - UseCount: tag.UseCount, - IsActive: tag.IsActive, - IsPopular: tag.IsPopular(), - CreatedAt: tag.CreatedAt, - UpdatedAt: tag.UpdatedAt, - } -} - -// ConvertToTagListItem 将标签实体转换为列表项DTO -func ConvertToTagListItem(tag *entity.Tag) TagListItem { - return TagListItem{ - ID: tag.ID, - Name: tag.Name, - Color: tag.Color, - UseCount: tag.UseCount, - IsActive: tag.IsActive, - IsPopular: tag.IsPopular(), - } -} - -// ConvertToTagCloudItem 将标签实体转换为标签云项 -func ConvertToTagCloudItem(tag *entity.Tag, maxUseCount int) TagCloudItem { - // 计算权重(1-10) - weight := 1 - if maxUseCount > 0 { - weight = int(float64(tag.UseCount)/float64(maxUseCount)*9) + 1 - if weight > 10 { - weight = 10 - } - } - - return TagCloudItem{ - ID: tag.ID, - Name: tag.Name, - Color: tag.Color, - UseCount: tag.UseCount, - Weight: weight, - } -} \ No newline at end of file diff --git a/backend-old/internal/model/dto/user_dto.go b/backend-old/internal/model/dto/user_dto.go deleted file mode 100644 index 6bc2522..0000000 --- a/backend-old/internal/model/dto/user_dto.go +++ /dev/null @@ -1,148 +0,0 @@ -package dto - -import ( - "time" - - "photography-backend/internal/model/entity" -) - -// CreateUserRequest 创建用户请求 -type CreateUserRequest struct { - Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"` - Email string `json:"email" binding:"required,email" validate:"required,email"` - Password string `json:"password" binding:"required,min=6" validate:"required,min=6"` - Name string `json:"name" binding:"max=100" validate:"max=100"` - Role entity.UserRole `json:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"` -} - -// UpdateUserRequest 更新用户请求 -type UpdateUserRequest struct { - Username *string `json:"username" binding:"omitempty,min=3,max=50" validate:"omitempty,min=3,max=50"` - Email *string `json:"email" binding:"omitempty,email" validate:"omitempty,email"` - Name *string `json:"name" binding:"omitempty,max=100" validate:"omitempty,max=100"` - Avatar *string `json:"avatar" binding:"omitempty,url" validate:"omitempty,url"` - Bio *string `json:"bio" binding:"omitempty,max=1000" validate:"omitempty,max=1000"` - Website *string `json:"website" binding:"omitempty,url" validate:"omitempty,url"` - Location *string `json:"location" binding:"omitempty,max=100" validate:"omitempty,max=100"` - IsActive *bool `json:"is_active" binding:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` -} - -// ChangePasswordRequest 修改密码请求 -type ChangePasswordRequest struct { - OldPassword string `json:"old_password" binding:"required" validate:"required"` - NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"` -} - -// UserResponse 用户响应 -type UserResponse struct { - ID uint `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Bio string `json:"bio"` - Website string `json:"website"` - Location string `json:"location"` - Role entity.UserRole `json:"role"` - IsActive bool `json:"is_active"` - IsPublic bool `json:"is_public"` - EmailVerified bool `json:"email_verified"` - LastLogin *time.Time `json:"last_login"` - LoginCount int `json:"login_count"` - PhotoCount int64 `json:"photo_count"` - AlbumCount int64 `json:"album_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// UserProfileResponse 用户档案响应(公开信息) -type UserProfileResponse struct { - ID uint `json:"id"` - Username string `json:"username"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Bio string `json:"bio"` - Website string `json:"website"` - Location string `json:"location"` - Role entity.UserRole `json:"role"` - PhotoCount int64 `json:"photo_count"` - AlbumCount int64 `json:"album_count"` - CreatedAt time.Time `json:"created_at"` -} - -// ListUsersOptions 用户列表查询选项 -type ListUsersOptions struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` - Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id username email created_at updated_at" validate:"omitempty,oneof=id username email created_at updated_at"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` - Role entity.UserRole `json:"role" form:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"` - IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"` - IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"` - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` -} - -// UserListResponse 用户列表响应 -type UserListResponse struct { - Users []UserResponse `json:"users"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -// UserStatsResponse 用户统计响应 -type UserStatsResponse struct { - Total int64 `json:"total"` - Active int64 `json:"active"` - Inactive int64 `json:"inactive"` - Verified int64 `json:"verified"` - Unverified int64 `json:"unverified"` - RoleCounts map[entity.UserRole]int64 `json:"role_counts"` - RecentLogins []UserResponse `json:"recent_logins"` -} - -// ConvertToUserResponse 将用户实体转换为响应DTO -func ConvertToUserResponse(user *entity.User) *UserResponse { - if user == nil { - return nil - } - - return &UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - Name: user.Name, - Avatar: user.Avatar, - Bio: user.Bio, - Website: user.Website, - Location: user.Location, - Role: user.Role, - IsActive: user.IsActive, - IsPublic: user.IsPublic, - EmailVerified: user.EmailVerified, - LastLogin: user.LastLogin, - LoginCount: user.LoginCount, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - } -} - -// ConvertToUserProfile 将用户实体转换为公开档案DTO -func ConvertToUserProfile(user *entity.User) *UserProfileResponse { - if user == nil { - return nil - } - - return &UserProfileResponse{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Avatar: user.Avatar, - Bio: user.Bio, - Website: user.Website, - Location: user.Location, - Role: user.Role, - CreatedAt: user.CreatedAt, - } -} \ No newline at end of file diff --git a/backend-old/internal/model/entity/album.go b/backend-old/internal/model/entity/album.go deleted file mode 100644 index 58a61dd..0000000 --- a/backend-old/internal/model/entity/album.go +++ /dev/null @@ -1,84 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -// Album 相册实体 -type Album struct { - ID uint `json:"id" gorm:"primarykey"` - Title string `json:"title" gorm:"not null;size:200"` - Description string `json:"description" gorm:"type:text"` - Slug string `json:"slug" gorm:"uniqueIndex;size:255"` - CoverPhotoID *uint `json:"cover_photo_id" gorm:"index"` - UserID uint `json:"user_id" gorm:"not null;index"` - CategoryID *uint `json:"category_id" gorm:"index"` - IsPublic bool `json:"is_public" gorm:"default:true;index"` - IsFeatured bool `json:"is_featured" gorm:"default:false;index"` - Password string `json:"-" gorm:"size:255"` // 私密相册密码 - ViewCount int `json:"view_count" gorm:"default:0;index"` - LikeCount int `json:"like_count" gorm:"default:0;index"` - PhotoCount int `json:"photo_count" gorm:"default:0;index"` - SortOrder int `json:"sort_order" gorm:"default:0;index"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` - - // 关联 - User User `json:"user,omitempty" gorm:"foreignKey:UserID"` - Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"` - CoverPhoto *Photo `json:"cover_photo,omitempty" gorm:"foreignKey:CoverPhotoID"` - Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:AlbumID"` -} - -// AlbumStats 相册统计信息 -type AlbumStats struct { - Total int64 `json:"total"` // 总相册数 - Published int64 `json:"published"` // 已发布相册数 - Private int64 `json:"private"` // 私有相册数 - Featured int64 `json:"featured"` // 精选相册数 - TotalViews int64 `json:"total_views"` // 总浏览量 - TotalLikes int64 `json:"total_likes"` // 总点赞数 - TotalPhotos int64 `json:"total_photos"` // 总照片数 - CategoryCounts map[string]int64 `json:"category_counts"` // 各分类相册数量 -} - -// TableName 指定表名 -func (Album) TableName() string { - return "albums" -} - -// HasPassword 检查是否设置了密码 -func (a *Album) HasPassword() bool { - return a.Password != "" -} - -// IsEmpty 检查相册是否为空 -func (a *Album) IsEmpty() bool { - return a.PhotoCount == 0 -} - -// CanViewBy 检查指定用户是否可以查看相册 -func (a *Album) CanViewBy(user *User) bool { - // 公开相册 - if a.IsPublic && !a.HasPassword() { - return true - } - - // 相册所有者或管理员 - if user != nil && (user.ID == a.UserID || user.IsAdmin()) { - return true - } - - return false -} - -// CanEditBy 检查指定用户是否可以编辑相册 -func (a *Album) CanEditBy(user *User) bool { - if user == nil { - return false - } - return user.ID == a.UserID || user.IsAdmin() -} \ No newline at end of file diff --git a/backend-old/internal/model/entity/category.go b/backend-old/internal/model/entity/category.go deleted file mode 100644 index b4613c2..0000000 --- a/backend-old/internal/model/entity/category.go +++ /dev/null @@ -1,131 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -// Category 分类实体 -type Category struct { - ID uint `json:"id" gorm:"primarykey"` - Name string `json:"name" gorm:"not null;size:100"` - Slug string `json:"slug" gorm:"uniqueIndex;not null;size:100"` - Description string `json:"description" gorm:"type:text"` - ParentID *uint `json:"parent_id" gorm:"index"` - Color string `json:"color" gorm:"default:#3b82f6;size:7"` - CoverImage string `json:"cover_image" gorm:"size:500"` - Sort int `json:"sort" gorm:"default:0;index"` - SortOrder int `json:"sort_order" gorm:"default:0;index"` - IsActive bool `json:"is_active" gorm:"default:true;index"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` - - // 关联 - Parent *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"` - Children []Category `json:"children,omitempty" gorm:"foreignKey:ParentID"` - Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:CategoryID"` - Albums []Album `json:"albums,omitempty" gorm:"foreignKey:CategoryID"` - PhotoCount int64 `json:"photo_count" gorm:"-"` // 照片数量,不存储在数据库中 -} - -// CategoryTree 分类树结构(用于前端显示) -type CategoryTree struct { - ID uint `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - ParentID *uint `json:"parent_id"` - Color string `json:"color"` - CoverImage string `json:"cover_image"` - Sort int `json:"sort"` - SortOrder int `json:"sort_order"` - IsActive bool `json:"is_active"` - PhotoCount int64 `json:"photo_count"` - Children []CategoryTree `json:"children"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// CategoryStats 分类统计信息 -type CategoryStats struct { - Total int64 `json:"total"` // 总分类数 - Active int64 `json:"active"` // 活跃分类数 - TopLevel int64 `json:"top_level"` // 顶级分类数 - TotalCategories int64 `json:"total_categories"` // 总分类数(别名) - MaxLevel int64 `json:"max_level"` // 最大层级 - FeaturedCount int64 `json:"featured_count"` // 特色分类数 - PhotoCounts map[string]int64 `json:"photo_counts"` // 各分类照片数量 -} - -// CategoryListParams 分类列表查询参数 -type CategoryListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Search string `json:"search" form:"search"` - ParentID *uint `json:"parent_id" form:"parent_id"` - IsActive *bool `json:"is_active" form:"is_active"` - IncludeStats bool `json:"include_stats" form:"include_stats"` - SortBy string `json:"sort_by" form:"sort_by"` - Order string `json:"order" form:"order"` -} - -// CreateCategoryRequest 创建分类请求 -type CreateCategoryRequest struct { - Name string `json:"name" binding:"required,max=100"` - Slug string `json:"slug" binding:"required,max=100"` - Description string `json:"description" binding:"max=500"` - ParentID *uint `json:"parent_id"` - Color string `json:"color" binding:"max=7"` - CoverImage string `json:"cover_image" binding:"max=500"` - Sort int `json:"sort"` -} - -// UpdateCategoryRequest 更新分类请求 -type UpdateCategoryRequest struct { - Name *string `json:"name" binding:"omitempty,max=100"` - Slug *string `json:"slug" binding:"omitempty,max=100"` - Description *string `json:"description" binding:"max=500"` - ParentID *uint `json:"parent_id"` - Color *string `json:"color" binding:"omitempty,max=7"` - CoverImage *string `json:"cover_image" binding:"omitempty,max=500"` - SortOrder *int `json:"sort_order"` - IsActive *bool `json:"is_active"` -} - -// ReorderCategoriesRequest 重新排序分类请求 -type ReorderCategoriesRequest struct { - ParentID *uint `json:"parent_id"` - CategoryIDs []uint `json:"category_ids" binding:"required,min=1"` -} - -// GenerateSlugRequest 生成slug请求 -type GenerateSlugRequest struct { - Name string `json:"name" binding:"required,max=100"` -} - -// GenerateSlugResponse 生成slug响应 -type GenerateSlugResponse struct { - Slug string `json:"slug"` -} - -// SuccessResponse 成功响应 -type SuccessResponse struct { - Message string `json:"message"` -} - -// TableName 指定表名 -func (Category) TableName() string { - return "categories" -} - -// IsTopLevel 检查是否为顶级分类 -func (c *Category) IsTopLevel() bool { - return c.ParentID == nil -} - -// HasChildren 检查是否有子分类 -func (c *Category) HasChildren() bool { - return len(c.Children) > 0 -} \ No newline at end of file diff --git a/backend-old/internal/model/entity/photo.go b/backend-old/internal/model/entity/photo.go deleted file mode 100644 index b7517f0..0000000 --- a/backend-old/internal/model/entity/photo.go +++ /dev/null @@ -1,244 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -// Photo 照片实体 -type Photo struct { - ID uint `json:"id" gorm:"primarykey"` - Title string `json:"title" gorm:"not null;size:200"` - Description string `json:"description" gorm:"type:text"` - Filename string `json:"filename" gorm:"not null;size:255"` - OriginalFilename string `json:"original_filename" gorm:"not null;size:255"` - UniqueFilename string `json:"unique_filename" gorm:"not null;size:255"` - FilePath string `json:"file_path" gorm:"not null;size:500"` - OriginalURL string `json:"original_url" gorm:"not null;size:500"` - ThumbnailURL string `json:"thumbnail_url" gorm:"size:500"` - MediumURL string `json:"medium_url" gorm:"size:500"` - LargeURL string `json:"large_url" gorm:"size:500"` - FileSize int64 `json:"file_size"` - MimeType string `json:"mime_type" gorm:"size:100"` - Width int `json:"width"` - Height int `json:"height"` - Status PhotoStatus `json:"status" gorm:"default:active;size:20"` - - // EXIF 信息 - Camera string `json:"camera" gorm:"size:100"` - Lens string `json:"lens" gorm:"size:100"` - CameraMake string `json:"camera_make" gorm:"size:100"` - CameraModel string `json:"camera_model" gorm:"size:100"` - LensModel string `json:"lens_model" gorm:"size:100"` - FocalLength *float64 `json:"focal_length" gorm:"type:decimal(5,2)"` - Aperture *float64 `json:"aperture" gorm:"type:decimal(3,1)"` - ShutterSpeed string `json:"shutter_speed" gorm:"size:20"` - ISO *int `json:"iso"` - TakenAt *time.Time `json:"taken_at"` - - // 地理位置信息 - LocationName string `json:"location_name" gorm:"size:200"` - Latitude *float64 `json:"latitude" gorm:"type:decimal(10,8)"` - Longitude *float64 `json:"longitude" gorm:"type:decimal(11,8)"` - - // 关联 - UserID uint `json:"user_id" gorm:"not null;index"` - AlbumID *uint `json:"album_id" gorm:"index"` - CategoryID *uint `json:"category_id" gorm:"index"` - - // 状态和统计 - IsPublic bool `json:"is_public" gorm:"default:true;index"` - IsFeatured bool `json:"is_featured" gorm:"default:false;index"` - ViewCount int `json:"view_count" gorm:"default:0;index"` - LikeCount int `json:"like_count" gorm:"default:0;index"` - DownloadCount int `json:"download_count" gorm:"default:0"` - SortOrder int `json:"sort_order" gorm:"default:0;index"` - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` - - // 关联对象 - User User `json:"user,omitempty" gorm:"foreignKey:UserID"` - Album *Album `json:"album,omitempty" gorm:"foreignKey:AlbumID"` - Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"` - Tags []Tag `json:"tags,omitempty" gorm:"many2many:photo_tags;"` -} - -// PhotoStatus 照片状态枚举 -type PhotoStatus string - -const ( - PhotoStatusActive PhotoStatus = "active" - PhotoStatusInactive PhotoStatus = "inactive" - PhotoStatusDeleted PhotoStatus = "deleted" - PhotoStatusDraft PhotoStatus = "draft" - PhotoStatusPrivate PhotoStatus = "private" -) - -// Status constants for compatibility -const ( - StatusPublished PhotoStatus = "active" - StatusDraft PhotoStatus = "draft" - StatusArchived PhotoStatus = "inactive" -) - -// PhotoTag 照片标签关联表 -type PhotoTag struct { - PhotoID uint `json:"photo_id" gorm:"primaryKey"` - TagID uint `json:"tag_id" gorm:"primaryKey"` -} - -// PhotoStats 照片统计信息 -type PhotoStats struct { - Total int64 `json:"total"` // 总照片数 - Published int64 `json:"published"` // 已发布照片数 - Private int64 `json:"private"` // 私有照片数 - Featured int64 `json:"featured"` // 精选照片数 - TotalViews int64 `json:"total_views"` // 总浏览量 - TotalLikes int64 `json:"total_likes"` // 总点赞数 - TotalDownloads int64 `json:"total_downloads"` // 总下载数 - FileSize int64 `json:"file_size"` // 总文件大小 - TotalSize int64 `json:"total_size"` // 总大小(别名) - ThisMonth int64 `json:"this_month"` // 本月新增 - Today int64 `json:"today"` // 今日新增 - StatusStats map[string]int64 `json:"status_stats"` // 状态统计 - CategoryCounts map[string]int64 `json:"category_counts"` // 各分类照片数量 - TagCounts map[string]int64 `json:"tag_counts"` // 各标签照片数量 -} - -// PhotoListParams 照片列表查询参数 -type PhotoListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Sort string `json:"sort" form:"sort"` - Order string `json:"order" form:"order"` - Search string `json:"search" form:"search"` - UserID *uint `json:"user_id" form:"user_id"` - Status *PhotoStatus `json:"status" form:"status"` - CategoryID *uint `json:"category_id" form:"category_id"` - TagID *uint `json:"tag_id" form:"tag_id"` - DateFrom *time.Time `json:"date_from" form:"date_from"` - DateTo *time.Time `json:"date_to" form:"date_to"` -} - -// CreatePhotoRequest 创建照片请求 -type CreatePhotoRequest struct { - Title string `json:"title" binding:"required,max=200"` - Description string `json:"description" binding:"max=1000"` - OriginalFilename string `json:"original_filename"` - FileSize int64 `json:"file_size"` - Status string `json:"status" binding:"oneof=active inactive"` - Camera string `json:"camera" binding:"max=100"` - Lens string `json:"lens" binding:"max=100"` - ISO *int `json:"iso"` - Aperture *float64 `json:"aperture"` - ShutterSpeed string `json:"shutter_speed" binding:"max=20"` - FocalLength *float64 `json:"focal_length"` - TakenAt *time.Time `json:"taken_at"` - CategoryIDs []uint `json:"category_ids"` - TagIDs []uint `json:"tag_ids"` -} - -// UpdatePhotoRequest 更新照片请求 -type UpdatePhotoRequest struct { - Title *string `json:"title" binding:"omitempty,max=200"` - Description *string `json:"description" binding:"max=1000"` - Status *string `json:"status" binding:"omitempty,oneof=active inactive"` - Camera *string `json:"camera" binding:"omitempty,max=100"` - Lens *string `json:"lens" binding:"omitempty,max=100"` - ISO *int `json:"iso"` - Aperture *float64 `json:"aperture"` - ShutterSpeed *string `json:"shutter_speed" binding:"omitempty,max=20"` - FocalLength *float64 `json:"focal_length"` - TakenAt *time.Time `json:"taken_at"` - CategoryIDs *[]uint `json:"category_ids"` - TagIDs *[]uint `json:"tag_ids"` -} - -// BatchUpdatePhotosRequest 批量更新照片请求 -type BatchUpdatePhotosRequest struct { - Status *string `json:"status" binding:"omitempty,oneof=active inactive"` - CategoryIDs *[]uint `json:"category_ids"` - TagIDs *[]uint `json:"tag_ids"` -} - -// PhotoFormat 照片格式 -type PhotoFormat struct { - ID uint `json:"id" gorm:"primarykey"` - PhotoID uint `json:"photo_id" gorm:"not null;index"` - Format string `json:"format" gorm:"not null;size:20"` // jpg, png, webp - Quality int `json:"quality" gorm:"not null"` // 1-100 - Width int `json:"width" gorm:"not null"` - Height int `json:"height" gorm:"not null"` - FileSize int64 `json:"file_size" gorm:"not null"` - URL string `json:"url" gorm:"not null;size:500"` - CreatedAt time.Time `json:"created_at"` -} - -func (PhotoFormat) TableName() string { - return "photo_formats" -} - -// TableName 指定表名 -func (Photo) TableName() string { - return "photos" -} - -// TableName 指定关联表名 -func (PhotoTag) TableName() string { - return "photo_tags" -} - -// GetAspectRatio 获取宽高比 -func (p *Photo) GetAspectRatio() float64 { - if p.Height == 0 { - return 0 - } - return float64(p.Width) / float64(p.Height) -} - -// IsLandscape 是否为横向 -func (p *Photo) IsLandscape() bool { - return p.Width > p.Height -} - -// IsPortrait 是否为纵向 -func (p *Photo) IsPortrait() bool { - return p.Width < p.Height -} - -// IsSquare 是否为正方形 -func (p *Photo) IsSquare() bool { - return p.Width == p.Height -} - -// HasLocation 是否有地理位置信息 -func (p *Photo) HasLocation() bool { - return p.Latitude != nil && p.Longitude != nil -} - -// HasEXIF 是否有EXIF信息 -func (p *Photo) HasEXIF() bool { - return p.CameraMake != "" || p.CameraModel != "" || p.TakenAt != nil -} - -// GetDisplayURL 获取显示URL(根据尺寸) -func (p *Photo) GetDisplayURL(size string) string { - switch size { - case "thumbnail": - if p.ThumbnailURL != "" { - return p.ThumbnailURL - } - case "medium": - if p.MediumURL != "" { - return p.MediumURL - } - case "large": - if p.LargeURL != "" { - return p.LargeURL - } - } - return p.OriginalURL -} \ No newline at end of file diff --git a/backend-old/internal/model/entity/tag.go b/backend-old/internal/model/entity/tag.go deleted file mode 100644 index 71bb7fe..0000000 --- a/backend-old/internal/model/entity/tag.go +++ /dev/null @@ -1,99 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -// Tag 标签实体 -type Tag struct { - ID uint `json:"id" gorm:"primarykey"` - Name string `json:"name" gorm:"uniqueIndex;not null;size:50"` - Slug string `json:"slug" gorm:"uniqueIndex;not null;size:50"` - Description string `json:"description" gorm:"type:text"` - Color string `json:"color" gorm:"default:#6b7280;size:7"` - UseCount int `json:"use_count" gorm:"default:0;index"` - PhotoCount int64 `json:"photo_count" gorm:"-"` // 不存储在数据库中 - IsActive bool `json:"is_active" gorm:"default:true;index"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` - - // 关联 - Photos []Photo `json:"photos,omitempty" gorm:"many2many:photo_tags;"` -} - -// TagStats 标签统计信息 -type TagStats struct { - Total int64 `json:"total"` // 总标签数 - Active int64 `json:"active"` // 活跃标签数 - Used int64 `json:"used"` // 已使用标签数 - Unused int64 `json:"unused"` // 未使用标签数 - AvgPhotosPerTag float64 `json:"avg_photos_per_tag"` // 平均每个标签的照片数 - Popular []Tag `json:"popular"` // 热门标签 - PhotoCounts map[string]int64 `json:"photo_counts"` // 各标签照片数量 -} - -// TagListParams 标签列表查询参数 -type TagListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Search string `json:"search" form:"search"` - IsActive *bool `json:"is_active" form:"is_active"` - SortBy string `json:"sort_by" form:"sort_by"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -// CreateTagRequest 创建标签请求 -type CreateTagRequest struct { - Name string `json:"name" binding:"required,max=50"` - Slug string `json:"slug" binding:"required,max=50"` - Description string `json:"description" binding:"max=500"` - Color string `json:"color" binding:"max=7"` -} - -// UpdateTagRequest 更新标签请求 -type UpdateTagRequest struct { - Name *string `json:"name" binding:"omitempty,max=50"` - Slug *string `json:"slug" binding:"omitempty,max=50"` - Description *string `json:"description" binding:"max=500"` - Color *string `json:"color" binding:"omitempty,max=7"` - IsActive *bool `json:"is_active"` -} - -// TagWithCount 带有照片数量的标签 -type TagWithCount struct { - Tag - PhotoCount int64 `json:"photo_count"` -} - -// TagCloudItem 标签云项目 -type TagCloudItem struct { - Name string `json:"name"` - Slug string `json:"slug"` - Color string `json:"color"` - Count int64 `json:"count"` -} - -// TableName 指定表名 -func (Tag) TableName() string { - return "tags" -} - -// IsPopular 检查是否为热门标签(使用次数 >= 10) -func (t *Tag) IsPopular() bool { - return t.UseCount >= 10 -} - -// IncrementUseCount 增加使用次数 -func (t *Tag) IncrementUseCount() { - t.UseCount++ -} - -// DecrementUseCount 减少使用次数 -func (t *Tag) DecrementUseCount() { - if t.UseCount > 0 { - t.UseCount-- - } -} \ No newline at end of file diff --git a/backend-old/internal/model/entity/user.go b/backend-old/internal/model/entity/user.go deleted file mode 100644 index 3716e2e..0000000 --- a/backend-old/internal/model/entity/user.go +++ /dev/null @@ -1,150 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -// UserRole 用户角色枚举 -type UserRole string - -const ( - UserRoleUser UserRole = "user" - UserRoleAdmin UserRole = "admin" - UserRolePhotographer UserRole = "photographer" -) - -// User 用户实体 -type User struct { - ID uint `json:"id" gorm:"primarykey"` - Username string `json:"username" gorm:"uniqueIndex;not null;size:50"` - Email string `json:"email" gorm:"uniqueIndex;not null;size:100"` - Password string `json:"-" gorm:"not null;size:255"` - Name string `json:"name" gorm:"size:100"` - Avatar string `json:"avatar" gorm:"size:500"` - Bio string `json:"bio" gorm:"type:text"` - Website string `json:"website" gorm:"size:200"` - Location string `json:"location" gorm:"size:100"` - Role UserRole `json:"role" gorm:"default:user;size:20"` - IsActive bool `json:"is_active" gorm:"default:true"` - IsPublic bool `json:"is_public" gorm:"default:true"` - EmailVerified bool `json:"email_verified" gorm:"default:false"` - LastLogin *time.Time `json:"last_login"` - LoginCount int `json:"login_count" gorm:"default:0"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` - - // 关联 - Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:UserID"` - Albums []Album `json:"albums,omitempty" gorm:"foreignKey:UserID"` -} - -// TableName 指定表名 -func (User) TableName() string { - return "users" -} - -// IsAdmin 检查是否为管理员 -func (u *User) IsAdmin() bool { - return u.Role == UserRoleAdmin -} - -// IsPhotographer 检查是否为摄影师 -func (u *User) IsPhotographer() bool { - return u.Role == UserRolePhotographer || u.Role == UserRoleAdmin -} - -// CanManagePhoto 检查是否可以管理指定照片 -func (u *User) CanManagePhoto(photo *Photo) bool { - return u.ID == photo.UserID || u.IsAdmin() -} - -// CanManageAlbum 检查是否可以管理指定相册 -func (u *User) CanManageAlbum(album *Album) bool { - return u.ID == album.UserID || u.IsAdmin() -} - -// UserStats 用户统计信息 -type UserStats struct { - Total int64 `json:"total"` // 总用户数 - Active int64 `json:"active"` // 活跃用户数 - ThisMonth int64 `json:"this_month"` // 本月新增 - Today int64 `json:"today"` // 今日新增 - RoleStats map[string]int64 `json:"role_stats"` // 角色统计 -} - -// CreateUserRequest 创建用户请求 -type CreateUserRequest struct { - Username string `json:"username" binding:"required,min=3,max=50"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - Name string `json:"name" binding:"max=100"` - Role UserRole `json:"role" binding:"oneof=user admin photographer"` -} - -// UpdateUserRequest 更新用户请求 -type UpdateUserRequest struct { - Username *string `json:"username" binding:"omitempty,min=3,max=50"` - Email *string `json:"email" binding:"omitempty,email"` - Name *string `json:"name" binding:"omitempty,max=100"` - Avatar *string `json:"avatar" binding:"omitempty,max=500"` - Bio *string `json:"bio" binding:"omitempty,max=1000"` - Website *string `json:"website" binding:"omitempty,max=200"` - Location *string `json:"location" binding:"omitempty,max=100"` - Role *UserRole `json:"role" binding:"omitempty,oneof=user admin photographer"` - IsActive *bool `json:"is_active"` -} - -// UpdateCurrentUserRequest 更新当前用户请求 -type UpdateCurrentUserRequest struct { - Username *string `json:"username" binding:"omitempty,min=3,max=50"` - Email *string `json:"email" binding:"omitempty,email"` - Name *string `json:"name" binding:"omitempty,max=100"` - Avatar *string `json:"avatar" binding:"omitempty,max=500"` - Bio *string `json:"bio" binding:"omitempty,max=1000"` - Website *string `json:"website" binding:"omitempty,max=200"` - Location *string `json:"location" binding:"omitempty,max=100"` -} - -// ChangePasswordRequest 修改密码请求 -type ChangePasswordRequest struct { - OldPassword string `json:"old_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=6"` -} - -// UserStatus 用户状态 -type UserStatus string - -const ( - UserStatusActive UserStatus = "active" - UserStatusInactive UserStatus = "inactive" - UserStatusBanned UserStatus = "banned" - UserStatusPending UserStatus = "pending" -) - -// UserListParams 用户列表查询参数 -type UserListParams struct { - Page int `json:"page"` - Limit int `json:"limit"` - Sort string `json:"sort"` - Order string `json:"order"` - Role *UserRole `json:"role"` - Status *UserStatus `json:"status"` - Search string `json:"search"` - CreatedFrom *time.Time `json:"created_from"` - CreatedTo *time.Time `json:"created_to"` - LastLoginFrom *time.Time `json:"last_login_from"` - LastLoginTo *time.Time `json:"last_login_to"` -} - -// UserGlobalStats 全局用户统计信息 -type UserGlobalStats struct { - Total int64 `json:"total"` - Active int64 `json:"active"` - Admins int64 `json:"admins"` - Editors int64 `json:"editors"` - Users int64 `json:"users"` - MonthlyRegistrations int64 `json:"monthly_registrations"` -} \ No newline at end of file diff --git a/backend-old/internal/model/request/common.go b/backend-old/internal/model/request/common.go deleted file mode 100644 index c3310fa..0000000 --- a/backend-old/internal/model/request/common.go +++ /dev/null @@ -1,90 +0,0 @@ -package request - -// PaginationRequest 分页请求 -type PaginationRequest struct { - Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"` - Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"` -} - -// SortRequest 排序请求 -type SortRequest struct { - Sort string `json:"sort" form:"sort" binding:"omitempty" validate:"omitempty"` - Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"` -} - -// SearchRequest 搜索请求 -type SearchRequest struct { - Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"` -} - -// BaseListRequest 基础列表请求 -type BaseListRequest struct { - PaginationRequest - SortRequest - SearchRequest -} - -// IDRequest ID 请求 -type IDRequest struct { - ID uint `json:"id" uri:"id" binding:"required,min=1" validate:"required,min=1"` -} - -// SlugRequest Slug 请求 -type SlugRequest struct { - Slug string `json:"slug" uri:"slug" binding:"required,min=1" validate:"required,min=1"` -} - -// BulkIDsRequest 批量 ID 请求 -type BulkIDsRequest struct { - IDs []uint `json:"ids" binding:"required,min=1" validate:"required,min=1"` -} - -// StatusRequest 状态请求 -type StatusRequest struct { - IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"` -} - -// TimeRangeRequest 时间范围请求 -type TimeRangeRequest struct { - StartDate string `json:"start_date" form:"start_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"` - EndDate string `json:"end_date" form:"end_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"` -} - -// GetDefaultPagination 获取默认分页参数 -func (p *PaginationRequest) GetDefaultPagination() (int, int) { - page := p.Page - if page <= 0 { - page = 1 - } - - limit := p.Limit - if limit <= 0 { - limit = 20 - } - if limit > 100 { - limit = 100 - } - - return page, limit -} - -// GetDefaultSort 获取默认排序参数 -func (s *SortRequest) GetDefaultSort(defaultSort, defaultOrder string) (string, string) { - sort := s.Sort - if sort == "" { - sort = defaultSort - } - - order := s.Order - if order == "" { - order = defaultOrder - } - - return sort, order -} - -// GetOffset 计算偏移量 -func (p *PaginationRequest) GetOffset() int { - page, limit := p.GetDefaultPagination() - return (page - 1) * limit -} \ No newline at end of file diff --git a/backend-old/internal/repository/CLAUDE.md b/backend-old/internal/repository/CLAUDE.md deleted file mode 100644 index d982b22..0000000 --- a/backend-old/internal/repository/CLAUDE.md +++ /dev/null @@ -1,873 +0,0 @@ -# Repository Layer - CLAUDE.md - -本文件为 Claude Code 在数据访问层中工作时提供指导。 - -## 🎯 模块概览 - -Repository 层负责数据访问逻辑,提供数据库操作的抽象接口,隔离业务逻辑与数据存储细节。 - -### 主要职责 -- 🔧 提供数据库操作接口 -- 📊 实现 CRUD 操作 -- 🔍 提供复杂查询支持 -- 💾 管理数据库连接和事务 -- 🚀 优化查询性能 -- 🔄 支持多种数据源 - -## 📁 模块结构 - -``` -internal/repository/ -├── CLAUDE.md # 📋 当前文件 - 数据访问开发指导 -├── interfaces/ # 🔗 仓储接口定义 -│ ├── user_repository.go # 用户仓储接口 -│ ├── photo_repository.go # 照片仓储接口 -│ ├── category_repository.go # 分类仓储接口 -│ ├── tag_repository.go # 标签仓储接口 -│ └── album_repository.go # 相册仓储接口 -├── postgres/ # 🐘 PostgreSQL 实现 -│ ├── user_repository.go # 用户仓储实现 -│ ├── photo_repository.go # 照片仓储实现 -│ ├── category_repository.go # 分类仓储实现 -│ ├── tag_repository.go # 标签仓储实现 -│ ├── album_repository.go # 相册仓储实现 -│ └── base_repository.go # 基础仓储实现 -├── redis/ # 🟥 Redis 缓存实现 -│ ├── user_cache.go # 用户缓存 -│ ├── photo_cache.go # 照片缓存 -│ └── cache_manager.go # 缓存管理器 -├── sqlite/ # 📦 SQLite 实现(开发用) -│ ├── user_repository.go # 用户仓储实现 -│ ├── photo_repository.go # 照片仓储实现 -│ └── base_repository.go # 基础仓储实现 -├── mocks/ # 🧪 模拟对象(测试用) -│ ├── user_repository_mock.go # 用户仓储模拟 -│ ├── photo_repository_mock.go # 照片仓储模拟 -│ └── generate.go # 模拟对象生成 -└── errors.go # 📝 仓储层错误定义 -``` - -## 🔗 接口设计 - -### 基础仓储接口 -```go -// interfaces/base_repository.go - 基础仓储接口 -package interfaces - -import ( - "context" - "gorm.io/gorm" -) - -// BaseRepositoryr 基础仓储接口 -type BaseRepositoryr[T any] interface { - // 基础 CRUD 操作 - Create(ctx context.Context, entity *T) (*T, error) - GetByID(ctx context.Context, id uint) (*T, error) - Update(ctx context.Context, entity *T) (*T, error) - Delete(ctx context.Context, id uint) error - - // 批量操作 - CreateBatch(ctx context.Context, entities []*T) error - UpdateBatch(ctx context.Context, entities []*T) error - DeleteBatch(ctx context.Context, ids []uint) error - - // 查询操作 - List(ctx context.Context, opts *ListOptions) ([]*T, int64, error) - Count(ctx context.Context, conditions map[string]interface{}) (int64, error) - Exists(ctx context.Context, conditions map[string]interface{}) (bool, error) - - // 事务操作 - WithTx(tx *gorm.DB) BaseRepositoryr[T] - Transaction(ctx context.Context, fn func(repo BaseRepositoryr[T]) error) error -} - -// ListOptions 查询选项 -type ListOptions struct { - Page int `json:"page"` - Limit int `json:"limit"` - Sort string `json:"sort"` - Order string `json:"order"` - Conditions map[string]interface{} `json:"conditions"` - Preloads []string `json:"preloads"` -} -``` - -### 用户仓储接口 -```go -// interfaces/user_repository.go - 用户仓储接口 -package interfaces - -import ( - "context" - "photography-backend/internal/model/entity" -) - -// UserRepositoryr 用户仓储接口 -type UserRepositoryr interface { - BaseRepositoryr[entity.User] - - // 用户特定查询 - GetByEmail(ctx context.Context, email string) (*entity.User, error) - GetByUsername(ctx context.Context, username string) (*entity.User, error) - GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) - - // 用户列表查询 - ListByRole(ctx context.Context, role entity.UserRole, opts *ListOptions) ([]*entity.User, int64, error) - ListByStatus(ctx context.Context, status entity.UserStatus, opts *ListOptions) ([]*entity.User, int64, error) - SearchUsers(ctx context.Context, keyword string, opts *ListOptions) ([]*entity.User, int64, error) - - // 用户统计 - CountByRole(ctx context.Context, role entity.UserRole) (int64, error) - CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error) - CountActiveUsers(ctx context.Context) (int64, error) - - // 用户状态更新 - UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error - UpdateLastLogin(ctx context.Context, id uint) error - - // 密码相关 - UpdatePassword(ctx context.Context, id uint, hashedPassword string) error - - // 软删除恢复 - Restore(ctx context.Context, id uint) error -} -``` - -### 照片仓储接口 -```go -// interfaces/photo_repository.go - 照片仓储接口 -package interfaces - -import ( - "context" - "time" - "photography-backend/internal/model/entity" -) - -// PhotoRepositoryr 照片仓储接口 -type PhotoRepositoryr interface { - BaseRepositoryr[entity.Photo] - - // 照片查询 - GetByFilename(ctx context.Context, filename string) (*entity.Photo, error) - GetByUserID(ctx context.Context, userID uint) ([]*entity.Photo, error) - ListByUserID(ctx context.Context, userID uint, opts *ListOptions) ([]*entity.Photo, int64, error) - ListByStatus(ctx context.Context, status entity.PhotoStatus, opts *ListOptions) ([]*entity.Photo, int64, error) - - // 分类和标签查询 - ListByCategory(ctx context.Context, categoryID uint, opts *ListOptions) ([]*entity.Photo, int64, error) - ListByTag(ctx context.Context, tagID uint, opts *ListOptions) ([]*entity.Photo, int64, error) - ListByAlbum(ctx context.Context, albumID uint, opts *ListOptions) ([]*entity.Photo, int64, error) - - // 搜索功能 - SearchPhotos(ctx context.Context, keyword string, opts *SearchOptions) ([]*entity.Photo, int64, error) - SearchByMetadata(ctx context.Context, metadata map[string]interface{}, opts *ListOptions) ([]*entity.Photo, int64, error) - - // 时间范围查询 - ListByDateRange(ctx context.Context, startDate, endDate time.Time, opts *ListOptions) ([]*entity.Photo, int64, error) - ListByCreatedDateRange(ctx context.Context, startDate, endDate time.Time, opts *ListOptions) ([]*entity.Photo, int64, error) - - // 统计查询 - CountByUser(ctx context.Context, userID uint) (int64, error) - CountByStatus(ctx context.Context, status entity.PhotoStatus) (int64, error) - CountByCategory(ctx context.Context, categoryID uint) (int64, error) - CountByTag(ctx context.Context, tagID uint) (int64, error) - - // 关联操作 - AddCategories(ctx context.Context, photoID uint, categoryIDs []uint) error - RemoveCategories(ctx context.Context, photoID uint, categoryIDs []uint) error - AddTags(ctx context.Context, photoID uint, tagIDs []uint) error - RemoveTags(ctx context.Context, photoID uint, tagIDs []uint) error - - // 统计更新 - IncrementViewCount(ctx context.Context, id uint) error - IncrementDownloadCount(ctx context.Context, id uint) error - - // 批量操作 - BatchUpdateStatus(ctx context.Context, ids []uint, status entity.PhotoStatus) error - BatchDelete(ctx context.Context, ids []uint) error -} - -// SearchOptions 搜索选项 -type SearchOptions struct { - ListOptions - Fields []string `json:"fields"` - DateFrom *time.Time `json:"date_from"` - DateTo *time.Time `json:"date_to"` - MinWidth int `json:"min_width"` - MaxWidth int `json:"max_width"` - MinHeight int `json:"min_height"` - MaxHeight int `json:"max_height"` -} -``` - -## 🔧 仓储实现 - -### 基础仓储实现 -```go -// postgres/base_repository.go - 基础仓储实现 -package postgres - -import ( - "context" - "errors" - "fmt" - "reflect" - - "gorm.io/gorm" - "go.uber.org/zap" - - "photography-backend/internal/repository/interfaces" - "photography-backend/pkg/logger" -) - -// BaseRepository 基础仓储实现 -type BaseRepository[T any] struct { - db *gorm.DB - logger logger.Logger -} - -// NewBaseRepository 创建基础仓储 -func NewBaseRepository[T any](db *gorm.DB, logger logger.Logger) *BaseRepository[T] { - return &BaseRepository[T]{ - db: db, - logger: logger, - } -} - -// Create 创建记录 -func (r *BaseRepository[T]) Create(ctx context.Context, entity *T) (*T, error) { - if err := r.db.WithContext(ctx).Create(entity).Error; err != nil { - r.logger.Error("failed to create entity", zap.Error(err)) - return nil, err - } - return entity, nil -} - -// GetByID 根据ID获取记录 -func (r *BaseRepository[T]) GetByID(ctx context.Context, id uint) (*T, error) { - var entity T - if err := r.db.WithContext(ctx).First(&entity, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - r.logger.Error("failed to get entity by id", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - return &entity, nil -} - -// Update 更新记录 -func (r *BaseRepository[T]) Update(ctx context.Context, entity *T) (*T, error) { - if err := r.db.WithContext(ctx).Save(entity).Error; err != nil { - r.logger.Error("failed to update entity", zap.Error(err)) - return nil, err - } - return entity, nil -} - -// Delete 删除记录 -func (r *BaseRepository[T]) Delete(ctx context.Context, id uint) error { - var entity T - if err := r.db.WithContext(ctx).Delete(&entity, id).Error; err != nil { - r.logger.Error("failed to delete entity", zap.Error(err), zap.Uint("id", id)) - return err - } - return nil -} - -// List 列表查询 -func (r *BaseRepository[T]) List(ctx context.Context, opts *interfaces.ListOptions) ([]*T, int64, error) { - var entities []*T - var total int64 - - db := r.db.WithContext(ctx) - - // 应用条件 - if opts.Conditions != nil { - for key, value := range opts.Conditions { - db = db.Where(key, value) - } - } - - // 获取总数 - if err := db.Model(new(T)).Count(&total).Error; err != nil { - r.logger.Error("failed to count entities", zap.Error(err)) - return nil, 0, err - } - - // 应用排序 - if opts.Sort != "" { - order := "ASC" - if opts.Order == "desc" { - order = "DESC" - } - db = db.Order(fmt.Sprintf("%s %s", opts.Sort, order)) - } - - // 应用分页 - if opts.Page > 0 && opts.Limit > 0 { - offset := (opts.Page - 1) * opts.Limit - db = db.Offset(offset).Limit(opts.Limit) - } - - // 应用预加载 - if opts.Preloads != nil { - for _, preload := range opts.Preloads { - db = db.Preload(preload) - } - } - - // 查询数据 - if err := db.Find(&entities).Error; err != nil { - r.logger.Error("failed to list entities", zap.Error(err)) - return nil, 0, err - } - - return entities, total, nil -} - -// Transaction 事务执行 -func (r *BaseRepository[T]) Transaction(ctx context.Context, fn func(repo interfaces.BaseRepositoryr[T]) error) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - repo := &BaseRepository[T]{ - db: tx, - logger: r.logger, - } - return fn(repo) - }) -} - -// WithTx 使用事务 -func (r *BaseRepository[T]) WithTx(tx *gorm.DB) interfaces.BaseRepositoryr[T] { - return &BaseRepository[T]{ - db: tx, - logger: r.logger, - } -} -``` - -### 用户仓储实现 -```go -// postgres/user_repository.go - 用户仓储实现 -package postgres - -import ( - "context" - "errors" - "strings" - "time" - - "gorm.io/gorm" - "go.uber.org/zap" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - "photography-backend/pkg/logger" -) - -// UserRepository 用户仓储实现 -type UserRepository struct { - *BaseRepository[entity.User] - db *gorm.DB - logger logger.Logger -} - -// NewUserRepository 创建用户仓储 -func NewUserRepository(db *gorm.DB, logger logger.Logger) interfaces.UserRepositoryr { - return &UserRepository{ - BaseRepository: NewBaseRepository[entity.User](db, logger), - db: db, - logger: logger, - } -} - -// GetByEmail 根据邮箱获取用户 -func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - r.logger.Error("failed to get user by email", zap.Error(err), zap.String("email", email)) - return nil, err - } - return &user, nil -} - -// GetByUsername 根据用户名获取用户 -func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - r.logger.Error("failed to get user by username", zap.Error(err), zap.String("username", username)) - return nil, err - } - return &user, nil -} - -// GetByEmailOrUsername 根据邮箱或用户名获取用户 -func (r *UserRepository) GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).Where("email = ? OR username = ?", emailOrUsername, emailOrUsername).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - r.logger.Error("failed to get user by email or username", zap.Error(err), zap.String("emailOrUsername", emailOrUsername)) - return nil, err - } - return &user, nil -} - -// SearchUsers 搜索用户 -func (r *UserRepository) SearchUsers(ctx context.Context, keyword string, opts *interfaces.ListOptions) ([]*entity.User, int64, error) { - var users []*entity.User - var total int64 - - db := r.db.WithContext(ctx) - - // 搜索条件 - searchCondition := fmt.Sprintf("username ILIKE %s OR email ILIKE %s OR first_name ILIKE %s OR last_name ILIKE %s", - "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") - db = db.Where(searchCondition) - - // 应用其他条件 - if opts.Conditions != nil { - for key, value := range opts.Conditions { - db = db.Where(key, value) - } - } - - // 获取总数 - if err := db.Model(&entity.User{}).Count(&total).Error; err != nil { - r.logger.Error("failed to count users", zap.Error(err)) - return nil, 0, err - } - - // 应用排序和分页 - if opts.Sort != "" { - order := "ASC" - if opts.Order == "desc" { - order = "DESC" - } - db = db.Order(fmt.Sprintf("%s %s", opts.Sort, order)) - } - - if opts.Page > 0 && opts.Limit > 0 { - offset := (opts.Page - 1) * opts.Limit - db = db.Offset(offset).Limit(opts.Limit) - } - - // 查询数据 - if err := db.Find(&users).Error; err != nil { - r.logger.Error("failed to search users", zap.Error(err)) - return nil, 0, err - } - - return users, total, nil -} - -// UpdateStatus 更新用户状态 -func (r *UserRepository) UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error { - err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("status", status).Error - if err != nil { - r.logger.Error("failed to update user status", zap.Error(err), zap.Uint("id", id), zap.String("status", string(status))) - return err - } - return nil -} - -// UpdateLastLogin 更新最后登录时间 -func (r *UserRepository) UpdateLastLogin(ctx context.Context, id uint) error { - now := time.Now() - err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("last_login_at", now).Error - if err != nil { - r.logger.Error("failed to update last login time", zap.Error(err), zap.Uint("id", id)) - return err - } - return nil -} - -// UpdatePassword 更新密码 -func (r *UserRepository) UpdatePassword(ctx context.Context, id uint, hashedPassword string) error { - err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("password", hashedPassword).Error - if err != nil { - r.logger.Error("failed to update password", zap.Error(err), zap.Uint("id", id)) - return err - } - return nil -} - -// CountByRole 按角色统计用户数 -func (r *UserRepository) CountByRole(ctx context.Context, role entity.UserRole) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.User{}).Where("role = ?", role).Count(&count).Error - if err != nil { - r.logger.Error("failed to count users by role", zap.Error(err), zap.String("role", string(role))) - return 0, err - } - return count, nil -} - -// CountActiveUsers 统计活跃用户数 -func (r *UserRepository) CountActiveUsers(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.User{}).Where("status = ?", entity.UserStatusActive).Count(&count).Error - if err != nil { - r.logger.Error("failed to count active users", zap.Error(err)) - return 0, err - } - return count, nil -} -``` - -## 💾 Redis 缓存实现 - -### 缓存管理器 -```go -// redis/cache_manager.go - 缓存管理器 -package redis - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/go-redis/redis/v8" - "go.uber.org/zap" - - "photography-backend/pkg/logger" -) - -// CacheManager 缓存管理器 -type CacheManager struct { - client *redis.Client - logger logger.Logger -} - -// NewCacheManager 创建缓存管理器 -func NewCacheManager(client *redis.Client, logger logger.Logger) *CacheManager { - return &CacheManager{ - client: client, - logger: logger, - } -} - -// Set 设置缓存 -func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { - data, err := json.Marshal(value) - if err != nil { - cm.logger.Error("failed to marshal cache value", zap.Error(err), zap.String("key", key)) - return err - } - - err = cm.client.Set(ctx, key, data, ttl).Err() - if err != nil { - cm.logger.Error("failed to set cache", zap.Error(err), zap.String("key", key)) - return err - } - - return nil -} - -// Get 获取缓存 -func (cm *CacheManager) Get(ctx context.Context, key string, dest interface{}) error { - data, err := cm.client.Get(ctx, key).Result() - if err != nil { - if err == redis.Nil { - return ErrCacheNotFound - } - cm.logger.Error("failed to get cache", zap.Error(err), zap.String("key", key)) - return err - } - - err = json.Unmarshal([]byte(data), dest) - if err != nil { - cm.logger.Error("failed to unmarshal cache value", zap.Error(err), zap.String("key", key)) - return err - } - - return nil -} - -// Delete 删除缓存 -func (cm *CacheManager) Delete(ctx context.Context, key string) error { - err := cm.client.Del(ctx, key).Err() - if err != nil { - cm.logger.Error("failed to delete cache", zap.Error(err), zap.String("key", key)) - return err - } - return nil -} - -// DeletePattern 批量删除缓存 -func (cm *CacheManager) DeletePattern(ctx context.Context, pattern string) error { - keys, err := cm.client.Keys(ctx, pattern).Result() - if err != nil { - cm.logger.Error("failed to get keys by pattern", zap.Error(err), zap.String("pattern", pattern)) - return err - } - - if len(keys) > 0 { - err = cm.client.Del(ctx, keys...).Err() - if err != nil { - cm.logger.Error("failed to delete keys by pattern", zap.Error(err), zap.String("pattern", pattern)) - return err - } - } - - return nil -} - -// Exists 检查缓存是否存在 -func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) { - count, err := cm.client.Exists(ctx, key).Result() - if err != nil { - cm.logger.Error("failed to check cache existence", zap.Error(err), zap.String("key", key)) - return false, err - } - return count > 0, nil -} - -// SetTTL 设置过期时间 -func (cm *CacheManager) SetTTL(ctx context.Context, key string, ttl time.Duration) error { - err := cm.client.Expire(ctx, key, ttl).Err() - if err != nil { - cm.logger.Error("failed to set cache ttl", zap.Error(err), zap.String("key", key)) - return err - } - return nil -} -``` - -### 用户缓存 -```go -// redis/user_cache.go - 用户缓存 -package redis - -import ( - "context" - "fmt" - "time" - - "photography-backend/internal/model/entity" - "photography-backend/pkg/logger" -) - -// UserCache 用户缓存 -type UserCache struct { - *CacheManager - ttl time.Duration -} - -// NewUserCache 创建用户缓存 -func NewUserCache(cm *CacheManager, ttl time.Duration) *UserCache { - return &UserCache{ - CacheManager: cm, - ttl: ttl, - } -} - -// SetUser 缓存用户 -func (uc *UserCache) SetUser(ctx context.Context, user *entity.User) error { - key := fmt.Sprintf("user:id:%d", user.ID) - return uc.Set(ctx, key, user, uc.ttl) -} - -// GetUser 获取用户缓存 -func (uc *UserCache) GetUser(ctx context.Context, id uint) (*entity.User, error) { - key := fmt.Sprintf("user:id:%d", id) - var user entity.User - err := uc.Get(ctx, key, &user) - if err != nil { - return nil, err - } - return &user, nil -} - -// SetUserByEmail 按邮箱缓存用户 -func (uc *UserCache) SetUserByEmail(ctx context.Context, user *entity.User) error { - key := fmt.Sprintf("user:email:%s", user.Email) - return uc.Set(ctx, key, user, uc.ttl) -} - -// GetUserByEmail 按邮箱获取用户缓存 -func (uc *UserCache) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) { - key := fmt.Sprintf("user:email:%s", email) - var user entity.User - err := uc.Get(ctx, key, &user) - if err != nil { - return nil, err - } - return &user, nil -} - -// DeleteUser 删除用户缓存 -func (uc *UserCache) DeleteUser(ctx context.Context, id uint) error { - key := fmt.Sprintf("user:id:%d", id) - return uc.Delete(ctx, key) -} - -// DeleteUserByEmail 按邮箱删除用户缓存 -func (uc *UserCache) DeleteUserByEmail(ctx context.Context, email string) error { - key := fmt.Sprintf("user:email:%s", email) - return uc.Delete(ctx, key) -} - -// InvalidateUserCache 失效用户相关缓存 -func (uc *UserCache) InvalidateUserCache(ctx context.Context, userID uint) error { - pattern := fmt.Sprintf("user:*:%d", userID) - return uc.DeletePattern(ctx, pattern) -} -``` - -## 🔍 错误处理 - -### 仓储层错误定义 -```go -// errors.go - 仓储层错误定义 -package repository - -import "errors" - -var ( - // 通用错误 - ErrNotFound = errors.New("record not found") - ErrDuplicateKey = errors.New("duplicate key") - ErrInvalidParameter = errors.New("invalid parameter") - ErrDatabaseError = errors.New("database error") - ErrTransactionError = errors.New("transaction error") - - // 缓存错误 - ErrCacheNotFound = errors.New("cache not found") - ErrCacheError = errors.New("cache error") - ErrCacheExpired = errors.New("cache expired") - - // 连接错误 - ErrConnectionFailed = errors.New("connection failed") - ErrConnectionTimeout = errors.New("connection timeout") -) -``` - -## 🧪 测试 - -### 仓储测试 -```go -// postgres/user_repository_test.go - 用户仓储测试 -package postgres - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - - "photography-backend/internal/model/entity" - "photography-backend/pkg/logger" -) - -type UserRepositoryTestSuite struct { - suite.Suite - db *gorm.DB - repo *UserRepository -} - -func (suite *UserRepositoryTestSuite) SetupTest() { - // 使用内存数据库进行测试 - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - suite.Require().NoError(err) - - // 自动迁移 - err = db.AutoMigrate(&entity.User{}) - suite.Require().NoError(err) - - suite.db = db - suite.repo = NewUserRepository(db, logger.NewNoop()).(*UserRepository) -} - -func (suite *UserRepositoryTestSuite) TearDownTest() { - sqlDB, _ := suite.db.DB() - sqlDB.Close() -} - -func (suite *UserRepositoryTestSuite) TestCreateUser() { - ctx := context.Background() - - user := &entity.User{ - Username: "testuser", - Email: "test@example.com", - Password: "hashedpassword", - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - createdUser, err := suite.repo.Create(ctx, user) - - assert.NoError(suite.T(), err) - assert.NotZero(suite.T(), createdUser.ID) - assert.Equal(suite.T(), user.Username, createdUser.Username) - assert.Equal(suite.T(), user.Email, createdUser.Email) -} - -func (suite *UserRepositoryTestSuite) TestGetUserByEmail() { - ctx := context.Background() - - // 创建测试用户 - user := &entity.User{ - Username: "testuser", - Email: "test@example.com", - Password: "hashedpassword", - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - createdUser, err := suite.repo.Create(ctx, user) - suite.Require().NoError(err) - - // 根据邮箱获取用户 - foundUser, err := suite.repo.GetByEmail(ctx, user.Email) - - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), createdUser.ID, foundUser.ID) - assert.Equal(suite.T(), createdUser.Email, foundUser.Email) -} - -func TestUserRepositoryTestSuite(t *testing.T) { - suite.Run(t, new(UserRepositoryTestSuite)) -} -``` - -## 💡 最佳实践 - -### 设计原则 -1. **接口隔离**: 定义清晰的仓储接口 -2. **依赖倒置**: 依赖接口而非具体实现 -3. **单一职责**: 每个仓储只负责一个实体 -4. **错误处理**: 统一错误处理和日志记录 -5. **事务支持**: 提供事务操作支持 - -### 性能优化 -1. **查询优化**: 使用适当的索引和查询条件 -2. **批量操作**: 支持批量插入和更新 -3. **缓存策略**: 合理使用缓存减少数据库访问 -4. **连接池**: 使用连接池管理数据库连接 -5. **预加载**: 避免 N+1 查询问题 - -### 测试策略 -1. **单元测试**: 为每个仓储编写单元测试 -2. **集成测试**: 测试数据库交互 -3. **模拟对象**: 使用 Mock 对象进行测试 -4. **测试数据**: 准备充分的测试数据 -5. **性能测试**: 测试查询性能和并发性能 - -本模块是数据访问的核心,确保数据操作的正确性和性能是关键。 \ No newline at end of file diff --git a/backend-old/internal/repository/interfaces/category_repository.go b/backend-old/internal/repository/interfaces/category_repository.go deleted file mode 100644 index d5e191a..0000000 --- a/backend-old/internal/repository/interfaces/category_repository.go +++ /dev/null @@ -1,39 +0,0 @@ -package interfaces - -import ( - "context" - "photography-backend/internal/model/entity" -) - -// CategoryRepository 分类仓储接口 -type CategoryRepository interface { - // 基本CRUD操作 - Create(ctx context.Context, category *entity.Category) error - GetByID(ctx context.Context, id uint) (*entity.Category, error) - GetBySlug(ctx context.Context, slug string) (*entity.Category, error) - Update(ctx context.Context, category *entity.Category) error - Delete(ctx context.Context, id uint) error - - // 查询操作 - List(ctx context.Context, parentID *uint) ([]*entity.Category, error) - GetTree(ctx context.Context) ([]*entity.CategoryTree, error) - GetChildren(ctx context.Context, parentID uint) ([]*entity.Category, error) - GetParent(ctx context.Context, categoryID uint) (*entity.Category, error) - - // 排序操作 - Reorder(ctx context.Context, parentID *uint, categoryIDs []uint) error - GetNextSortOrder(ctx context.Context, parentID *uint) (int, error) - - // 验证操作 - ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error - ValidateParentCategory(ctx context.Context, categoryID, parentID uint) error - - // 统计操作 - Count(ctx context.Context) (int64, error) - CountActive(ctx context.Context) (int64, error) - CountTopLevel(ctx context.Context) (int64, error) - GetStats(ctx context.Context) (*entity.CategoryStats, error) - - // 工具方法 - GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) -} \ No newline at end of file diff --git a/backend-old/internal/repository/interfaces/photo_repository.go b/backend-old/internal/repository/interfaces/photo_repository.go deleted file mode 100644 index 22a7244..0000000 --- a/backend-old/internal/repository/interfaces/photo_repository.go +++ /dev/null @@ -1,33 +0,0 @@ -package interfaces - -import ( - "context" - "photography-backend/internal/model/entity" -) - -// PhotoRepository 照片仓储接口 -type PhotoRepository interface { - // 基本CRUD操作 - Create(ctx context.Context, photo *entity.Photo) error - GetByID(ctx context.Context, id uint) (*entity.Photo, error) - Update(ctx context.Context, photo *entity.Photo) error - Delete(ctx context.Context, id uint) error - - // 查询操作 - List(ctx context.Context, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) - ListByUserID(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) - ListByCategory(ctx context.Context, categoryID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) - Search(ctx context.Context, query string, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) - - // 批量操作 - BatchUpdate(ctx context.Context, ids []uint, updates map[string]interface{}) error - BatchDelete(ctx context.Context, ids []uint) error - BatchUpdateCategories(ctx context.Context, photoIDs []uint, categoryIDs []uint) error - BatchUpdateTags(ctx context.Context, photoIDs []uint, tagIDs []uint) error - - // 统计操作 - Count(ctx context.Context) (int64, error) - CountByStatus(ctx context.Context, status string) (int64, error) - CountByUser(ctx context.Context, userID uint) (int64, error) - GetStats(ctx context.Context) (*entity.PhotoStats, error) -} \ No newline at end of file diff --git a/backend-old/internal/repository/interfaces/tag_repository.go b/backend-old/internal/repository/interfaces/tag_repository.go deleted file mode 100644 index 607a6b2..0000000 --- a/backend-old/internal/repository/interfaces/tag_repository.go +++ /dev/null @@ -1,42 +0,0 @@ -package interfaces - -import ( - "context" - "photography-backend/internal/model/entity" -) - -// TagRepository 标签仓储接口 -type TagRepository interface { - // 基本CRUD操作 - Create(ctx context.Context, tag *entity.Tag) error - GetByID(ctx context.Context, id uint) (*entity.Tag, error) - GetBySlug(ctx context.Context, slug string) (*entity.Tag, error) - GetByName(ctx context.Context, name string) (*entity.Tag, error) - Update(ctx context.Context, tag *entity.Tag) error - Delete(ctx context.Context, id uint) error - - // 查询操作 - List(ctx context.Context, params *entity.TagListParams) ([]*entity.Tag, int64, error) - Search(ctx context.Context, query string) ([]*entity.Tag, error) - GetPopular(ctx context.Context, limit int) ([]*entity.Tag, error) - GetByPhotos(ctx context.Context, photoIDs []uint) ([]*entity.Tag, error) - - // 批量操作 - CreateMultiple(ctx context.Context, tags []*entity.Tag) error - GetOrCreateByNames(ctx context.Context, names []string) ([]*entity.Tag, error) - BatchDelete(ctx context.Context, ids []uint) error - - // 关联操作 - AttachToPhoto(ctx context.Context, tagID, photoID uint) error - DetachFromPhoto(ctx context.Context, tagID, photoID uint) error - GetPhotoTags(ctx context.Context, photoID uint) ([]*entity.Tag, error) - - // 统计操作 - Count(ctx context.Context) (int64, error) - CountByPhotos(ctx context.Context) (map[uint]int64, error) - GetStats(ctx context.Context) (*entity.TagStats, error) - - // 工具方法 - GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) - ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error -} \ No newline at end of file diff --git a/backend-old/internal/repository/interfaces/user_repository.go b/backend-old/internal/repository/interfaces/user_repository.go deleted file mode 100644 index 3a6717f..0000000 --- a/backend-old/internal/repository/interfaces/user_repository.go +++ /dev/null @@ -1,40 +0,0 @@ -package interfaces - -import ( - "context" - "photography-backend/internal/model/entity" -) - -// UserRepository 用户仓储接口 -type UserRepository interface { - // 基本CRUD操作 - Create(ctx context.Context, user *entity.User) error - GetByID(ctx context.Context, id uint) (*entity.User, error) - GetByEmail(ctx context.Context, email string) (*entity.User, error) - GetByUsername(ctx context.Context, username string) (*entity.User, error) - Update(ctx context.Context, user *entity.User) error - Delete(ctx context.Context, id uint) error - - // 查询操作 - List(ctx context.Context, params *entity.UserListParams) ([]*entity.User, int64, error) - Search(ctx context.Context, query string, params *entity.UserListParams) ([]*entity.User, int64, error) - - // 认证相关 - UpdatePassword(ctx context.Context, userID uint, hashedPassword string) error - UpdateLastLogin(ctx context.Context, userID uint) error - IncrementLoginCount(ctx context.Context, userID uint) error - - // 状态管理 - SetActive(ctx context.Context, userID uint, isActive bool) error - VerifyEmail(ctx context.Context, userID uint) error - - // 统计操作 - Count(ctx context.Context) (int64, error) - CountByRole(ctx context.Context, role entity.UserRole) (int64, error) - CountActive(ctx context.Context) (int64, error) - GetStats(ctx context.Context) (*entity.UserStats, error) - - // 验证操作 - ExistsByEmail(ctx context.Context, email string) (bool, error) - ExistsByUsername(ctx context.Context, username string) (bool, error) -} \ No newline at end of file diff --git a/backend-old/internal/repository/postgres/category_repository_impl.go b/backend-old/internal/repository/postgres/category_repository_impl.go deleted file mode 100644 index 62008d5..0000000 --- a/backend-old/internal/repository/postgres/category_repository_impl.go +++ /dev/null @@ -1,345 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "fmt" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - "photography-backend/internal/utils" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -// categoryRepositoryImpl 分类仓储实现 -type categoryRepositoryImpl struct { - db *gorm.DB - logger *zap.Logger -} - -// NewCategoryRepository 创建分类仓储实现 -func NewCategoryRepository(db *gorm.DB, logger *zap.Logger) interfaces.CategoryRepository { - return &categoryRepositoryImpl{ - db: db, - logger: logger, - } -} - -// Create 创建分类 -func (r *categoryRepositoryImpl) Create(ctx context.Context, category *entity.Category) error { - return r.db.WithContext(ctx).Create(category).Error -} - -// GetByID 根据ID获取分类 -func (r *categoryRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Category, error) { - var category entity.Category - err := r.db.WithContext(ctx).First(&category, id).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("category not found") - } - return nil, err - } - return &category, nil -} - -// GetBySlug 根据slug获取分类 -func (r *categoryRepositoryImpl) GetBySlug(ctx context.Context, slug string) (*entity.Category, error) { - var category entity.Category - err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&category).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("category not found") - } - return nil, err - } - return &category, nil -} - -// Update 更新分类 -func (r *categoryRepositoryImpl) Update(ctx context.Context, category *entity.Category) error { - return r.db.WithContext(ctx).Save(category).Error -} - -// Delete 删除分类 -func (r *categoryRepositoryImpl) Delete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Delete(&entity.Category{}, id).Error -} - -// List 获取分类列表 -func (r *categoryRepositoryImpl) List(ctx context.Context, parentID *uint) ([]*entity.Category, error) { - var categories []*entity.Category - - query := r.db.WithContext(ctx).Order("sort_order ASC, created_at ASC") - - if parentID != nil { - query = query.Where("parent_id = ?", *parentID) - } else { - query = query.Where("parent_id IS NULL") - } - - err := query.Find(&categories).Error - return categories, err -} - -// GetTree 获取分类树 -func (r *categoryRepositoryImpl) GetTree(ctx context.Context) ([]*entity.CategoryTree, error) { - var categories []*entity.Category - if err := r.db.WithContext(ctx). - Order("sort_order ASC, created_at ASC"). - Find(&categories).Error; err != nil { - return nil, err - } - - // 构建树形结构 - tree := r.buildCategoryTree(categories, nil) - return tree, nil -} - -// GetChildren 获取子分类 -func (r *categoryRepositoryImpl) GetChildren(ctx context.Context, parentID uint) ([]*entity.Category, error) { - var children []*entity.Category - err := r.db.WithContext(ctx). - Where("parent_id = ?", parentID). - Order("sort_order ASC"). - Find(&children).Error - return children, err -} - -// GetParent 获取父分类 -func (r *categoryRepositoryImpl) GetParent(ctx context.Context, categoryID uint) (*entity.Category, error) { - var category entity.Category - err := r.db.WithContext(ctx). - Preload("Parent"). - First(&category, categoryID).Error - if err != nil { - return nil, err - } - return category.Parent, nil -} - -// Reorder 重新排序分类 -func (r *categoryRepositoryImpl) Reorder(ctx context.Context, parentID *uint, categoryIDs []uint) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - for i, categoryID := range categoryIDs { - if err := tx.Model(&entity.Category{}). - Where("id = ?", categoryID). - Update("sort_order", i+1).Error; err != nil { - return err - } - } - return nil - }) -} - -// GetNextSortOrder 获取下一个排序顺序 -func (r *categoryRepositoryImpl) GetNextSortOrder(ctx context.Context, parentID *uint) (int, error) { - var maxOrder int - - query := r.db.WithContext(ctx).Model(&entity.Category{}).Select("COALESCE(MAX(sort_order), 0)") - - if parentID != nil { - query = query.Where("parent_id = ?", *parentID) - } else { - query = query.Where("parent_id IS NULL") - } - - err := query.Row().Scan(&maxOrder) - return maxOrder + 1, err -} - -// ValidateSlugUnique 验证slug唯一性 -func (r *categoryRepositoryImpl) ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error { - var count int64 - query := r.db.WithContext(ctx).Model(&entity.Category{}).Where("slug = ?", slug) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return errors.New("slug already exists") - } - - return nil -} - -// ValidateParentCategory 验证父分类(防止循环引用) -func (r *categoryRepositoryImpl) ValidateParentCategory(ctx context.Context, categoryID, parentID uint) error { - if categoryID == parentID { - return errors.New("category cannot be its own parent") - } - - // 检查父分类是否存在 - var parent entity.Category - if err := r.db.WithContext(ctx).First(&parent, parentID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("parent category not found") - } - return err - } - - // 检查是否会形成循环引用 - current := parentID - for current != 0 { - var category entity.Category - if err := r.db.WithContext(ctx).First(&category, current).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("parent category not found") - } - return err - } - - if category.ParentID == nil { - break - } - - if *category.ParentID == categoryID { - return errors.New("circular reference detected") - } - - current = *category.ParentID - } - - return nil -} - -// Count 统计分类总数 -func (r *categoryRepositoryImpl) Count(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Category{}).Count(&count).Error - return count, err -} - -// CountActive 统计活跃分类数 -func (r *categoryRepositoryImpl) CountActive(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Category{}). - Where("is_active = ?", true).Count(&count).Error - return count, err -} - -// CountTopLevel 统计顶级分类数 -func (r *categoryRepositoryImpl) CountTopLevel(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Category{}). - Where("parent_id IS NULL").Count(&count).Error - return count, err -} - -// GetStats 获取分类统计信息 -func (r *categoryRepositoryImpl) GetStats(ctx context.Context) (*entity.CategoryStats, error) { - var stats entity.CategoryStats - - // 总分类数 - if total, err := r.Count(ctx); err != nil { - return nil, err - } else { - stats.Total = total - } - - // 活跃分类数 - if active, err := r.CountActive(ctx); err != nil { - return nil, err - } else { - stats.Active = active - } - - // 顶级分类数 - if topLevel, err := r.CountTopLevel(ctx); err != nil { - return nil, err - } else { - stats.TopLevel = topLevel - } - - // 各分类照片数量 - var categoryPhotoStats []struct { - CategoryID uint `json:"category_id"` - Name string `json:"name"` - PhotoCount int64 `json:"photo_count"` - } - - if err := r.db.WithContext(ctx). - Table("categories"). - Select("categories.id as category_id, categories.name, COUNT(photo_categories.photo_id) as photo_count"). - Joins("LEFT JOIN photo_categories ON categories.id = photo_categories.category_id"). - Group("categories.id, categories.name"). - Order("photo_count DESC"). - Limit(10). - Find(&categoryPhotoStats).Error; err != nil { - return nil, err - } - - stats.PhotoCounts = make(map[string]int64) - for _, stat := range categoryPhotoStats { - stats.PhotoCounts[stat.Name] = stat.PhotoCount - } - - return &stats, nil -} - -// GenerateUniqueSlug 生成唯一slug -func (r *categoryRepositoryImpl) GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) { - baseSlug := utils.GenerateSlug(baseName) - slug := baseSlug - - counter := 1 - for { - var count int64 - if err := r.db.WithContext(ctx).Model(&entity.Category{}). - Where("slug = ?", slug).Count(&count).Error; err != nil { - return "", err - } - - if count == 0 { - break - } - - slug = fmt.Sprintf("%s-%d", baseSlug, counter) - counter++ - } - - return slug, nil -} - -// buildCategoryTree 构建分类树 -func (r *categoryRepositoryImpl) buildCategoryTree(categories []*entity.Category, parentID *uint) []*entity.CategoryTree { - var tree []*entity.CategoryTree - - for _, category := range categories { - // 检查是否匹配父分类 - if (parentID == nil && category.ParentID == nil) || - (parentID != nil && category.ParentID != nil && *category.ParentID == *parentID) { - - node := &entity.CategoryTree{ - ID: category.ID, - Name: category.Name, - Slug: category.Slug, - Description: category.Description, - ParentID: category.ParentID, - SortOrder: category.SortOrder, - IsActive: category.IsActive, - PhotoCount: category.PhotoCount, - CreatedAt: category.CreatedAt, - UpdatedAt: category.UpdatedAt, - } - - // 递归构建子分类 - children := r.buildCategoryTree(categories, &category.ID) - node.Children = make([]entity.CategoryTree, len(children)) - for i, child := range children { - node.Children[i] = *child - } - - tree = append(tree, node) - } - } - - return tree -} \ No newline at end of file diff --git a/backend-old/internal/repository/postgres/database.go b/backend-old/internal/repository/postgres/database.go deleted file mode 100644 index e7f695c..0000000 --- a/backend-old/internal/repository/postgres/database.go +++ /dev/null @@ -1,78 +0,0 @@ -package postgres - -import ( - "fmt" - "time" - "gorm.io/gorm" - "gorm.io/driver/postgres" - "photography-backend/internal/config" - "photography-backend/internal/model/entity" -) - -// Database 数据库连接 -type Database struct { - DB *gorm.DB -} - -// NewDatabase 创建数据库连接 -func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { - dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", - cfg.Host, - cfg.Port, - cfg.Username, - cfg.Password, - cfg.Database, - cfg.SSLMode, - ) - - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("failed to connect to database: %w", err) - } - - // 获取底层sql.DB实例配置连接池 - sqlDB, err := db.DB() - if err != nil { - return nil, fmt.Errorf("failed to get sql.DB instance: %w", err) - } - - // 设置连接池参数 - sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) - sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) - sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second) - - // 测试连接 - if err := sqlDB.Ping(); err != nil { - return nil, fmt.Errorf("failed to ping database: %w", err) - } - - return &Database{DB: db}, nil -} - -// AutoMigrate 自动迁移数据库表结构 -func (d *Database) AutoMigrate() error { - return d.DB.AutoMigrate( - &entity.User{}, - &entity.Category{}, - &entity.Tag{}, - &entity.Photo{}, - ) -} - -// Close 关闭数据库连接 -func (d *Database) Close() error { - sqlDB, err := d.DB.DB() - if err != nil { - return err - } - return sqlDB.Close() -} - -// Health 检查数据库健康状态 -func (d *Database) Health() error { - sqlDB, err := d.DB.DB() - if err != nil { - return err - } - return sqlDB.Ping() -} \ No newline at end of file diff --git a/backend-old/internal/repository/postgres/photo_repository_impl.go b/backend-old/internal/repository/postgres/photo_repository_impl.go deleted file mode 100644 index f3d83d9..0000000 --- a/backend-old/internal/repository/postgres/photo_repository_impl.go +++ /dev/null @@ -1,375 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "fmt" - "time" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -// photoRepositoryImpl 照片仓储实现 -type photoRepositoryImpl struct { - db *gorm.DB - logger *zap.Logger -} - -// NewPhotoRepository 创建照片仓储实现 -func NewPhotoRepository(db *gorm.DB, logger *zap.Logger) interfaces.PhotoRepository { - return &photoRepositoryImpl{ - db: db, - logger: logger, - } -} - -// Create 创建照片 -func (r *photoRepositoryImpl) Create(ctx context.Context, photo *entity.Photo) error { - return r.db.WithContext(ctx).Create(photo).Error -} - -// GetByID 根据ID获取照片 -func (r *photoRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Photo, error) { - var photo entity.Photo - err := r.db.WithContext(ctx). - Preload("User"). - Preload("Categories"). - Preload("Tags"). - First(&photo, id).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("photo not found") - } - return nil, err - } - return &photo, nil -} - -// GetByFilename 根据文件名获取照片 -func (r *photoRepositoryImpl) GetByFilename(ctx context.Context, filename string) (*entity.Photo, error) { - var photo entity.Photo - err := r.db.WithContext(ctx).Where("filename = ?", filename).First(&photo).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("photo not found") - } - return nil, err - } - return &photo, nil -} - -// Update 更新照片 -func (r *photoRepositoryImpl) Update(ctx context.Context, photo *entity.Photo) error { - return r.db.WithContext(ctx).Save(photo).Error -} - -// Delete 删除照片 -func (r *photoRepositoryImpl) Delete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Delete(&entity.Photo{}, id).Error -} - -// List 获取照片列表 -func (r *photoRepositoryImpl) List(ctx context.Context, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - var photos []*entity.Photo - var total int64 - - query := r.db.WithContext(ctx).Model(&entity.Photo{}) - - // 应用过滤条件 - if params.UserID != nil { - query = query.Where("user_id = ?", *params.UserID) - } - - if params.Status != nil { - query = query.Where("status = ?", *params.Status) - } - - if params.CategoryID != nil { - query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). - Where("photo_categories.category_id = ?", *params.CategoryID) - } - - if params.TagID != nil { - query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). - Where("photo_tags.tag_id = ?", *params.TagID) - } - - if params.DateFrom != nil { - query = query.Where("taken_at >= ?", *params.DateFrom) - } - - if params.DateTo != nil { - query = query.Where("taken_at <= ?", *params.DateTo) - } - - if params.Search != "" { - query = query.Where("title ILIKE ? OR description ILIKE ?", - "%"+params.Search+"%", "%"+params.Search+"%") - } - - // 获取总数 - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用排序 - orderBy := "created_at DESC" - if params.Sort != "" { - order := "ASC" - if params.Order == "desc" { - order = "DESC" - } - orderBy = fmt.Sprintf("%s %s", params.Sort, order) - } - query = query.Order(orderBy) - - // 应用分页 - if params.Page > 0 && params.Limit > 0 { - offset := (params.Page - 1) * params.Limit - query = query.Offset(offset).Limit(params.Limit) - } - - // 预加载关联数据 - query = query.Preload("User").Preload("Categories").Preload("Tags") - - // 查询数据 - if err := query.Find(&photos).Error; err != nil { - return nil, 0, err - } - - return photos, total, nil -} - -// ListByUserID 根据用户ID获取照片列表 -func (r *photoRepositoryImpl) ListByUserID(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - if params == nil { - params = &entity.PhotoListParams{} - } - params.UserID = &userID - return r.List(ctx, params) -} - -// ListByStatus 根据状态获取照片列表 -func (r *photoRepositoryImpl) ListByStatus(ctx context.Context, status entity.PhotoStatus, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - if params == nil { - params = &entity.PhotoListParams{} - } - params.Status = &status - return r.List(ctx, params) -} - -// ListByCategory 根据分类获取照片列表 -func (r *photoRepositoryImpl) ListByCategory(ctx context.Context, categoryID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - if params == nil { - params = &entity.PhotoListParams{} - } - params.CategoryID = &categoryID - return r.List(ctx, params) -} - -// Search 搜索照片 -func (r *photoRepositoryImpl) Search(ctx context.Context, query string, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - if params == nil { - params = &entity.PhotoListParams{} - } - params.Search = query - return r.List(ctx, params) -} - -// Count 统计照片总数 -func (r *photoRepositoryImpl) Count(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Photo{}).Count(&count).Error - return count, err -} - -// CountByUser 统计用户照片数 -func (r *photoRepositoryImpl) CountByUser(ctx context.Context, userID uint) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ?", userID).Count(&count).Error - return count, err -} - -// CountByStatus 统计指定状态照片数 -func (r *photoRepositoryImpl) CountByStatus(ctx context.Context, status entity.PhotoStatus) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("status = ?", status).Count(&count).Error - return count, err -} - -// CountByCategory 统计分类照片数 -func (r *photoRepositoryImpl) CountByCategory(ctx context.Context, categoryID uint) (int64, error) { - var count int64 - err := r.db.WithContext(ctx). - Table("photo_categories"). - Where("category_id = ?", categoryID). - Count(&count).Error - return count, err -} - -// CountByStatus 统计指定状态照片数 -func (r *photoRepositoryImpl) CountByStatus(ctx context.Context, status string) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("status = ?", status).Count(&count).Error - return count, err -} - - -// BatchUpdate 批量更新 -func (r *photoRepositoryImpl) BatchUpdate(ctx context.Context, ids []uint, updates map[string]interface{}) error { - if len(ids) == 0 || len(updates) == 0 { - return nil - } - - return r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("id IN ?", ids). - Updates(updates).Error -} - -// BatchUpdateCategories 批量更新分类 -func (r *photoRepositoryImpl) BatchUpdateCategories(ctx context.Context, photoIDs []uint, categoryIDs []uint) error { - if len(photoIDs) == 0 { - return nil - } - - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 删除现有关联 - if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", photoIDs).Error; err != nil { - return err - } - - // 添加新关联 - for _, photoID := range photoIDs { - for _, categoryID := range categoryIDs { - if err := tx.Exec("INSERT INTO photo_categories (photo_id, category_id) VALUES (?, ?)", - photoID, categoryID).Error; err != nil { - return err - } - } - } - return nil - }) -} - -// BatchUpdateTags 批量更新标签 -func (r *photoRepositoryImpl) BatchUpdateTags(ctx context.Context, photoIDs []uint, tagIDs []uint) error { - if len(photoIDs) == 0 { - return nil - } - - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 删除现有关联 - if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", photoIDs).Error; err != nil { - return err - } - - // 添加新关联 - for _, photoID := range photoIDs { - for _, tagID := range tagIDs { - if err := tx.Exec("INSERT INTO photo_tags (photo_id, tag_id) VALUES (?, ?)", - photoID, tagID).Error; err != nil { - return err - } - } - } - return nil - }) -} - -// BatchDelete 批量删除 -func (r *photoRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return nil - } - - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 删除关联关系 - if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - // 删除照片记录 - return tx.Delete(&entity.Photo{}, ids).Error - }) -} - -// GetStats 获取照片统计信息 -func (r *photoRepositoryImpl) GetStats(ctx context.Context) (*entity.PhotoStats, error) { - var stats entity.PhotoStats - - // 总照片数 - if total, err := r.Count(ctx); err != nil { - return nil, err - } else { - stats.Total = total - } - - // 按状态统计 - for _, status := range []entity.PhotoStatus{ - entity.PhotoStatusActive, - entity.PhotoStatusDraft, - entity.PhotoStatusArchived, - } { - if count, err := r.CountByStatus(ctx, status); err != nil { - return nil, err - } else { - switch status { - case entity.PhotoStatusActive: - stats.Published = count - case entity.PhotoStatusDraft: - stats.Draft = count - case entity.PhotoStatusArchived: - stats.Archived = count - } - } - } - - // 本月新增照片数 - now := time.Now() - startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond) - - var monthlyCount int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("created_at >= ? AND created_at <= ?", startOfMonth, endOfMonth). - Count(&monthlyCount).Error; err != nil { - return nil, err - } - stats.ThisMonth = monthlyCount - - // 用户照片分布(Top 10) - var userPhotoStats []struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - PhotoCount int64 `json:"photo_count"` - } - - if err := r.db.WithContext(ctx). - Table("photos"). - Select("photos.user_id, users.username, COUNT(photos.id) as photo_count"). - Joins("LEFT JOIN users ON photos.user_id = users.id"). - Group("photos.user_id, users.username"). - Order("photo_count DESC"). - Limit(10). - Find(&userPhotoStats).Error; err != nil { - return nil, err - } - - stats.UserPhotoCounts = make(map[string]int64) - for _, stat := range userPhotoStats { - stats.UserPhotoCounts[stat.Username] = stat.PhotoCount - } - - return &stats, nil -} \ No newline at end of file diff --git a/backend-old/internal/repository/postgres/tag_repository_impl.go b/backend-old/internal/repository/postgres/tag_repository_impl.go deleted file mode 100644 index 3ec5c80..0000000 --- a/backend-old/internal/repository/postgres/tag_repository_impl.go +++ /dev/null @@ -1,468 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "fmt" - "strings" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - "photography-backend/internal/utils" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -// tagRepositoryImpl 标签仓储实现 -type tagRepositoryImpl struct { - db *gorm.DB - logger *zap.Logger -} - -// NewTagRepository 创建标签仓储实现 -func NewTagRepository(db *gorm.DB, logger *zap.Logger) interfaces.TagRepository { - return &tagRepositoryImpl{ - db: db, - logger: logger, - } -} - -// Create 创建标签 -func (r *tagRepositoryImpl) Create(ctx context.Context, tag *entity.Tag) error { - return r.db.WithContext(ctx).Create(tag).Error -} - -// GetByID 根据ID获取标签 -func (r *tagRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.Tag, error) { - var tag entity.Tag - err := r.db.WithContext(ctx).First(&tag, id).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - return nil, err - } - return &tag, nil -} - -// GetBySlug 根据slug获取标签 -func (r *tagRepositoryImpl) GetBySlug(ctx context.Context, slug string) (*entity.Tag, error) { - var tag entity.Tag - err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&tag).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - return nil, err - } - return &tag, nil -} - -// GetByName 根据名称获取标签 -func (r *tagRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Tag, error) { - var tag entity.Tag - err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - return nil, err - } - return &tag, nil -} - -// Update 更新标签 -func (r *tagRepositoryImpl) Update(ctx context.Context, tag *entity.Tag) error { - return r.db.WithContext(ctx).Save(tag).Error -} - -// Delete 删除标签 -func (r *tagRepositoryImpl) Delete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 删除照片标签关联 - if err := tx.Exec("DELETE FROM photo_tags WHERE tag_id = ?", id).Error; err != nil { - return err - } - - // 删除标签 - return tx.Delete(&entity.Tag{}, id).Error - }) -} - -// List 获取标签列表 -func (r *tagRepositoryImpl) List(ctx context.Context, params *entity.TagListParams) ([]*entity.Tag, int64, error) { - var tags []*entity.Tag - var total int64 - - query := r.db.WithContext(ctx).Model(&entity.Tag{}) - - // 应用过滤条件 - if params.Search != "" { - query = query.Where("name ILIKE ? OR description ILIKE ?", - "%"+params.Search+"%", "%"+params.Search+"%") - } - - if params.Color != "" { - query = query.Where("color = ?", params.Color) - } - - if params.CreatedFrom != nil { - query = query.Where("created_at >= ?", *params.CreatedFrom) - } - - if params.CreatedTo != nil { - query = query.Where("created_at <= ?", *params.CreatedTo) - } - - // 获取总数 - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用排序 - orderBy := "created_at DESC" - if params.Sort != "" { - order := "ASC" - if params.Order == "desc" { - order = "DESC" - } - orderBy = fmt.Sprintf("%s %s", params.Sort, order) - } - query = query.Order(orderBy) - - // 应用分页 - if params.Page > 0 && params.Limit > 0 { - offset := (params.Page - 1) * params.Limit - query = query.Offset(offset).Limit(params.Limit) - } - - // 如果需要包含照片计数 - if params.IncludePhotoCount { - query = query.Select("tags.*, COUNT(photo_tags.photo_id) as photo_count"). - Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Group("tags.id") - } - - // 查询数据 - if err := query.Find(&tags).Error; err != nil { - return nil, 0, err - } - - return tags, total, nil -} - -// Search 搜索标签 -func (r *tagRepositoryImpl) Search(ctx context.Context, query string) ([]*entity.Tag, error) { - var tags []*entity.Tag - - err := r.db.WithContext(ctx). - Where("name ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%"). - Order("name ASC"). - Limit(50). - Find(&tags).Error - - return tags, err -} - -// GetPopular 获取热门标签 -func (r *tagRepositoryImpl) GetPopular(ctx context.Context, limit int) ([]*entity.Tag, error) { - var tags []*entity.Tag - - err := r.db.WithContext(ctx). - Select("tags.*, COUNT(photo_tags.photo_id) as photo_count"). - Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Group("tags.id"). - Order("photo_count DESC"). - Limit(limit). - Find(&tags).Error - - return tags, err -} - -// GetByPhotos 根据照片IDs获取标签 -func (r *tagRepositoryImpl) GetByPhotos(ctx context.Context, photoIDs []uint) ([]*entity.Tag, error) { - if len(photoIDs) == 0 { - return []*entity.Tag{}, nil - } - - var tags []*entity.Tag - - err := r.db.WithContext(ctx). - Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Where("photo_tags.photo_id IN ?", photoIDs). - Distinct(). - Find(&tags).Error - - return tags, err -} - -// CreateMultiple 批量创建标签 -func (r *tagRepositoryImpl) CreateMultiple(ctx context.Context, tags []*entity.Tag) error { - if len(tags) == 0 { - return nil - } - - return r.db.WithContext(ctx).CreateInBatches(tags, 100).Error -} - -// GetOrCreateByNames 根据名称获取或创建标签 -func (r *tagRepositoryImpl) GetOrCreateByNames(ctx context.Context, names []string) ([]*entity.Tag, error) { - if len(names) == 0 { - return []*entity.Tag{}, nil - } - - var tags []*entity.Tag - - return tags, r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - for _, name := range names { - name = strings.TrimSpace(name) - if name == "" { - continue - } - - var tag entity.Tag - - // 尝试获取现有标签 - err := tx.Where("name = ?", name).First(&tag).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // 创建新标签 - slug, err := r.generateUniqueSlug(ctx, name) - if err != nil { - return err - } - - tag = entity.Tag{ - Name: name, - Slug: slug, - Color: r.generateRandomColor(), - PhotoCount: 0, - } - - if err := tx.Create(&tag).Error; err != nil { - return err - } - } else { - return err - } - } - - tags = append(tags, &tag) - } - - return nil - }) -} - -// BatchDelete 批量删除标签 -func (r *tagRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return nil - } - - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 删除照片标签关联 - if err := tx.Exec("DELETE FROM photo_tags WHERE tag_id IN ?", ids).Error; err != nil { - return err - } - - // 删除标签 - return tx.Delete(&entity.Tag{}, ids).Error - }) -} - -// AttachToPhoto 为照片添加标签 -func (r *tagRepositoryImpl) AttachToPhoto(ctx context.Context, tagID, photoID uint) error { - // 检查关联是否已存在 - var count int64 - if err := r.db.WithContext(ctx).Table("photo_tags"). - Where("tag_id = ? AND photo_id = ?", tagID, photoID). - Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return nil // 关联已存在 - } - - // 创建关联 - return r.db.WithContext(ctx).Exec( - "INSERT INTO photo_tags (tag_id, photo_id) VALUES (?, ?)", - tagID, photoID, - ).Error -} - -// DetachFromPhoto 从照片移除标签 -func (r *tagRepositoryImpl) DetachFromPhoto(ctx context.Context, tagID, photoID uint) error { - return r.db.WithContext(ctx).Exec( - "DELETE FROM photo_tags WHERE tag_id = ? AND photo_id = ?", - tagID, photoID, - ).Error -} - -// GetPhotoTags 获取照片的标签 -func (r *tagRepositoryImpl) GetPhotoTags(ctx context.Context, photoID uint) ([]*entity.Tag, error) { - var tags []*entity.Tag - - err := r.db.WithContext(ctx). - Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Where("photo_tags.photo_id = ?", photoID). - Order("tags.name ASC"). - Find(&tags).Error - - return tags, err -} - -// Count 统计标签总数 -func (r *tagRepositoryImpl) Count(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.Tag{}).Count(&count).Error - return count, err -} - -// CountByPhotos 统计各标签的照片数量 -func (r *tagRepositoryImpl) CountByPhotos(ctx context.Context) (map[uint]int64, error) { - var results []struct { - TagID uint `json:"tag_id"` - PhotoCount int64 `json:"photo_count"` - } - - err := r.db.WithContext(ctx). - Table("photo_tags"). - Select("tag_id, COUNT(photo_id) as photo_count"). - Group("tag_id"). - Find(&results).Error - - if err != nil { - return nil, err - } - - counts := make(map[uint]int64) - for _, result := range results { - counts[result.TagID] = result.PhotoCount - } - - return counts, nil -} - -// GetStats 获取标签统计信息 -func (r *tagRepositoryImpl) GetStats(ctx context.Context) (*entity.TagStats, error) { - var stats entity.TagStats - - // 总标签数 - if total, err := r.Count(ctx); err != nil { - return nil, err - } else { - stats.Total = total - } - - // 已使用标签数(有照片关联的标签) - var usedCount int64 - if err := r.db.WithContext(ctx). - Table("tags"). - Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Distinct("tags.id"). - Count(&usedCount).Error; err != nil { - return nil, err - } - stats.Used = usedCount - - // 未使用标签数 - stats.Unused = stats.Total - stats.Used - - // 平均每个标签的照片数 - if stats.Used > 0 { - var totalPhotos int64 - if err := r.db.WithContext(ctx). - Table("photo_tags"). - Count(&totalPhotos).Error; err != nil { - return nil, err - } - stats.AvgPhotosPerTag = float64(totalPhotos) / float64(stats.Used) - } - - // 最受欢迎的标签(前10) - var popularTags []struct { - TagID uint `json:"tag_id"` - Name string `json:"name"` - PhotoCount int64 `json:"photo_count"` - } - - if err := r.db.WithContext(ctx). - Table("tags"). - Select("tags.id as tag_id, tags.name, COUNT(photo_tags.photo_id) as photo_count"). - Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Group("tags.id, tags.name"). - Order("photo_count DESC"). - Limit(10). - Find(&popularTags).Error; err != nil { - return nil, err - } - - stats.PopularTags = make(map[string]int64) - for _, tag := range popularTags { - stats.PopularTags[tag.Name] = tag.PhotoCount - } - - return &stats, nil -} - -// GenerateUniqueSlug 生成唯一slug -func (r *tagRepositoryImpl) GenerateUniqueSlug(ctx context.Context, baseName string) (string, error) { - return r.generateUniqueSlug(ctx, baseName) -} - -// ValidateSlugUnique 验证slug唯一性 -func (r *tagRepositoryImpl) ValidateSlugUnique(ctx context.Context, slug string, excludeID uint) error { - var count int64 - query := r.db.WithContext(ctx).Model(&entity.Tag{}).Where("slug = ?", slug) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return errors.New("slug already exists") - } - - return nil -} - -// generateUniqueSlug 生成唯一slug -func (r *tagRepositoryImpl) generateUniqueSlug(ctx context.Context, baseName string) (string, error) { - baseSlug := utils.GenerateSlug(baseName) - slug := baseSlug - - counter := 1 - for { - var count int64 - if err := r.db.WithContext(ctx).Model(&entity.Tag{}). - Where("slug = ?", slug).Count(&count).Error; err != nil { - return "", err - } - - if count == 0 { - break - } - - slug = fmt.Sprintf("%s-%d", baseSlug, counter) - counter++ - } - - return slug, nil -} - -// generateRandomColor 生成随机颜色 -func (r *tagRepositoryImpl) generateRandomColor() string { - colors := []string{ - "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", - "#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9", - "#F8C471", "#82E0AA", "#F1948A", "#85C1E9", "#D7BDE2", - } - return colors[len(colors)%15] // 简单的颜色选择 -} \ No newline at end of file diff --git a/backend-old/internal/repository/postgres/user_repository_impl.go b/backend-old/internal/repository/postgres/user_repository_impl.go deleted file mode 100644 index 125a60e..0000000 --- a/backend-old/internal/repository/postgres/user_repository_impl.go +++ /dev/null @@ -1,516 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "fmt" - "time" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -// userRepositoryImpl 用户仓储实现 -type userRepositoryImpl struct { - db *gorm.DB - logger *zap.Logger -} - -// NewUserRepository 创建用户仓储实现 -func NewUserRepository(db *gorm.DB, logger *zap.Logger) interfaces.UserRepository { - return &userRepositoryImpl{ - db: db, - logger: logger, - } -} - -// Create 创建用户 -func (r *userRepositoryImpl) Create(ctx context.Context, user *entity.User) error { - return r.db.WithContext(ctx).Create(user).Error -} - -// GetByID 根据ID获取用户 -func (r *userRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).First(&user, id).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - return &user, nil -} - -// GetByEmail 根据邮箱获取用户 -func (r *userRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - return &user, nil -} - -// GetByUsername 根据用户名获取用户 -func (r *userRepositoryImpl) GetByUsername(ctx context.Context, username string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - return &user, nil -} - -// GetByEmailOrUsername 根据邮箱或用户名获取用户 -func (r *userRepositoryImpl) GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) { - var user entity.User - err := r.db.WithContext(ctx). - Where("email = ? OR username = ?", emailOrUsername, emailOrUsername). - First(&user).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - return &user, nil -} - -// Update 更新用户 -func (r *userRepositoryImpl) Update(ctx context.Context, user *entity.User) error { - return r.db.WithContext(ctx).Save(user).Error -} - -// Delete 删除用户 -func (r *userRepositoryImpl) Delete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error -} - -// List 获取用户列表 -func (r *userRepositoryImpl) List(ctx context.Context, params *entity.UserListParams) ([]*entity.User, int64, error) { - var users []*entity.User - var total int64 - - query := r.db.WithContext(ctx).Model(&entity.User{}) - - // 应用过滤条件 - if params.Role != nil { - query = query.Where("role = ?", *params.Role) - } - - if params.Status != nil { - query = query.Where("status = ?", *params.Status) - } - - if params.Search != "" { - query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", - "%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%") - } - - if params.CreatedFrom != nil { - query = query.Where("created_at >= ?", *params.CreatedFrom) - } - - if params.CreatedTo != nil { - query = query.Where("created_at <= ?", *params.CreatedTo) - } - - if params.LastLoginFrom != nil { - query = query.Where("last_login_at >= ?", *params.LastLoginFrom) - } - - if params.LastLoginTo != nil { - query = query.Where("last_login_at <= ?", *params.LastLoginTo) - } - - // 获取总数 - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用排序 - orderBy := "created_at DESC" - if params.Sort != "" { - order := "ASC" - if params.Order == "desc" { - order = "DESC" - } - orderBy = fmt.Sprintf("%s %s", params.Sort, order) - } - query = query.Order(orderBy) - - // 应用分页 - if params.Page > 0 && params.Limit > 0 { - offset := (params.Page - 1) * params.Limit - query = query.Offset(offset).Limit(params.Limit) - } - - // 查询数据 - if err := query.Find(&users).Error; err != nil { - return nil, 0, err - } - - return users, total, nil -} - -// ListByRole 根据角色获取用户列表 -func (r *userRepositoryImpl) ListByRole(ctx context.Context, role entity.UserRole, params *entity.UserListParams) ([]*entity.User, int64, error) { - if params == nil { - params = &entity.UserListParams{} - } - params.Role = &role - return r.List(ctx, params) -} - -// ListByStatus 根据状态获取用户列表 -func (r *userRepositoryImpl) ListByStatus(ctx context.Context, status entity.UserStatus, params *entity.UserListParams) ([]*entity.User, int64, error) { - if params == nil { - params = &entity.UserListParams{} - } - params.Status = &status - return r.List(ctx, params) -} - -// SearchUsers 搜索用户 -func (r *userRepositoryImpl) SearchUsers(ctx context.Context, keyword string, params *entity.UserListParams) ([]*entity.User, int64, error) { - if params == nil { - params = &entity.UserListParams{} - } - params.Search = keyword - return r.List(ctx, params) -} - -// Count 统计用户总数 -func (r *userRepositoryImpl) Count(ctx context.Context) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.User{}).Count(&count).Error - return count, err -} - -// CountByRole 根据角色统计用户数 -func (r *userRepositoryImpl) CountByRole(ctx context.Context, role entity.UserRole) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.User{}). - Where("role = ?", role).Count(&count).Error - return count, err -} - -// CountByStatus 根据状态统计用户数 -func (r *userRepositoryImpl) CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entity.User{}). - Where("status = ?", status).Count(&count).Error - return count, err -} - -// CountActiveUsers 统计活跃用户数 -func (r *userRepositoryImpl) CountActiveUsers(ctx context.Context) (int64, error) { - return r.CountByStatus(ctx, entity.UserStatusActive) -} - -// UpdateStatus 更新用户状态 -func (r *userRepositoryImpl) UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error { - return r.db.WithContext(ctx).Model(&entity.User{}). - Where("id = ?", id). - Update("status", status).Error -} - -// UpdateLastLogin 更新最后登录时间 -func (r *userRepositoryImpl) UpdateLastLogin(ctx context.Context, id uint) error { - now := time.Now() - return r.db.WithContext(ctx).Model(&entity.User{}). - Where("id = ?", id). - Update("last_login_at", now).Error -} - -// UpdatePassword 更新密码 -func (r *userRepositoryImpl) UpdatePassword(ctx context.Context, id uint, hashedPassword string) error { - return r.db.WithContext(ctx).Model(&entity.User{}). - Where("id = ?", id). - Update("password", hashedPassword).Error -} - -// SoftDelete 软删除用户 -func (r *userRepositoryImpl) SoftDelete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error -} - -// Restore 恢复软删除用户 -func (r *userRepositoryImpl) Restore(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Unscoped().Model(&entity.User{}). - Where("id = ?", id). - Update("deleted_at", nil).Error -} - -// BatchUpdateStatus 批量更新用户状态 -func (r *userRepositoryImpl) BatchUpdateStatus(ctx context.Context, ids []uint, status entity.UserStatus) error { - if len(ids) == 0 { - return nil - } - - return r.db.WithContext(ctx).Model(&entity.User{}). - Where("id IN ?", ids). - Update("status", status).Error -} - -// BatchDelete 批量删除用户 -func (r *userRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return nil - } - - return r.db.WithContext(ctx).Delete(&entity.User{}, ids).Error -} - -// ValidateEmailUnique 验证邮箱唯一性 -func (r *userRepositoryImpl) ValidateEmailUnique(ctx context.Context, email string, excludeID uint) error { - var count int64 - query := r.db.WithContext(ctx).Model(&entity.User{}).Where("email = ?", email) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return errors.New("email already exists") - } - - return nil -} - -// ValidateUsernameUnique 验证用户名唯一性 -func (r *userRepositoryImpl) ValidateUsernameUnique(ctx context.Context, username string, excludeID uint) error { - var count int64 - query := r.db.WithContext(ctx).Model(&entity.User{}).Where("username = ?", username) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return errors.New("username already exists") - } - - return nil -} - -// GetUserPhotos 获取用户照片 -func (r *userRepositoryImpl) GetUserPhotos(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) { - var photos []*entity.Photo - var total int64 - - query := r.db.WithContext(ctx).Model(&entity.Photo{}).Where("user_id = ?", userID) - - // 应用过滤条件 - if params != nil { - if params.Status != nil { - query = query.Where("status = ?", *params.Status) - } - - if params.CategoryID != nil { - query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). - Where("photo_categories.category_id = ?", *params.CategoryID) - } - - if params.TagID != nil { - query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). - Where("photo_tags.tag_id = ?", *params.TagID) - } - - if params.DateFrom != nil { - query = query.Where("taken_at >= ?", *params.DateFrom) - } - - if params.DateTo != nil { - query = query.Where("taken_at <= ?", *params.DateTo) - } - - if params.Search != "" { - query = query.Where("title ILIKE ? OR description ILIKE ?", - "%"+params.Search+"%", "%"+params.Search+"%") - } - } - - // 获取总数 - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用排序和分页 - if params != nil { - orderBy := "created_at DESC" - if params.Sort != "" { - order := "ASC" - if params.Order == "desc" { - order = "DESC" - } - orderBy = fmt.Sprintf("%s %s", params.Sort, order) - } - query = query.Order(orderBy) - - if params.Page > 0 && params.Limit > 0 { - offset := (params.Page - 1) * params.Limit - query = query.Offset(offset).Limit(params.Limit) - } - } - - // 预加载关联数据 - query = query.Preload("Categories").Preload("Tags") - - // 查询数据 - if err := query.Find(&photos).Error; err != nil { - return nil, 0, err - } - - return photos, total, nil -} - -// GetUserStats 获取用户统计信息 -func (r *userRepositoryImpl) GetUserStats(ctx context.Context, userID uint) (*entity.UserStats, error) { - var stats entity.UserStats - - // 照片统计 - var photoCount int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ?", userID).Count(&photoCount).Error; err != nil { - return nil, err - } - stats.PhotoCount = photoCount - - // 按状态统计照片 - for _, status := range []entity.PhotoStatus{ - entity.PhotoStatusActive, - entity.PhotoStatusDraft, - entity.PhotoStatusArchived, - } { - var count int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ? AND status = ?", userID, status). - Count(&count).Error; err != nil { - return nil, err - } - - switch status { - case entity.PhotoStatusActive: - stats.PublishedPhotos = count - case entity.PhotoStatusDraft: - stats.DraftPhotos = count - case entity.PhotoStatusArchived: - stats.ArchivedPhotos = count - } - } - - // 总浏览数 - var totalViews int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ?", userID). - Select("COALESCE(SUM(view_count), 0)").Row().Scan(&totalViews); err != nil { - return nil, err - } - stats.TotalViews = totalViews - - // 总下载数 - var totalDownloads int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ?", userID). - Select("COALESCE(SUM(download_count), 0)").Row().Scan(&totalDownloads); err != nil { - return nil, err - } - stats.TotalDownloads = totalDownloads - - // 存储空间使用 - var storageUsed int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ?", userID). - Select("COALESCE(SUM(file_size), 0)").Row().Scan(&storageUsed); err != nil { - return nil, err - } - stats.StorageUsed = storageUsed - - // 本月新增照片 - now := time.Now() - startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond) - - var monthlyPhotos int64 - if err := r.db.WithContext(ctx).Model(&entity.Photo{}). - Where("user_id = ? AND created_at >= ? AND created_at <= ?", userID, startOfMonth, endOfMonth). - Count(&monthlyPhotos).Error; err != nil { - return nil, err - } - stats.MonthlyPhotos = monthlyPhotos - - return &stats, nil -} - -// GetAllStats 获取全部用户统计信息 -func (r *userRepositoryImpl) GetAllStats(ctx context.Context) (*entity.UserGlobalStats, error) { - var stats entity.UserGlobalStats - - // 总用户数 - if total, err := r.Count(ctx); err != nil { - return nil, err - } else { - stats.Total = total - } - - // 活跃用户数 - if active, err := r.CountActiveUsers(ctx); err != nil { - return nil, err - } else { - stats.Active = active - } - - // 按角色统计 - for _, role := range []entity.UserRole{ - entity.UserRoleAdmin, - entity.UserRoleEditor, - entity.UserRoleUser, - } { - if count, err := r.CountByRole(ctx, role); err != nil { - return nil, err - } else { - switch role { - case entity.UserRoleAdmin: - stats.Admins = count - case entity.UserRoleEditor: - stats.Editors = count - case entity.UserRoleUser: - stats.Users = count - } - } - } - - // 本月新注册用户 - now := time.Now() - startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond) - - var monthlyUsers int64 - if err := r.db.WithContext(ctx).Model(&entity.User{}). - Where("created_at >= ? AND created_at <= ?", startOfMonth, endOfMonth). - Count(&monthlyUsers).Error; err != nil { - return nil, err - } - stats.MonthlyRegistrations = monthlyUsers - - return &stats, nil -} \ No newline at end of file diff --git a/backend-old/internal/service/CLAUDE.md b/backend-old/internal/service/CLAUDE.md deleted file mode 100644 index 1d9b717..0000000 --- a/backend-old/internal/service/CLAUDE.md +++ /dev/null @@ -1,549 +0,0 @@ -# Service Layer - CLAUDE.md - -本文件为 Claude Code 在业务逻辑层中工作时提供指导。 - -## 🎯 模块概览 - -Service 层是业务逻辑的核心,负责处理业务规则、数据转换和服务编排。 - -### 主要职责 -- 📋 业务逻辑处理和规则验证 -- 🔄 数据转换和格式化 -- 🧩 服务编排和组合 -- 🚀 事务管理和数据一致性 -- 🔐 业务权限控制 -- 📊 性能优化和缓存策略 - -## 📁 模块结构 - -``` -internal/service/ -├── CLAUDE.md # 📋 当前文件 - 业务逻辑开发指导 -├── interfaces.go # 🔗 服务接口定义 -├── auth/ # 🔐 认证服务 -│ ├── auth_service.go # 认证业务逻辑 -│ ├── jwt_service.go # JWT 令牌服务 -│ └── auth_service_test.go # 认证服务测试 -├── user/ # 👤 用户服务 -│ ├── user_service.go # 用户业务逻辑 -│ ├── user_validator.go # 用户数据验证 -│ └── user_service_test.go # 用户服务测试 -├── photo/ # 📸 照片服务 -│ ├── photo_service.go # 照片业务逻辑 -│ ├── photo_processor.go # 照片处理器 -│ └── photo_service_test.go # 照片服务测试 -├── category/ # 📂 分类服务 -│ ├── category_service.go # 分类业务逻辑 -│ └── category_service_test.go # 分类服务测试 -└── storage/ # 📁 存储服务 - ├── storage_service.go # 存储业务逻辑 - ├── local_storage.go # 本地存储实现 - └── s3_storage.go # S3 存储实现 -``` - -## 🔧 Go 风格服务设计 - -### 接口定义规范 -```go -// interfaces.go - 服务接口定义 -package service - -import ( - "context" - "mime/multipart" - - "photography-backend/internal/model/dto" - "photography-backend/internal/model/entity" -) - -// UserServicer 用户服务接口 -type UserServicer interface { - // 用户管理 - CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) - GetUser(ctx context.Context, id uint) (*entity.User, error) - GetUserByEmail(ctx context.Context, email string) (*entity.User, error) - UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) (*entity.User, error) - DeleteUser(ctx context.Context, id uint) error - ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error) - - // 用户验证 - ValidateUser(ctx context.Context, email, password string) (*entity.User, error) - ChangePassword(ctx context.Context, id uint, req *dto.ChangePasswordRequest) error -} - -// PhotoServicer 照片服务接口 -type PhotoServicer interface { - // 照片管理 - CreatePhoto(ctx context.Context, req *dto.CreatePhotoRequest) (*entity.Photo, error) - GetPhoto(ctx context.Context, id uint) (*entity.Photo, error) - UpdatePhoto(ctx context.Context, id uint, req *dto.UpdatePhotoRequest) (*entity.Photo, error) - DeletePhoto(ctx context.Context, id uint) error - ListPhotos(ctx context.Context, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) - - // 照片处理 - UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) - ProcessPhoto(ctx context.Context, photoID uint, opts *dto.ProcessPhotoOptions) error - - // 照片查询 - GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) - GetPhotosByCategory(ctx context.Context, categoryID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) - SearchPhotos(ctx context.Context, query string, opts *dto.SearchPhotosOptions) ([]*entity.Photo, int64, error) -} - -// AuthServicer 认证服务接口 -type AuthServicer interface { - // 认证 - Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error) - Register(ctx context.Context, req *dto.RegisterRequest) (*dto.RegisterResponse, error) - RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error) - Logout(ctx context.Context, token string) error - - // 令牌管理 - ValidateToken(ctx context.Context, token string) (*dto.TokenClaims, error) - RevokeToken(ctx context.Context, token string) error -} -``` - -### 服务实现规范 -```go -// user/user_service.go - 用户服务实现 -package user - -import ( - "context" - "errors" - "fmt" - - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" - - "photography-backend/internal/model/dto" - "photography-backend/internal/model/entity" - "photography-backend/internal/repository" - "photography-backend/pkg/logger" -) - -type UserService struct { - userRepo repository.UserRepositoryr - logger logger.Logger -} - -func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) *UserService { - return &UserService{ - userRepo: userRepo, - logger: logger, - } -} - -func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) { - // 1. 验证输入 - if err := s.validateCreateUserRequest(req); err != nil { - s.logger.Error("invalid create user request", zap.Error(err)) - return nil, err - } - - // 2. 检查用户是否存在 - existingUser, err := s.userRepo.GetByEmail(ctx, req.Email) - if err != nil && !errors.Is(err, repository.ErrNotFound) { - s.logger.Error("failed to check existing user", zap.Error(err)) - return nil, err - } - if existingUser != nil { - return nil, errors.New("user already exists") - } - - // 3. 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - s.logger.Error("failed to hash password", zap.Error(err)) - return nil, err - } - - // 4. 创建用户实体 - user := &entity.User{ - Email: req.Email, - Username: req.Username, - Password: string(hashedPassword), - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - // 5. 保存用户 - createdUser, err := s.userRepo.Create(ctx, user) - if err != nil { - s.logger.Error("failed to create user", zap.Error(err)) - return nil, err - } - - s.logger.Info("user created successfully", - zap.String("user_id", fmt.Sprintf("%d", createdUser.ID)), - zap.String("email", createdUser.Email)) - - return createdUser, nil -} - -func (s *UserService) GetUser(ctx context.Context, id uint) (*entity.User, error) { - user, err := s.userRepo.GetByID(ctx, id) - if err != nil { - if errors.Is(err, repository.ErrNotFound) { - return nil, errors.New("user not found") - } - s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", id)) - return nil, err - } - - return user, nil -} - -func (s *UserService) ValidateUser(ctx context.Context, email, password string) (*entity.User, error) { - // 1. 获取用户 - user, err := s.userRepo.GetByEmail(ctx, email) - if err != nil { - if errors.Is(err, repository.ErrNotFound) { - return nil, errors.New("invalid credentials") - } - s.logger.Error("failed to get user by email", zap.Error(err)) - return nil, err - } - - // 2. 验证密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return nil, errors.New("invalid credentials") - } - - // 3. 检查用户状态 - if user.Status != entity.UserStatusActive { - return nil, errors.New("user account is not active") - } - - return user, nil -} - -func (s *UserService) validateCreateUserRequest(req *dto.CreateUserRequest) error { - if req.Email == "" { - return errors.New("email is required") - } - if req.Username == "" { - return errors.New("username is required") - } - if req.Password == "" { - return errors.New("password is required") - } - if len(req.Password) < 6 { - return errors.New("password must be at least 6 characters") - } - return nil -} -``` - -### 照片服务实现 -```go -// photo/photo_service.go - 照片服务实现 -package photo - -import ( - "context" - "errors" - "fmt" - "mime/multipart" - "path/filepath" - "strings" - "time" - - "go.uber.org/zap" - - "photography-backend/internal/model/dto" - "photography-backend/internal/model/entity" - "photography-backend/internal/repository" - "photography-backend/internal/service" - "photography-backend/pkg/logger" -) - -type PhotoService struct { - photoRepo repository.PhotoRepositoryr - userRepo repository.UserRepositoryr - storageService service.StorageServicer - logger logger.Logger -} - -func NewPhotoService( - photoRepo repository.PhotoRepositoryr, - userRepo repository.UserRepositoryr, - storageService service.StorageServicer, - logger logger.Logger, -) *PhotoService { - return &PhotoService{ - photoRepo: photoRepo, - userRepo: userRepo, - storageService: storageService, - logger: logger, - } -} - -func (s *PhotoService) UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) { - // 1. 验证文件 - if err := s.validatePhotoFile(file); err != nil { - s.logger.Error("invalid photo file", zap.Error(err)) - return nil, err - } - - // 2. 验证用户 - user, err := s.userRepo.GetByID(ctx, userID) - if err != nil { - s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", userID)) - return nil, err - } - - // 3. 生成文件名 - filename := s.generatePhotoFilename(file.Filename, userID) - - // 4. 上传文件 - uploadedFile, err := s.storageService.UploadFile(ctx, file, filename) - if err != nil { - s.logger.Error("failed to upload photo", zap.Error(err)) - return nil, err - } - - // 5. 创建照片记录 - photo := &entity.Photo{ - UserID: userID, - Title: strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)), - Description: "", - Filename: uploadedFile.Filename, - FilePath: uploadedFile.Path, - FileSize: uploadedFile.Size, - MimeType: uploadedFile.MimeType, - Width: uploadedFile.Width, - Height: uploadedFile.Height, - Status: entity.PhotoStatusActive, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - // 6. 保存照片记录 - createdPhoto, err := s.photoRepo.Create(ctx, photo) - if err != nil { - // 如果数据库保存失败,删除已上传的文件 - s.storageService.DeleteFile(ctx, uploadedFile.Path) - s.logger.Error("failed to create photo record", zap.Error(err)) - return nil, err - } - - s.logger.Info("photo uploaded successfully", - zap.String("photo_id", fmt.Sprintf("%d", createdPhoto.ID)), - zap.String("filename", createdPhoto.Filename), - zap.Uint("user_id", userID)) - - return createdPhoto, nil -} - -func (s *PhotoService) GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) { - // 设置默认选项 - if opts == nil { - opts = &dto.ListPhotosOptions{ - Page: 1, - Limit: 20, - Sort: "created_at", - Order: "desc", - } - } - - // 查询照片 - photos, total, err := s.photoRepo.ListByUserID(ctx, userID, &repository.ListPhotosOptions{ - Page: opts.Page, - Limit: opts.Limit, - Sort: opts.Sort, - Order: opts.Order, - Status: string(entity.PhotoStatusActive), - }) - - if err != nil { - s.logger.Error("failed to list photos by user", zap.Error(err), zap.Uint("user_id", userID)) - return nil, 0, err - } - - return photos, total, nil -} - -func (s *PhotoService) validatePhotoFile(file *multipart.FileHeader) error { - // 检查文件大小 (10MB 限制) - const maxFileSize = 10 * 1024 * 1024 - if file.Size > maxFileSize { - return errors.New("file size exceeds limit") - } - - // 检查文件类型 - ext := strings.ToLower(filepath.Ext(file.Filename)) - allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} - - isAllowed := false - for _, allowedExt := range allowedExts { - if ext == allowedExt { - isAllowed = true - break - } - } - - if !isAllowed { - return errors.New("file type not allowed") - } - - return nil -} - -func (s *PhotoService) generatePhotoFilename(originalFilename string, userID uint) string { - ext := filepath.Ext(originalFilename) - timestamp := time.Now().Unix() - return fmt.Sprintf("photos/%d_%d%s", userID, timestamp, ext) -} -``` - -## 🧪 服务测试 - -### 单元测试规范 -```go -// user/user_service_test.go - 用户服务测试 -package user - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "photography-backend/internal/model/dto" - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/mocks" - "photography-backend/pkg/logger" -) - -func TestUserService_CreateUser(t *testing.T) { - // Setup - mockUserRepo := new(mocks.UserRepositoryr) - mockLogger := logger.NewNoop() - userService := NewUserService(mockUserRepo, mockLogger) - - ctx := context.Background() - req := &dto.CreateUserRequest{ - Email: "test@example.com", - Username: "testuser", - Password: "password123", - } - - // Mock expectations - mockUserRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound) - mockUserRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")). - Return(&entity.User{ - ID: 1, - Email: req.Email, - Username: req.Username, - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - }, nil) - - // Execute - user, err := userService.CreateUser(ctx, req) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, user) - assert.Equal(t, req.Email, user.Email) - assert.Equal(t, req.Username, user.Username) - assert.Equal(t, entity.UserRoleUser, user.Role) - assert.Equal(t, entity.UserStatusActive, user.Status) - - mockUserRepo.AssertExpectations(t) -} - -func TestUserService_CreateUser_UserAlreadyExists(t *testing.T) { - // Setup - mockUserRepo := new(mocks.UserRepositoryr) - mockLogger := logger.NewNoop() - userService := NewUserService(mockUserRepo, mockLogger) - - ctx := context.Background() - req := &dto.CreateUserRequest{ - Email: "test@example.com", - Username: "testuser", - Password: "password123", - } - - existingUser := &entity.User{ - ID: 1, - Email: req.Email, - } - - // Mock expectations - mockUserRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil) - - // Execute - user, err := userService.CreateUser(ctx, req) - - // Assert - assert.Error(t, err) - assert.Nil(t, user) - assert.Contains(t, err.Error(), "user already exists") - - mockUserRepo.AssertExpectations(t) -} -``` - -## 🔍 错误处理 - -### 自定义错误类型 -```go -// errors.go - 服务层错误定义 -package service - -import "errors" - -var ( - // 通用错误 - ErrInvalidInput = errors.New("invalid input") - ErrInternalError = errors.New("internal error") - ErrNotFound = errors.New("not found") - ErrUnauthorized = errors.New("unauthorized") - ErrForbidden = errors.New("forbidden") - - // 用户相关错误 - ErrUserNotFound = errors.New("user not found") - ErrUserExists = errors.New("user already exists") - ErrInvalidCredentials = errors.New("invalid credentials") - ErrUserInactive = errors.New("user account is not active") - - // 照片相关错误 - ErrPhotoNotFound = errors.New("photo not found") - ErrInvalidFileType = errors.New("invalid file type") - ErrFileSizeExceeded = errors.New("file size exceeded") - ErrUploadFailed = errors.New("file upload failed") - - // 认证相关错误 - ErrInvalidToken = errors.New("invalid token") - ErrTokenExpired = errors.New("token expired") - ErrTokenRevoked = errors.New("token revoked") -) -``` - -## 💡 最佳实践 - -### 服务设计原则 -1. **单一职责**: 每个服务只负责一个业务领域 -2. **接口优先**: 先定义接口,再实现具体逻辑 -3. **依赖注入**: 通过构造函数注入依赖 -4. **错误处理**: 明确的错误类型和处理 -5. **日志记录**: 关键操作的日志记录 - -### 性能优化 -1. **缓存策略**: 合理使用缓存减少数据库查询 -2. **批量操作**: 优化批量数据处理 -3. **异步处理**: 使用 goroutine 处理耗时操作 -4. **数据库优化**: 合理使用索引和查询优化 - -### 安全考虑 -1. **输入验证**: 严格验证所有输入参数 -2. **权限检查**: 确保用户只能操作自己的数据 -3. **敏感信息**: 避免在日志中记录敏感信息 -4. **SQL 注入**: 使用参数化查询防止 SQL 注入 - -本模块是业务逻辑的核心,确保代码质量和测试覆盖率是关键。 \ No newline at end of file diff --git a/backend-old/internal/service/auth/auth_service.go b/backend-old/internal/service/auth/auth_service.go deleted file mode 100644 index ffb14fc..0000000 --- a/backend-old/internal/service/auth/auth_service.go +++ /dev/null @@ -1,249 +0,0 @@ -package auth - -import ( - "fmt" - "golang.org/x/crypto/bcrypt" - "photography-backend/internal/model/entity" - "photography-backend/internal/model/dto" - "photography-backend/internal/repository/postgres" -) - -// AuthService 认证服务 -type AuthService struct { - userRepo postgres.UserRepository - jwtService *JWTService -} - -// NewAuthService 创建认证服务 -func NewAuthService(userRepo postgres.UserRepository, jwtService *JWTService) *AuthService { - return &AuthService{ - userRepo: userRepo, - jwtService: jwtService, - } -} - -// Login 用户登录 -func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) { - // 根据用户名或邮箱查找用户 - var user *entity.User - var err error - - // 按邮箱查找用户 - user, err = s.userRepo.GetByEmail(req.Email) - if err != nil { - return nil, fmt.Errorf("failed to get user by email: %w", err) - } - - if user == nil { - return nil, fmt.Errorf("invalid credentials") - } - - // 检查用户是否激活 - if !user.IsActive { - return nil, fmt.Errorf("user account is deactivated") - } - - // 验证密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { - return nil, fmt.Errorf("invalid credentials") - } - - // 生成JWT令牌 - tokenPair, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, string(user.Role)) - if err != nil { - return nil, fmt.Errorf("failed to generate tokens: %w", err) - } - - // 更新最后登录时间 - if err := s.userRepo.UpdateLastLogin(user.ID); err != nil { - // 记录错误但不中断登录流程 - fmt.Printf("failed to update last login: %v\n", err) - } - - // 清除密码字段 - user.Password = "" - - return &dto.LoginResponse{ - Token: dto.TokenResponse{ - AccessToken: tokenPair.AccessToken, - RefreshToken: tokenPair.RefreshToken, - TokenType: tokenPair.TokenType, - ExpiresIn: tokenPair.ExpiresIn, - }, - User: *dto.ConvertToUserResponse(user), - }, nil -} - -// Register 用户注册 -func (s *AuthService) Register(req *dto.CreateUserRequest) (*entity.User, error) { - // 检查用户名是否已存在 - existingUser, err := s.userRepo.GetByUsername(req.Username) - if err != nil { - return nil, fmt.Errorf("failed to check username: %w", err) - } - if existingUser != nil { - return nil, fmt.Errorf("username already exists") - } - - // 检查邮箱是否已存在 - existingUser, err = s.userRepo.GetByEmail(req.Email) - if err != nil { - return nil, fmt.Errorf("failed to check email: %w", err) - } - if existingUser != nil { - return nil, fmt.Errorf("email already exists") - } - - // 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("failed to hash password: %w", err) - } - - // 创建用户 - user := &entity.User{ - Username: req.Username, - Email: req.Email, - Password: string(hashedPassword), - Name: req.Name, - Role: req.Role, - IsActive: true, - } - - // 如果没有指定角色,默认为普通用户 - if user.Role == "" { - user.Role = entity.UserRoleUser - } - - if err := s.userRepo.Create(user); err != nil { - return nil, fmt.Errorf("failed to create user: %w", err) - } - - // 清除密码字段 - user.Password = "" - - return user, nil -} - -// RefreshToken 刷新令牌 -func (s *AuthService) RefreshToken(req *dto.RefreshTokenRequest) (*dto.LoginResponse, error) { - // 验证刷新令牌 - claims, err := s.jwtService.ValidateToken(req.RefreshToken) - if err != nil { - return nil, fmt.Errorf("invalid refresh token: %w", err) - } - - // 获取用户信息 - user, err := s.userRepo.GetByID(claims.UserID) - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - - if user == nil { - return nil, fmt.Errorf("user not found") - } - - // 检查用户是否激活 - if !user.IsActive { - return nil, fmt.Errorf("user account is deactivated") - } - - // 生成新的令牌对 - tokenPair, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, string(user.Role)) - if err != nil { - return nil, fmt.Errorf("failed to generate tokens: %w", err) - } - - // 清除密码字段 - user.Password = "" - - return &dto.LoginResponse{ - Token: dto.TokenResponse{ - AccessToken: tokenPair.AccessToken, - RefreshToken: tokenPair.RefreshToken, - TokenType: tokenPair.TokenType, - ExpiresIn: tokenPair.ExpiresIn, - }, - User: *dto.ConvertToUserResponse(user), - }, nil -} - -// GetUserByID 根据ID获取用户 -func (s *AuthService) GetUserByID(id uint) (*entity.User, error) { - user, err := s.userRepo.GetByID(id) - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - - if user == nil { - return nil, fmt.Errorf("user not found") - } - - // 清除密码字段 - user.Password = "" - - return user, nil -} - -// UpdatePassword 更新密码 -func (s *AuthService) UpdatePassword(userID uint, req *dto.ChangePasswordRequest) error { - // 获取用户信息 - user, err := s.userRepo.GetByID(userID) - if err != nil { - return fmt.Errorf("failed to get user: %w", err) - } - - if user == nil { - return fmt.Errorf("user not found") - } - - // 验证旧密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil { - return fmt.Errorf("invalid old password") - } - - // 加密新密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) - if err != nil { - return fmt.Errorf("failed to hash password: %w", err) - } - - // 更新密码 - user.Password = string(hashedPassword) - if err := s.userRepo.Update(user); err != nil { - return fmt.Errorf("failed to update password: %w", err) - } - - return nil -} - -// CheckPermission 检查权限 -func (s *AuthService) CheckPermission(userRole entity.UserRole, requiredRole entity.UserRole) bool { - roleLevel := map[entity.UserRole]int{ - entity.UserRoleUser: 1, - entity.UserRolePhotographer: 2, - entity.UserRoleAdmin: 3, - } - - userLevel, exists := roleLevel[userRole] - if !exists { - return false - } - - requiredLevel, exists := roleLevel[requiredRole] - if !exists { - return false - } - - return userLevel >= requiredLevel -} - -// IsAdmin 检查是否为管理员 -func (s *AuthService) IsAdmin(userRole entity.UserRole) bool { - return userRole == entity.UserRoleAdmin -} - -// IsPhotographer 检查是否为摄影师或以上 -func (s *AuthService) IsPhotographer(userRole entity.UserRole) bool { - return userRole == entity.UserRolePhotographer || userRole == entity.UserRoleAdmin -} \ No newline at end of file diff --git a/backend-old/internal/service/auth/jwt_service.go b/backend-old/internal/service/auth/jwt_service.go deleted file mode 100644 index 1782d04..0000000 --- a/backend-old/internal/service/auth/jwt_service.go +++ /dev/null @@ -1,132 +0,0 @@ -package auth - -import ( - "fmt" - "time" - "github.com/golang-jwt/jwt/v5" - "photography-backend/internal/config" -) - -// JWTService JWT服务 -type JWTService struct { - secretKey []byte - accessTokenDuration time.Duration - refreshTokenDuration time.Duration -} - -// JWTClaims JWT声明 -type JWTClaims struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Role string `json:"role"` - jwt.RegisteredClaims -} - -// TokenPair 令牌对 -type TokenPair struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` -} - -// NewJWTService 创建JWT服务 -func NewJWTService(cfg *config.JWTConfig) *JWTService { - accessDuration, _ := time.ParseDuration(cfg.ExpiresIn) - refreshDuration, _ := time.ParseDuration(cfg.RefreshExpiresIn) - - return &JWTService{ - secretKey: []byte(cfg.Secret), - accessTokenDuration: accessDuration, - refreshTokenDuration: refreshDuration, - } -} - -// GenerateTokenPair 生成令牌对 -func (s *JWTService) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) { - // 生成访问令牌 - accessToken, err := s.generateToken(userID, username, role, s.accessTokenDuration) - if err != nil { - return nil, fmt.Errorf("failed to generate access token: %w", err) - } - - // 生成刷新令牌 - refreshToken, err := s.generateToken(userID, username, role, s.refreshTokenDuration) - if err != nil { - return nil, fmt.Errorf("failed to generate refresh token: %w", err) - } - - return &TokenPair{ - AccessToken: accessToken, - RefreshToken: refreshToken, - TokenType: "Bearer", - ExpiresIn: int64(s.accessTokenDuration.Seconds()), - }, nil -} - -// generateToken 生成令牌 -func (s *JWTService) generateToken(userID uint, username, role string, duration time.Duration) (string, error) { - now := time.Now() - claims := &JWTClaims{ - UserID: userID, - Username: username, - Role: role, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(now.Add(duration)), - IssuedAt: jwt.NewNumericDate(now), - NotBefore: jwt.NewNumericDate(now), - Issuer: "photography-backend", - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(s.secretKey) -} - -// ValidateToken 验证令牌 -func (s *JWTService) ValidateToken(tokenString string) (*JWTClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return s.secretKey, nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to parse token: %w", err) - } - - if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { - return claims, nil - } - - return nil, fmt.Errorf("invalid token") -} - -// RefreshToken 刷新令牌 -func (s *JWTService) RefreshToken(refreshToken string) (*TokenPair, error) { - claims, err := s.ValidateToken(refreshToken) - if err != nil { - return nil, fmt.Errorf("invalid refresh token: %w", err) - } - - // 生成新的令牌对 - return s.GenerateTokenPair(claims.UserID, claims.Username, claims.Role) -} - -// GetClaimsFromToken 从令牌中获取声明 -func (s *JWTService) GetClaimsFromToken(tokenString string) (*JWTClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { - return s.secretKey, nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*JWTClaims); ok { - return claims, nil - } - - return nil, fmt.Errorf("invalid claims") -} \ No newline at end of file diff --git a/backend-old/internal/service/category_service.go b/backend-old/internal/service/category_service.go deleted file mode 100644 index 99ffc3e..0000000 --- a/backend-old/internal/service/category_service.go +++ /dev/null @@ -1,233 +0,0 @@ -package service - -import ( - "context" - "errors" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - - "go.uber.org/zap" -) - -type CategoryService struct { - categoryRepo interfaces.CategoryRepository - logger *zap.Logger -} - -func NewCategoryService(categoryRepo interfaces.CategoryRepository, logger *zap.Logger) *CategoryService { - return &CategoryService{ - categoryRepo: categoryRepo, - logger: logger, - } -} - -// GetCategories 获取分类列表 -func (s *CategoryService) GetCategories(ctx context.Context, parentID *uint) ([]*entity.Category, error) { - categories, err := s.categoryRepo.List(ctx, parentID) - if err != nil { - s.logger.Error("Failed to get categories", zap.Error(err)) - return nil, err - } - - return categories, nil -} - -// GetCategoryTree 获取分类树 -func (s *CategoryService) GetCategoryTree(ctx context.Context) ([]*entity.CategoryTree, error) { - tree, err := s.categoryRepo.GetTree(ctx) - if err != nil { - s.logger.Error("Failed to get category tree", zap.Error(err)) - return nil, err - } - - return tree, nil -} - -// GetCategoryByID 根据ID获取分类 -func (s *CategoryService) GetCategoryByID(ctx context.Context, id uint) (*entity.Category, error) { - category, err := s.categoryRepo.GetByID(ctx, id) - if err != nil { - s.logger.Error("Failed to get category by ID", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - - return category, nil -} - -// GetCategoryBySlug 根据slug获取分类 -func (s *CategoryService) GetCategoryBySlug(ctx context.Context, slug string) (*entity.Category, error) { - category, err := s.categoryRepo.GetBySlug(ctx, slug) - if err != nil { - s.logger.Error("Failed to get category by slug", zap.Error(err), zap.String("slug", slug)) - return nil, err - } - - return category, nil -} - -// CreateCategory 创建分类 -func (s *CategoryService) CreateCategory(ctx context.Context, req *entity.CreateCategoryRequest) (*entity.Category, error) { - // 验证slug唯一性 - if err := s.categoryRepo.ValidateSlugUnique(ctx, req.Slug, 0); err != nil { - return nil, err - } - - // 验证父分类存在性 - if req.ParentID != nil { - if err := s.categoryRepo.ValidateParentCategory(ctx, 0, *req.ParentID); err != nil { - return nil, err - } - } - - // 获取排序顺序 - sortOrder, err := s.categoryRepo.GetNextSortOrder(ctx, req.ParentID) - if err != nil { - return nil, err - } - - category := &entity.Category{ - Name: req.Name, - Slug: req.Slug, - Description: req.Description, - ParentID: req.ParentID, - SortOrder: sortOrder, - IsActive: true, - } - - if err := s.categoryRepo.Create(ctx, category); err != nil { - s.logger.Error("Failed to create category", zap.Error(err)) - return nil, err - } - - s.logger.Info("Category created successfully", zap.Uint("id", category.ID)) - return category, nil -} - -// UpdateCategory 更新分类 -func (s *CategoryService) UpdateCategory(ctx context.Context, id uint, req *entity.UpdateCategoryRequest) (*entity.Category, error) { - // 检查分类是否存在 - category, err := s.categoryRepo.GetByID(ctx, id) - if err != nil { - s.logger.Error("Failed to get category", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - - // 验证slug唯一性 - if req.Slug != nil && *req.Slug != category.Slug { - if err := s.categoryRepo.ValidateSlugUnique(ctx, *req.Slug, id); err != nil { - return nil, err - } - } - - // 验证父分类(防止循环引用) - if req.ParentID != nil { - // 检查是否有变更 - if (category.ParentID == nil && *req.ParentID != 0) || (category.ParentID != nil && *req.ParentID != *category.ParentID) { - if err := s.categoryRepo.ValidateParentCategory(ctx, id, *req.ParentID); err != nil { - return nil, err - } - } - } - - // 更新字段 - if req.Name != nil { - category.Name = *req.Name - } - if req.Slug != nil { - category.Slug = *req.Slug - } - if req.Description != nil { - category.Description = *req.Description - } - if req.ParentID != nil { - if *req.ParentID == 0 { - category.ParentID = nil - } else { - category.ParentID = req.ParentID - } - } - if req.SortOrder != nil { - category.SortOrder = *req.SortOrder - } - if req.IsActive != nil { - category.IsActive = *req.IsActive - } - - // 保存更新 - if err := s.categoryRepo.Update(ctx, category); err != nil { - s.logger.Error("Failed to update category", zap.Error(err)) - return nil, err - } - - s.logger.Info("Category updated successfully", zap.Uint("id", id)) - return category, nil -} - -// DeleteCategory 删除分类 -func (s *CategoryService) DeleteCategory(ctx context.Context, id uint) error { - // 检查分类是否存在 - _, err := s.categoryRepo.GetByID(ctx, id) - if err != nil { - s.logger.Error("Failed to get category", zap.Error(err), zap.Uint("id", id)) - return err - } - - // 检查是否有子分类 - children, err := s.categoryRepo.GetChildren(ctx, id) - if err != nil { - return err - } - - if len(children) > 0 { - return errors.New("cannot delete category with subcategories") - } - - // 直接删除分类,在Repository层检查照片关联 - if err := s.categoryRepo.Delete(ctx, id); err != nil { - s.logger.Error("Failed to delete category", zap.Error(err)) - return err - } - - s.logger.Info("Category deleted successfully", zap.Uint("id", id)) - return nil -} - -// ReorderCategories 重新排序分类 -func (s *CategoryService) ReorderCategories(ctx context.Context, parentID *uint, categoryIDs []uint) error { - if len(categoryIDs) == 0 { - return nil - } - - // 重新排序分类 - if err := s.categoryRepo.Reorder(ctx, parentID, categoryIDs); err != nil { - s.logger.Error("Failed to reorder categories", zap.Error(err)) - return err - } - - s.logger.Info("Categories reordered successfully", zap.Int("count", len(categoryIDs))) - return nil -} - -// GetCategoryStats 获取分类统计信息 -func (s *CategoryService) GetCategoryStats(ctx context.Context) (*entity.CategoryStats, error) { - stats, err := s.categoryRepo.GetStats(ctx) - if err != nil { - s.logger.Error("Failed to get category stats", zap.Error(err)) - return nil, err - } - - return stats, nil -} - - -// GenerateSlug 生成唯一slug -func (s *CategoryService) GenerateSlug(ctx context.Context, name string) (string, error) { - slug, err := s.categoryRepo.GenerateUniqueSlug(ctx, name) - if err != nil { - s.logger.Error("Failed to generate unique slug", zap.Error(err)) - return "", err - } - - return slug, nil -} \ No newline at end of file diff --git a/backend-old/internal/service/photo_service.go b/backend-old/internal/service/photo_service.go deleted file mode 100644 index cce1aec..0000000 --- a/backend-old/internal/service/photo_service.go +++ /dev/null @@ -1,677 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "mime/multipart" - "path/filepath" - "strings" - "time" - - "photography-backend/internal/config" - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/interfaces" - "photography-backend/internal/service/storage" - "photography-backend/internal/utils" - - "go.uber.org/zap" -) - -type PhotoService struct { - photoRepo interfaces.PhotoRepository - config *config.Config - logger *zap.Logger - storageService *storage.StorageService -} - -func NewPhotoService(photoRepo interfaces.PhotoRepository, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService { - return &PhotoService{ - photoRepo: photoRepo, - config: config, - logger: logger, - storageService: storageService, - } -} - -// PhotoListParams 照片列表查询参数 -type PhotoListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Search string `json:"search" form:"search"` - Status string `json:"status" form:"status"` - CategoryID uint `json:"category_id" form:"category_id"` - Tags []string `json:"tags" form:"tags"` - StartDate string `json:"start_date" form:"start_date"` - EndDate string `json:"end_date" form:"end_date"` - SortBy string `json:"sort_by" form:"sort_by"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -// PhotoListResponse 照片列表响应 -type PhotoListResponse struct { - Photos []entity.Photo `json:"photos"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` - Pages int `json:"pages"` -} - -// GetPhotos 获取照片列表 -func (s *PhotoService) GetPhotos(ctx context.Context, params PhotoListParams) (*PhotoListResponse, error) { - // 设置默认值 - if params.Page <= 0 { - params.Page = 1 - } - if params.Limit <= 0 { - params.Limit = 20 - } - if params.Limit > 100 { - params.Limit = 100 - } - - // 构建查询 - query := s.db.WithContext(ctx). - Preload("Categories"). - Preload("Tags"). - Preload("Formats") - - // 搜索过滤 - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - query = query.Where("title ILIKE ? OR description ILIKE ? OR original_filename ILIKE ?", - searchPattern, searchPattern, searchPattern) - } - - // 状态过滤 - if params.Status != "" { - query = query.Where("status = ?", params.Status) - } - - // 分类过滤 - if params.CategoryID > 0 { - query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id"). - Where("photo_categories.category_id = ?", params.CategoryID) - } - - // 标签过滤 - if len(params.Tags) > 0 { - query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). - Joins("JOIN tags ON photo_tags.tag_id = tags.id"). - Where("tags.slug IN ?", params.Tags) - } - - // 日期过滤 - if params.StartDate != "" { - if startDate, err := time.Parse("2006-01-02", params.StartDate); err == nil { - query = query.Where("taken_at >= ?", startDate) - } - } - if params.EndDate != "" { - if endDate, err := time.Parse("2006-01-02", params.EndDate); err == nil { - query = query.Where("taken_at <= ?", endDate) - } - } - - // 排序 - sortBy := "created_at" - sortOrder := "desc" - if params.SortBy != "" { - allowedSortFields := []string{"created_at", "updated_at", "taken_at", "title", "file_size"} - if utils.Contains(allowedSortFields, params.SortBy) { - sortBy = params.SortBy - } - } - if params.SortOrder == "asc" { - sortOrder = "asc" - } - - // 计算总数 - var total int64 - countQuery := query - if err := countQuery.Model(&entity.Photo{}).Count(&total).Error; err != nil { - s.logger.Error("Failed to count photos", zap.Error(err)) - return nil, err - } - - // 分页查询 - offset := (params.Page - 1) * params.Limit - var photos []entity.Photo - if err := query. - Order(fmt.Sprintf("%s %s", sortBy, sortOrder)). - Offset(offset). - Limit(params.Limit). - Find(&photos).Error; err != nil { - s.logger.Error("Failed to get photos", zap.Error(err)) - return nil, err - } - - // 计算总页数 - pages := int((total + int64(params.Limit) - 1) / int64(params.Limit)) - - return &PhotoListResponse{ - Photos: photos, - Total: total, - Page: params.Page, - Limit: params.Limit, - Pages: pages, - }, nil -} - -// GetPhotoByID 根据ID获取照片 -func (s *PhotoService) GetPhotoByID(ctx context.Context, id uint) (*entity.Photo, error) { - var photo entity.Photo - if err := s.db.WithContext(ctx). - Preload("Categories"). - Preload("Tags"). - Preload("Formats"). - First(&photo, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("photo not found") - } - s.logger.Error("Failed to get photo by ID", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - - return &photo, nil -} - -// CreatePhoto 创建照片 -func (s *PhotoService) CreatePhoto(ctx context.Context, req *entity.CreatePhotoRequest) (*entity.Photo, error) { - // 生成唯一的文件名 - uniqueFilename := utils.GenerateUniqueFilename(req.OriginalFilename) - - photo := &entity.Photo{ - Title: req.Title, - Description: req.Description, - OriginalFilename: req.OriginalFilename, - UniqueFilename: uniqueFilename, - FileSize: req.FileSize, - Status: entity.PhotoStatus(req.Status), - Camera: req.Camera, - Lens: req.Lens, - ISO: req.ISO, - Aperture: req.Aperture, - ShutterSpeed: req.ShutterSpeed, - FocalLength: req.FocalLength, - TakenAt: req.TakenAt, - } - - // 开始事务 - tx := s.db.WithContext(ctx).Begin() - if tx.Error != nil { - return nil, tx.Error - } - defer tx.Rollback() - - // 创建照片记录 - if err := tx.Create(photo).Error; err != nil { - s.logger.Error("Failed to create photo", zap.Error(err)) - return nil, err - } - - // 关联分类 - if len(req.CategoryIDs) > 0 { - var categories []entity.Category - if err := tx.Where("id IN ?", req.CategoryIDs).Find(&categories).Error; err != nil { - s.logger.Error("Failed to find categories", zap.Error(err)) - return nil, err - } - if err := tx.Model(photo).Association("Categories").Replace(categories); err != nil { - s.logger.Error("Failed to associate categories", zap.Error(err)) - return nil, err - } - } - - // 关联标签 - if len(req.TagIDs) > 0 { - var tags []entity.Tag - if err := tx.Where("id IN ?", req.TagIDs).Find(&tags).Error; err != nil { - s.logger.Error("Failed to find tags", zap.Error(err)) - return nil, err - } - if err := tx.Model(photo).Association("Tags").Replace(tags); err != nil { - s.logger.Error("Failed to associate tags", zap.Error(err)) - return nil, err - } - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - s.logger.Error("Failed to commit transaction", zap.Error(err)) - return nil, err - } - - // 重新加载关联数据 - if err := s.db.WithContext(ctx). - Preload("Categories"). - Preload("Tags"). - Preload("Formats"). - First(photo, photo.ID).Error; err != nil { - s.logger.Error("Failed to reload photo", zap.Error(err)) - return nil, err - } - - s.logger.Info("Photo created successfully", zap.Uint("id", photo.ID)) - return photo, nil -} - -// UpdatePhoto 更新照片 -func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *entity.UpdatePhotoRequest) (*entity.Photo, error) { - // 检查照片是否存在 - var photo entity.Photo - if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("photo not found") - } - return nil, err - } - - // 开始事务 - tx := s.db.WithContext(ctx).Begin() - if tx.Error != nil { - return nil, tx.Error - } - defer tx.Rollback() - - // 更新照片基本信息 - updates := map[string]interface{}{} - if req.Title != nil { - updates["title"] = *req.Title - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Status != nil { - updates["status"] = *req.Status - } - if req.Camera != nil { - updates["camera"] = *req.Camera - } - if req.Lens != nil { - updates["lens"] = *req.Lens - } - if req.ISO != nil { - updates["iso"] = *req.ISO - } - if req.Aperture != nil { - updates["aperture"] = *req.Aperture - } - if req.ShutterSpeed != nil { - updates["shutter_speed"] = *req.ShutterSpeed - } - if req.FocalLength != nil { - updates["focal_length"] = *req.FocalLength - } - if req.TakenAt != nil { - updates["taken_at"] = *req.TakenAt - } - - if len(updates) > 0 { - if err := tx.Model(&photo).Updates(updates).Error; err != nil { - s.logger.Error("Failed to update photo", zap.Error(err)) - return nil, err - } - } - - // 更新分类关联 - if req.CategoryIDs != nil { - var categories []entity.Category - if len(*req.CategoryIDs) > 0 { - if err := tx.Where("id IN ?", *req.CategoryIDs).Find(&categories).Error; err != nil { - s.logger.Error("Failed to find categories", zap.Error(err)) - return nil, err - } - } - if err := tx.Model(&photo).Association("Categories").Replace(categories); err != nil { - s.logger.Error("Failed to update categories", zap.Error(err)) - return nil, err - } - } - - // 更新标签关联 - if req.TagIDs != nil { - var tags []entity.Tag - if len(*req.TagIDs) > 0 { - if err := tx.Where("id IN ?", *req.TagIDs).Find(&tags).Error; err != nil { - s.logger.Error("Failed to find tags", zap.Error(err)) - return nil, err - } - } - if err := tx.Model(&photo).Association("Tags").Replace(tags); err != nil { - s.logger.Error("Failed to update tags", zap.Error(err)) - return nil, err - } - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - s.logger.Error("Failed to commit transaction", zap.Error(err)) - return nil, err - } - - // 重新加载照片数据 - if err := s.db.WithContext(ctx). - Preload("Categories"). - Preload("Tags"). - Preload("Formats"). - First(&photo, id).Error; err != nil { - s.logger.Error("Failed to reload photo", zap.Error(err)) - return nil, err - } - - s.logger.Info("Photo updated successfully", zap.Uint("id", id)) - return &photo, nil -} - -// DeletePhoto 删除照片 -func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error { - // 检查照片是否存在 - var photo entity.Photo - if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("photo not found") - } - return err - } - - // 开始事务 - tx := s.db.WithContext(ctx).Begin() - if tx.Error != nil { - return tx.Error - } - defer tx.Rollback() - - // 删除关联的格式文件 - if err := tx.Where("photo_id = ?", id).Delete(&entity.PhotoFormat{}).Error; err != nil { - s.logger.Error("Failed to delete photo formats", zap.Error(err)) - return err - } - - // 删除关联关系 - if err := tx.Model(&photo).Association("Categories").Clear(); err != nil { - s.logger.Error("Failed to clear categories", zap.Error(err)) - return err - } - - if err := tx.Model(&photo).Association("Tags").Clear(); err != nil { - s.logger.Error("Failed to clear tags", zap.Error(err)) - return err - } - - // 删除照片记录 - if err := tx.Delete(&photo).Error; err != nil { - s.logger.Error("Failed to delete photo", zap.Error(err)) - return err - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - s.logger.Error("Failed to commit transaction", zap.Error(err)) - return err - } - - // 异步删除文件 - go func() { - if err := (*s.storageService).DeletePhoto(photo.UniqueFilename); err != nil { - s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename)) - } - }() - - s.logger.Info("Photo deleted successfully", zap.Uint("id", id)) - return nil -} - -// UploadPhoto 上传照片 -func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, header *multipart.FileHeader, req *entity.CreatePhotoRequest) (*entity.Photo, error) { - // 验证文件类型 - if !s.isValidImageFile(header.Filename) { - return nil, errors.New("invalid file type") - } - - // 验证文件大小 - if header.Size > s.config.Upload.MaxFileSize { - return nil, errors.New("file size too large") - } - - // 生成唯一文件名 - uniqueFilename := utils.GenerateUniqueFilename(header.Filename) - - // 上传文件到存储服务 - uploadedFile, err := (*s.storageService).UploadPhoto(ctx, file, uniqueFilename) - if err != nil { - s.logger.Error("Failed to upload photo", zap.Error(err)) - return nil, err - } - - // 创建照片记录 - req.OriginalFilename = header.Filename - req.FileSize = header.Size - - photo, err := s.CreatePhoto(ctx, req) - if err != nil { - // 如果创建记录失败,删除已上传的文件 - go func() { - if err := (*s.storageService).DeletePhoto(uniqueFilename); err != nil { - s.logger.Error("Failed to cleanup uploaded file", zap.Error(err)) - } - }() - return nil, err - } - - // 异步处理图片格式转换 - go func() { - s.processPhotoFormats(context.Background(), photo, uploadedFile) - }() - - return photo, nil -} - -// BatchUpdatePhotos 批量更新照片 -func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *entity.BatchUpdatePhotosRequest) error { - if len(ids) == 0 { - return errors.New("no photos to update") - } - - // 开始事务 - tx := s.db.WithContext(ctx).Begin() - if tx.Error != nil { - return tx.Error - } - defer tx.Rollback() - - // 构建更新数据 - updates := map[string]interface{}{} - if req.Status != nil { - updates["status"] = *req.Status - } - - // 基础字段更新 - if len(updates) > 0 { - if err := tx.Model(&entity.Photo{}).Where("id IN ?", ids).Updates(updates).Error; err != nil { - s.logger.Error("Failed to batch update photos", zap.Error(err)) - return err - } - } - - // 批量更新分类 - if req.CategoryIDs != nil { - // 先删除现有关联 - if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - // 添加新关联 - if len(*req.CategoryIDs) > 0 { - for _, photoID := range ids { - for _, categoryID := range *req.CategoryIDs { - if err := tx.Exec("INSERT INTO photo_categories (photo_id, category_id) VALUES (?, ?)", photoID, categoryID).Error; err != nil { - return err - } - } - } - } - } - - // 批量更新标签 - if req.TagIDs != nil { - // 先删除现有关联 - if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - // 添加新关联 - if len(*req.TagIDs) > 0 { - for _, photoID := range ids { - for _, tagID := range *req.TagIDs { - if err := tx.Exec("INSERT INTO photo_tags (photo_id, tag_id) VALUES (?, ?)", photoID, tagID).Error; err != nil { - return err - } - } - } - } - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - s.logger.Error("Failed to commit batch update", zap.Error(err)) - return err - } - - s.logger.Info("Batch update completed", zap.Int("count", len(ids))) - return nil -} - -// BatchDeletePhotos 批量删除照片 -func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return errors.New("no photos to delete") - } - - // 获取要删除的照片信息 - var photos []entity.Photo - if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&photos).Error; err != nil { - return err - } - - // 开始事务 - tx := s.db.WithContext(ctx).Begin() - if tx.Error != nil { - return tx.Error - } - defer tx.Rollback() - - // 删除关联的格式文件 - if err := tx.Where("photo_id IN ?", ids).Delete(&entity.PhotoFormat{}).Error; err != nil { - return err - } - - // 删除关联关系 - if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil { - return err - } - - // 删除照片记录 - if err := tx.Where("id IN ?", ids).Delete(&entity.Photo{}).Error; err != nil { - return err - } - - // 提交事务 - if err := tx.Commit().Error; err != nil { - return err - } - - // 异步删除文件 - go func() { - for _, photo := range photos { - if err := (*s.storageService).DeletePhoto(photo.UniqueFilename); err != nil { - s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename)) - } - } - }() - - s.logger.Info("Batch delete completed", zap.Int("count", len(ids))) - return nil -} - -// GetPhotoStats 获取照片统计信息 -func (s *PhotoService) GetPhotoStats(ctx context.Context) (*entity.PhotoStats, error) { - var stats entity.PhotoStats - - // 总数统计 - if err := s.db.WithContext(ctx).Model(&entity.Photo{}).Count(&stats.Total).Error; err != nil { - return nil, err - } - - // 按状态统计 - var statusStats []struct { - Status string `json:"status"` - Count int64 `json:"count"` - } - if err := s.db.WithContext(ctx).Model(&entity.Photo{}). - Select("status, COUNT(*) as count"). - Group("status"). - Find(&statusStats).Error; err != nil { - return nil, err - } - - stats.StatusStats = make(map[string]int64) - for _, stat := range statusStats { - stats.StatusStats[stat.Status] = stat.Count - } - - // 本月新增 - startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1) - if err := s.db.WithContext(ctx).Model(&entity.Photo{}). - Where("created_at >= ?", startOfMonth). - Count(&stats.ThisMonth).Error; err != nil { - return nil, err - } - - // 今日新增 - startOfDay := time.Now().Truncate(24 * time.Hour) - if err := s.db.WithContext(ctx).Model(&entity.Photo{}). - Where("created_at >= ?", startOfDay). - Count(&stats.Today).Error; err != nil { - return nil, err - } - - // 总存储大小 - var totalSize sql.NullInt64 - if err := s.db.WithContext(ctx).Model(&entity.Photo{}). - Select("SUM(file_size)"). - Row().Scan(&totalSize); err != nil { - return nil, err - } - if totalSize.Valid { - stats.TotalSize = totalSize.Int64 - } - - return &stats, nil -} - -// isValidImageFile 验证图片文件类型 -func (s *PhotoService) isValidImageFile(filename string) bool { - ext := strings.ToLower(filepath.Ext(filename)) - allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"} - return utils.Contains(allowedExts, ext) -} - -// processPhotoFormats 处理照片格式转换 -func (s *PhotoService) processPhotoFormats(ctx context.Context, photo *entity.Photo, uploadedFile *storage.UploadedFile) { - // 这里将实现图片格式转换逻辑 - // 生成不同尺寸和格式的图片 - // 更新 photo_formats 表 - - s.logger.Info("Processing photo formats", zap.Uint("photo_id", photo.ID)) - - // TODO: 实现图片处理逻辑 - // 1. 生成缩略图 - // 2. 生成不同尺寸的图片 - // 3. 转换为不同格式 (WebP, AVIF) - // 4. 更新数据库记录 -} \ No newline at end of file diff --git a/backend-old/internal/service/storage/storage.go b/backend-old/internal/service/storage/storage.go deleted file mode 100644 index c911ec1..0000000 --- a/backend-old/internal/service/storage/storage.go +++ /dev/null @@ -1,218 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "io" - "mime/multipart" - "os" - "path/filepath" - - "photography-backend/internal/config" - - "go.uber.org/zap" -) - -// UploadedFile 上传后的文件信息 -type UploadedFile struct { - Filename string `json:"filename"` - OriginalURL string `json:"original_url"` - ThumbnailURL string `json:"thumbnail_url,omitempty"` - Size int64 `json:"size"` - MimeType string `json:"mime_type"` -} - -// StorageService 存储服务接口 -type StorageService interface { - UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error) - DeletePhoto(filename string) error - GetPhotoURL(filename string) string - GenerateThumbnail(ctx context.Context, filename string) error -} - -// LocalStorageService 本地存储服务实现 -type LocalStorageService struct { - config *config.Config - logger *zap.Logger - uploadDir string - baseURL string -} - -// NewLocalStorageService 创建本地存储服务 -func NewLocalStorageService(config *config.Config, logger *zap.Logger) *LocalStorageService { - uploadDir := config.Storage.Local.BasePath - if uploadDir == "" { - uploadDir = "./uploads" - } - - baseURL := config.Storage.Local.BaseURL - if baseURL == "" { - baseURL = fmt.Sprintf("http://localhost:%d/uploads", config.App.Port) - } - - // 确保上传目录存在 - if err := os.MkdirAll(uploadDir, 0755); err != nil { - logger.Error("Failed to create upload directory", zap.Error(err)) - } - - // 创建子目录 - dirs := []string{"photos", "thumbnails", "temp"} - for _, dir := range dirs { - dirPath := filepath.Join(uploadDir, dir) - if err := os.MkdirAll(dirPath, 0755); err != nil { - logger.Error("Failed to create subdirectory", zap.String("dir", dir), zap.Error(err)) - } - } - - return &LocalStorageService{ - config: config, - logger: logger, - uploadDir: uploadDir, - baseURL: baseURL, - } -} - -// UploadPhoto 上传照片 -func (s *LocalStorageService) UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error) { - // 保存原图 - photoPath := filepath.Join(s.uploadDir, "photos", filename) - - out, err := os.Create(photoPath) - if err != nil { - s.logger.Error("Failed to create file", zap.String("path", photoPath), zap.Error(err)) - return nil, err - } - defer out.Close() - - // 重置文件指针 - file.Seek(0, 0) - - // 复制文件内容 - size, err := io.Copy(out, file) - if err != nil { - s.logger.Error("Failed to copy file", zap.Error(err)) - return nil, err - } - - // 获取文件信息 - _, err = out.Stat() - if err != nil { - s.logger.Error("Failed to get file info", zap.Error(err)) - return nil, err - } - - uploadedFile := &UploadedFile{ - Filename: filename, - OriginalURL: s.GetPhotoURL(filename), - Size: size, - MimeType: s.getMimeType(filename), - } - - s.logger.Info("Photo uploaded successfully", - zap.String("filename", filename), - zap.Int64("size", size)) - - return uploadedFile, nil -} - -// DeletePhoto 删除照片 -func (s *LocalStorageService) DeletePhoto(filename string) error { - // 删除原图 - photoPath := filepath.Join(s.uploadDir, "photos", filename) - if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) { - s.logger.Error("Failed to delete photo", zap.String("path", photoPath), zap.Error(err)) - return err - } - - // 删除缩略图 - thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) - if err := os.Remove(thumbnailPath); err != nil && !os.IsNotExist(err) { - s.logger.Warn("Failed to delete thumbnail", zap.String("path", thumbnailPath), zap.Error(err)) - } - - s.logger.Info("Photo deleted successfully", zap.String("filename", filename)) - return nil -} - -// GetPhotoURL 获取照片 URL -func (s *LocalStorageService) GetPhotoURL(filename string) string { - return fmt.Sprintf("%s/photos/%s", s.baseURL, filename) -} - -// GetThumbnailURL 获取缩略图 URL -func (s *LocalStorageService) GetThumbnailURL(filename string) string { - return fmt.Sprintf("%s/thumbnails/%s", s.baseURL, filename) -} - -// GenerateThumbnail 生成缩略图 -func (s *LocalStorageService) GenerateThumbnail(ctx context.Context, filename string) error { - // TODO: 实现缩略图生成逻辑 - // 这里需要使用图像处理库,如 imaging 或 bild - s.logger.Info("Generating thumbnail", zap.String("filename", filename)) - - // 示例实现 - 实际项目中应该使用图像处理库 - photoPath := filepath.Join(s.uploadDir, "photos", filename) - thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) - - // 检查原图是否存在 - if _, err := os.Stat(photoPath); os.IsNotExist(err) { - return fmt.Errorf("original photo not found: %s", filename) - } - - // 这里应该实现实际的缩略图生成逻辑 - // 暂时复制原图作为缩略图 - sourceFile, err := os.Open(photoPath) - if err != nil { - return err - } - defer sourceFile.Close() - - destFile, err := os.Create(thumbnailPath) - if err != nil { - return err - } - defer destFile.Close() - - _, err = io.Copy(destFile, sourceFile) - if err != nil { - return err - } - - s.logger.Info("Thumbnail generated successfully", zap.String("filename", filename)) - return nil -} - -// getMimeType 根据文件扩展名获取 MIME 类型 -func (s *LocalStorageService) getMimeType(filename string) string { - ext := filepath.Ext(filename) - switch ext { - case ".jpg", ".jpeg": - return "image/jpeg" - case ".png": - return "image/png" - case ".gif": - return "image/gif" - case ".webp": - return "image/webp" - case ".bmp": - return "image/bmp" - default: - return "application/octet-stream" - } -} - -// NewStorageService 根据配置创建存储服务 -func NewStorageService(config *config.Config, logger *zap.Logger) StorageService { - switch config.Storage.Type { - case "s3": - // TODO: 实现 S3 存储服务 - logger.Warn("S3 storage not implemented yet, using local storage") - return NewLocalStorageService(config, logger) - case "minio": - // TODO: 实现 MinIO 存储服务 - logger.Warn("MinIO storage not implemented yet, using local storage") - return NewLocalStorageService(config, logger) - default: - return NewLocalStorageService(config, logger) - } -} \ No newline at end of file diff --git a/backend-old/internal/service/tag_service.go b/backend-old/internal/service/tag_service.go deleted file mode 100644 index c2f9fc2..0000000 --- a/backend-old/internal/service/tag_service.go +++ /dev/null @@ -1,482 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "strings" - - "photography-backend/internal/model/entity" - "photography-backend/internal/utils" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type TagService struct { - db *gorm.DB - logger *zap.Logger -} - -func NewTagService(db *gorm.DB, logger *zap.Logger) *TagService { - return &TagService{ - db: db, - logger: logger, - } -} - -// TagListParams 标签列表查询参数 -type TagListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Search string `json:"search" form:"search"` - IsActive *bool `json:"is_active" form:"is_active"` - SortBy string `json:"sort_by" form:"sort_by"` - SortOrder string `json:"sort_order" form:"sort_order"` -} - -// TagListResponse 标签列表响应 -type TagListResponse struct { - Tags []entity.Tag `json:"tags"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` - Pages int `json:"pages"` -} - -// GetTags 获取标签列表 -func (s *TagService) GetTags(ctx context.Context, params TagListParams) (*TagListResponse, error) { - // 设置默认值 - if params.Page <= 0 { - params.Page = 1 - } - if params.Limit <= 0 { - params.Limit = 20 - } - if params.Limit > 100 { - params.Limit = 100 - } - - // 构建查询 - query := s.db.WithContext(ctx) - - // 搜索过滤 - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - query = query.Where("name ILIKE ? OR slug ILIKE ?", searchPattern, searchPattern) - } - - // 状态过滤 - if params.IsActive != nil { - query = query.Where("is_active = ?", *params.IsActive) - } - - // 排序 - sortBy := "created_at" - sortOrder := "desc" - if params.SortBy != "" { - allowedSortFields := []string{"created_at", "updated_at", "name", "photo_count"} - if utils.Contains(allowedSortFields, params.SortBy) { - sortBy = params.SortBy - } - } - if params.SortOrder == "asc" { - sortOrder = "asc" - } - - // 计算总数 - var total int64 - countQuery := query - if err := countQuery.Model(&entity.Tag{}).Count(&total).Error; err != nil { - s.logger.Error("Failed to count tags", zap.Error(err)) - return nil, err - } - - // 分页查询 - offset := (params.Page - 1) * params.Limit - var tags []entity.Tag - if err := query. - Order(fmt.Sprintf("%s %s", sortBy, sortOrder)). - Offset(offset). - Limit(params.Limit). - Find(&tags).Error; err != nil { - s.logger.Error("Failed to get tags", zap.Error(err)) - return nil, err - } - - // 计算总页数 - pages := int((total + int64(params.Limit) - 1) / int64(params.Limit)) - - return &TagListResponse{ - Tags: tags, - Total: total, - Page: params.Page, - Limit: params.Limit, - Pages: pages, - }, nil -} - -// GetAllTags 获取所有活跃标签 -func (s *TagService) GetAllTags(ctx context.Context) ([]entity.Tag, error) { - var tags []entity.Tag - if err := s.db.WithContext(ctx). - Where("is_active = ?", true). - Order("name ASC"). - Find(&tags).Error; err != nil { - s.logger.Error("Failed to get all tags", zap.Error(err)) - return nil, err - } - - return tags, nil -} - -// GetTagByID 根据ID获取标签 -func (s *TagService) GetTagByID(ctx context.Context, id uint) (*entity.Tag, error) { - var tag entity.Tag - if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - s.logger.Error("Failed to get tag by ID", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - - return &tag, nil -} - -// GetTagBySlug 根据slug获取标签 -func (s *TagService) GetTagBySlug(ctx context.Context, slug string) (*entity.Tag, error) { - var tag entity.Tag - if err := s.db.WithContext(ctx).Where("slug = ?", slug).First(&tag).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - s.logger.Error("Failed to get tag by slug", zap.Error(err), zap.String("slug", slug)) - return nil, err - } - - return &tag, nil -} - -// CreateTag 创建标签 -func (s *TagService) CreateTag(ctx context.Context, req *entity.CreateTagRequest) (*entity.Tag, error) { - // 验证slug唯一性 - if err := s.validateSlugUnique(ctx, req.Slug, 0); err != nil { - return nil, err - } - - tag := &entity.Tag{ - Name: req.Name, - Slug: req.Slug, - Description: req.Description, - Color: req.Color, - IsActive: true, - } - - if err := s.db.WithContext(ctx).Create(tag).Error; err != nil { - s.logger.Error("Failed to create tag", zap.Error(err)) - return nil, err - } - - s.logger.Info("Tag created successfully", zap.Uint("id", tag.ID)) - return tag, nil -} - -// UpdateTag 更新标签 -func (s *TagService) UpdateTag(ctx context.Context, id uint, req *entity.UpdateTagRequest) (*entity.Tag, error) { - // 检查标签是否存在 - var tag entity.Tag - if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("tag not found") - } - return nil, err - } - - // 验证slug唯一性 - if req.Slug != nil && *req.Slug != tag.Slug { - if err := s.validateSlugUnique(ctx, *req.Slug, id); err != nil { - return nil, err - } - } - - // 构建更新数据 - updates := map[string]interface{}{} - if req.Name != nil { - updates["name"] = *req.Name - } - if req.Slug != nil { - updates["slug"] = *req.Slug - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Color != nil { - updates["color"] = *req.Color - } - if req.IsActive != nil { - updates["is_active"] = *req.IsActive - } - - if len(updates) > 0 { - if err := s.db.WithContext(ctx).Model(&tag).Updates(updates).Error; err != nil { - s.logger.Error("Failed to update tag", zap.Error(err)) - return nil, err - } - } - - s.logger.Info("Tag updated successfully", zap.Uint("id", id)) - return &tag, nil -} - -// DeleteTag 删除标签 -func (s *TagService) DeleteTag(ctx context.Context, id uint) error { - // 检查标签是否存在 - var tag entity.Tag - if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("tag not found") - } - return err - } - - // 检查是否有关联的照片 - var photoCount int64 - if err := s.db.WithContext(ctx).Table("photo_tags"). - Where("tag_id = ?", id).Count(&photoCount).Error; err != nil { - return err - } - - if photoCount > 0 { - return errors.New("cannot delete tag with associated photos") - } - - // 删除标签 - if err := s.db.WithContext(ctx).Delete(&tag).Error; err != nil { - s.logger.Error("Failed to delete tag", zap.Error(err)) - return err - } - - s.logger.Info("Tag deleted successfully", zap.Uint("id", id)) - return nil -} - -// BatchDeleteTags 批量删除标签 -func (s *TagService) BatchDeleteTags(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return errors.New("no tags to delete") - } - - // 检查是否有关联的照片 - var photoCount int64 - if err := s.db.WithContext(ctx).Table("photo_tags"). - Where("tag_id IN ?", ids).Count(&photoCount).Error; err != nil { - return err - } - - if photoCount > 0 { - return errors.New("cannot delete tags with associated photos") - } - - // 删除标签 - if err := s.db.WithContext(ctx).Where("id IN ?", ids).Delete(&entity.Tag{}).Error; err != nil { - s.logger.Error("Failed to batch delete tags", zap.Error(err)) - return err - } - - s.logger.Info("Batch delete tags completed", zap.Int("count", len(ids))) - return nil -} - -// GetPopularTags 获取热门标签 -func (s *TagService) GetPopularTags(ctx context.Context, limit int) ([]entity.TagWithCount, error) { - if limit <= 0 { - limit = 10 - } - - var tags []entity.TagWithCount - if err := s.db.WithContext(ctx). - Table("tags"). - Select("tags.*, COUNT(photo_tags.photo_id) as photo_count"). - Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Where("tags.is_active = ?", true). - Group("tags.id"). - Order("photo_count DESC"). - Limit(limit). - Find(&tags).Error; err != nil { - s.logger.Error("Failed to get popular tags", zap.Error(err)) - return nil, err - } - - return tags, nil -} - -// GetTagCloud 获取标签云数据 -func (s *TagService) GetTagCloud(ctx context.Context) ([]entity.TagCloudItem, error) { - var items []entity.TagCloudItem - if err := s.db.WithContext(ctx). - Table("tags"). - Select("tags.name, tags.slug, tags.color, COUNT(photo_tags.photo_id) as count"). - Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Where("tags.is_active = ?", true). - Group("tags.id, tags.name, tags.slug, tags.color"). - Having("COUNT(photo_tags.photo_id) > 0"). - Order("count DESC"). - Find(&items).Error; err != nil { - s.logger.Error("Failed to get tag cloud", zap.Error(err)) - return nil, err - } - - return items, nil -} - -// GetTagStats 获取标签统计信息 -func (s *TagService) GetTagStats(ctx context.Context) (*entity.TagStats, error) { - var stats entity.TagStats - - // 总标签数 - if err := s.db.WithContext(ctx).Model(&entity.Tag{}).Count(&stats.Total).Error; err != nil { - return nil, err - } - - // 活跃标签数 - if err := s.db.WithContext(ctx).Model(&entity.Tag{}). - Where("is_active = ?", true).Count(&stats.Active).Error; err != nil { - return nil, err - } - - // 已使用标签数 - if err := s.db.WithContext(ctx). - Table("tags"). - Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id"). - Where("tags.is_active = ?", true). - Group("tags.id"). - Count(&stats.Used).Error; err != nil { - return nil, err - } - - // 未使用标签数 - stats.Unused = stats.Active - stats.Used - - // 平均每个标签的照片数 - var totalPhotos int64 - if err := s.db.WithContext(ctx).Table("photo_tags"). - Joins("JOIN tags ON photo_tags.tag_id = tags.id"). - Where("tags.is_active = ?", true). - Count(&totalPhotos).Error; err != nil { - return nil, err - } - - if stats.Used > 0 { - stats.AvgPhotosPerTag = float64(totalPhotos) / float64(stats.Used) - } - - return &stats, nil -} - -// SearchTags 搜索标签 -func (s *TagService) SearchTags(ctx context.Context, query string, limit int) ([]entity.Tag, error) { - if limit <= 0 { - limit = 10 - } - - var tags []entity.Tag - searchPattern := "%" + query + "%" - - if err := s.db.WithContext(ctx). - Where("is_active = ? AND (name ILIKE ? OR slug ILIKE ?)", true, searchPattern, searchPattern). - Order("name ASC"). - Limit(limit). - Find(&tags).Error; err != nil { - s.logger.Error("Failed to search tags", zap.Error(err)) - return nil, err - } - - return tags, nil -} - -// CreateTagsFromNames 从名称列表创建标签 -func (s *TagService) CreateTagsFromNames(ctx context.Context, names []string) ([]entity.Tag, error) { - var tags []entity.Tag - - for _, name := range names { - name = strings.TrimSpace(name) - if name == "" { - continue - } - - // 生成slug - slug, err := s.GenerateSlug(ctx, name) - if err != nil { - s.logger.Error("Failed to generate slug", zap.Error(err)) - continue - } - - // 检查标签是否已存在 - var existingTag entity.Tag - if err := s.db.WithContext(ctx).Where("slug = ?", slug).First(&existingTag).Error; err == nil { - tags = append(tags, existingTag) - continue - } - - // 创建新标签 - tag := entity.Tag{ - Name: name, - Slug: slug, - IsActive: true, - } - - if err := s.db.WithContext(ctx).Create(&tag).Error; err != nil { - s.logger.Error("Failed to create tag", zap.Error(err)) - continue - } - - tags = append(tags, tag) - } - - return tags, nil -} - -// validateSlugUnique 验证slug唯一性 -func (s *TagService) validateSlugUnique(ctx context.Context, slug string, excludeID uint) error { - var count int64 - query := s.db.WithContext(ctx).Model(&entity.Tag{}).Where("slug = ?", slug) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return err - } - - if count > 0 { - return errors.New("slug already exists") - } - - return nil -} - -// GenerateSlug 生成slug -func (s *TagService) GenerateSlug(ctx context.Context, name string) (string, error) { - baseSlug := utils.GenerateSlug(name) - slug := baseSlug - - counter := 1 - for { - var count int64 - if err := s.db.WithContext(ctx).Model(&entity.Tag{}). - Where("slug = ?", slug).Count(&count).Error; err != nil { - return "", err - } - - if count == 0 { - break - } - - slug = fmt.Sprintf("%s-%d", baseSlug, counter) - counter++ - } - - return slug, nil -} \ No newline at end of file diff --git a/backend-old/internal/service/upload/upload_service.go b/backend-old/internal/service/upload/upload_service.go deleted file mode 100644 index dd8501f..0000000 --- a/backend-old/internal/service/upload/upload_service.go +++ /dev/null @@ -1,187 +0,0 @@ -package upload - -import ( - "fmt" - "io" - "mime/multipart" - "os" - "path/filepath" - "strings" - "time" - - "photography-backend/internal/config" -) - -// UploadService 文件上传服务 -type UploadService struct { - config *config.Config - uploadDir string - baseURL string -} - -// UploadResult 上传结果 -type UploadResult struct { - Filename string `json:"filename"` - OriginalName string `json:"original_name"` - FilePath string `json:"file_path"` - FileURL string `json:"file_url"` - FileSize int64 `json:"file_size"` - MimeType string `json:"mime_type"` -} - -// NewUploadService 创建文件上传服务 -func NewUploadService(cfg *config.Config) *UploadService { - uploadDir := cfg.Storage.Local.BasePath - if uploadDir == "" { - uploadDir = "./uploads" - } - - baseURL := cfg.Storage.Local.BaseURL - if baseURL == "" { - baseURL = fmt.Sprintf("http://localhost:%d/uploads", cfg.App.Port) - } - - // 确保上传目录存在 - os.MkdirAll(uploadDir, 0755) - - // 创建子目录 - subdirs := []string{"photos", "thumbnails", "temp"} - for _, subdir := range subdirs { - os.MkdirAll(filepath.Join(uploadDir, subdir), 0755) - } - - return &UploadService{ - config: cfg, - uploadDir: uploadDir, - baseURL: baseURL, - } -} - -// UploadPhoto 上传照片 -func (s *UploadService) UploadPhoto(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) { - // 验证文件类型 - if !s.isValidImageType(header.Header.Get("Content-Type")) { - return nil, fmt.Errorf("不支持的文件类型: %s", header.Header.Get("Content-Type")) - } - - // 验证文件大小 - if header.Size > s.config.Upload.MaxFileSize { - return nil, fmt.Errorf("文件大小超过限制: %d bytes", header.Size) - } - - // 生成唯一文件名 - filename := s.generateUniqueFilename(header.Filename) - - // 保存文件到 photos 目录 - photoPath := filepath.Join(s.uploadDir, "photos", filename) - - // 创建目标文件 - dst, err := os.Create(photoPath) - if err != nil { - return nil, fmt.Errorf("创建文件失败: %w", err) - } - defer dst.Close() - - // 复制文件内容 - _, err = io.Copy(dst, file) - if err != nil { - return nil, fmt.Errorf("保存文件失败: %w", err) - } - - // 构造文件URL - fileURL := fmt.Sprintf("%s/photos/%s", s.baseURL, filename) - - return &UploadResult{ - Filename: filename, - OriginalName: header.Filename, - FilePath: photoPath, - FileURL: fileURL, - FileSize: header.Size, - MimeType: header.Header.Get("Content-Type"), - }, nil -} - -// DeletePhoto 删除照片 -func (s *UploadService) DeletePhoto(filename string) error { - // 删除原图 - photoPath := filepath.Join(s.uploadDir, "photos", filename) - if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("删除文件失败: %w", err) - } - - // 删除缩略图 - thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) - os.Remove(thumbnailPath) // 忽略错误,因为缩略图可能不存在 - - return nil -} - -// GetPhotoURL 获取照片URL -func (s *UploadService) GetPhotoURL(filename string) string { - return fmt.Sprintf("%s/photos/%s", s.baseURL, filename) -} - -// isValidImageType 验证是否为有效的图片类型 -func (s *UploadService) isValidImageType(mimeType string) bool { - allowedTypes := s.config.Upload.AllowedTypes - if len(allowedTypes) == 0 { - // 默认允许的图片类型 - allowedTypes = []string{ - "image/jpeg", - "image/jpg", - "image/png", - "image/gif", - "image/webp", - "image/tiff", - } - } - - for _, allowedType := range allowedTypes { - if mimeType == allowedType { - return true - } - } - return false -} - -// generateUniqueFilename 生成唯一文件名 -func (s *UploadService) generateUniqueFilename(originalName string) string { - ext := filepath.Ext(originalName) - timestamp := time.Now().Unix() - - // 清理原文件名 - baseName := strings.TrimSuffix(originalName, ext) - baseName = strings.ReplaceAll(baseName, " ", "_") - - return fmt.Sprintf("%s_%d%s", baseName, timestamp, ext) -} - -// GetUploadStats 获取上传统计信息 -func (s *UploadService) GetUploadStats() (map[string]interface{}, error) { - photosDir := filepath.Join(s.uploadDir, "photos") - - var totalFiles int - var totalSize int64 - - err := filepath.Walk(photosDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - totalFiles++ - totalSize += info.Size() - } - return nil - }) - - if err != nil { - return nil, err - } - - return map[string]interface{}{ - "total_files": totalFiles, - "total_size": totalSize, - "upload_dir": s.uploadDir, - "base_url": s.baseURL, - }, nil -} \ No newline at end of file diff --git a/backend-old/internal/service/user_service.go b/backend-old/internal/service/user_service.go deleted file mode 100644 index 9a009b4..0000000 --- a/backend-old/internal/service/user_service.go +++ /dev/null @@ -1,432 +0,0 @@ -package service - -import ( - "context" - "errors" - "time" - - "photography-backend/internal/model/entity" - - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" -) - -type UserService struct { - db *gorm.DB - logger *zap.Logger -} - -func NewUserService(db *gorm.DB, logger *zap.Logger) *UserService { - return &UserService{ - db: db, - logger: logger, - } -} - -// UserListParams 用户列表查询参数 -type UserListParams struct { - Page int `json:"page" form:"page"` - Limit int `json:"limit" form:"limit"` - Search string `json:"search" form:"search"` - Role string `json:"role" form:"role"` - IsActive *bool `json:"is_active" form:"is_active"` -} - -// UserListResponse 用户列表响应 -type UserListResponse struct { - Users []entity.User `json:"users"` - Total int64 `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` - Pages int `json:"pages"` -} - -// GetUsers 获取用户列表 -func (s *UserService) GetUsers(ctx context.Context, params UserListParams) (*UserListResponse, error) { - // 设置默认值 - if params.Page <= 0 { - params.Page = 1 - } - if params.Limit <= 0 { - params.Limit = 20 - } - if params.Limit > 100 { - params.Limit = 100 - } - - // 构建查询 - query := s.db.WithContext(ctx) - - // 搜索过滤 - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - query = query.Where("username ILIKE ? OR email ILIKE ?", searchPattern, searchPattern) - } - - // 角色过滤 - if params.Role != "" { - query = query.Where("role = ?", params.Role) - } - - // 状态过滤 - if params.IsActive != nil { - query = query.Where("is_active = ?", *params.IsActive) - } - - // 计算总数 - var total int64 - countQuery := query - if err := countQuery.Model(&entity.User{}).Count(&total).Error; err != nil { - s.logger.Error("Failed to count users", zap.Error(err)) - return nil, err - } - - // 分页查询 - offset := (params.Page - 1) * params.Limit - var users []entity.User - if err := query. - Order("created_at DESC"). - Offset(offset). - Limit(params.Limit). - Find(&users).Error; err != nil { - s.logger.Error("Failed to get users", zap.Error(err)) - return nil, err - } - - // 计算总页数 - pages := int((total + int64(params.Limit) - 1) / int64(params.Limit)) - - return &UserListResponse{ - Users: users, - Total: total, - Page: params.Page, - Limit: params.Limit, - Pages: pages, - }, nil -} - -// GetUserByID 根据ID获取用户 -func (s *UserService) GetUserByID(ctx context.Context, id uint) (*entity.User, error) { - var user entity.User - if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - s.logger.Error("Failed to get user by ID", zap.Error(err), zap.Uint("id", id)) - return nil, err - } - - return &user, nil -} - -// GetUserByUsername 根据用户名获取用户 -func (s *UserService) GetUserByUsername(ctx context.Context, username string) (*entity.User, error) { - var user entity.User - if err := s.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - s.logger.Error("Failed to get user by username", zap.Error(err), zap.String("username", username)) - return nil, err - } - - return &user, nil -} - -// GetUserByEmail 根据邮箱获取用户 -func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) { - var user entity.User - if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - s.logger.Error("Failed to get user by email", zap.Error(err), zap.String("email", email)) - return nil, err - } - - return &user, nil -} - -// CreateUser 创建用户 -func (s *UserService) CreateUser(ctx context.Context, req *entity.CreateUserRequest) (*entity.User, error) { - // 验证用户名唯一性 - var existingUser entity.User - if err := s.db.WithContext(ctx).Where("username = ?", req.Username).First(&existingUser).Error; err == nil { - return nil, errors.New("username already exists") - } - - // 验证邮箱唯一性 - if err := s.db.WithContext(ctx).Where("email = ?", req.Email).First(&existingUser).Error; err == nil { - return nil, errors.New("email already exists") - } - - // 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - s.logger.Error("Failed to hash password", zap.Error(err)) - return nil, err - } - - user := &entity.User{ - Username: req.Username, - Email: req.Email, - Password: string(hashedPassword), - Role: req.Role, - IsActive: true, - } - - if err := s.db.WithContext(ctx).Create(user).Error; err != nil { - s.logger.Error("Failed to create user", zap.Error(err)) - return nil, err - } - - s.logger.Info("User created successfully", zap.Uint("id", user.ID)) - return user, nil -} - -// UpdateUser 更新用户 -func (s *UserService) UpdateUser(ctx context.Context, id uint, req *entity.UpdateUserRequest) (*entity.User, error) { - // 检查用户是否存在 - var user entity.User - if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - - // 构建更新数据 - updates := map[string]interface{}{} - - if req.Username != nil { - // 验证用户名唯一性 - var existingUser entity.User - if err := s.db.WithContext(ctx).Where("username = ? AND id != ?", *req.Username, id).First(&existingUser).Error; err == nil { - return nil, errors.New("username already exists") - } - updates["username"] = *req.Username - } - - if req.Email != nil { - // 验证邮箱唯一性 - var existingUser entity.User - if err := s.db.WithContext(ctx).Where("email = ? AND id != ?", *req.Email, id).First(&existingUser).Error; err == nil { - return nil, errors.New("email already exists") - } - updates["email"] = *req.Email - } - - if req.Role != nil { - updates["role"] = *req.Role - } - - if req.IsActive != nil { - updates["is_active"] = *req.IsActive - } - - if len(updates) > 0 { - if err := s.db.WithContext(ctx).Model(&user).Updates(updates).Error; err != nil { - s.logger.Error("Failed to update user", zap.Error(err)) - return nil, err - } - } - - s.logger.Info("User updated successfully", zap.Uint("id", id)) - return &user, nil -} - -// UpdateCurrentUser 更新当前用户信息 -func (s *UserService) UpdateCurrentUser(ctx context.Context, id uint, req *entity.UpdateCurrentUserRequest) (*entity.User, error) { - // 检查用户是否存在 - var user entity.User - if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("user not found") - } - return nil, err - } - - // 构建更新数据 - updates := map[string]interface{}{} - - if req.Username != nil { - // 验证用户名唯一性 - var existingUser entity.User - if err := s.db.WithContext(ctx).Where("username = ? AND id != ?", *req.Username, id).First(&existingUser).Error; err == nil { - return nil, errors.New("username already exists") - } - updates["username"] = *req.Username - } - - if req.Email != nil { - // 验证邮箱唯一性 - var existingUser entity.User - if err := s.db.WithContext(ctx).Where("email = ? AND id != ?", *req.Email, id).First(&existingUser).Error; err == nil { - return nil, errors.New("email already exists") - } - updates["email"] = *req.Email - } - - if len(updates) > 0 { - if err := s.db.WithContext(ctx).Model(&user).Updates(updates).Error; err != nil { - s.logger.Error("Failed to update current user", zap.Error(err)) - return nil, err - } - } - - s.logger.Info("Current user updated successfully", zap.Uint("id", id)) - return &user, nil -} - -// DeleteUser 删除用户 -func (s *UserService) DeleteUser(ctx context.Context, id uint) error { - // 检查用户是否存在 - var user entity.User - if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("user not found") - } - return err - } - - // 删除用户 - if err := s.db.WithContext(ctx).Delete(&user).Error; err != nil { - s.logger.Error("Failed to delete user", zap.Error(err)) - return err - } - - s.logger.Info("User deleted successfully", zap.Uint("id", id)) - return nil -} - -// ChangePassword 修改密码 -func (s *UserService) ChangePassword(ctx context.Context, id uint, req *entity.ChangePasswordRequest) error { - // 检查用户是否存在 - var user entity.User - if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("user not found") - } - return err - } - - // 验证旧密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil { - return errors.New("old password is incorrect") - } - - // 加密新密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) - if err != nil { - s.logger.Error("Failed to hash new password", zap.Error(err)) - return err - } - - // 更新密码 - if err := s.db.WithContext(ctx).Model(&user).Update("password", string(hashedPassword)).Error; err != nil { - s.logger.Error("Failed to update password", zap.Error(err)) - return err - } - - s.logger.Info("Password changed successfully", zap.Uint("id", id)) - return nil -} - -// ValidateCredentials 验证用户凭据 -func (s *UserService) ValidateCredentials(ctx context.Context, username, password string) (*entity.User, error) { - var user entity.User - - // 根据用户名或邮箱查找用户 - if err := s.db.WithContext(ctx).Where("username = ? OR email = ?", username, username).First(&user).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("invalid credentials") - } - s.logger.Error("Failed to find user", zap.Error(err)) - return nil, err - } - - // 检查用户是否激活 - if !user.IsActive { - return nil, errors.New("user account is disabled") - } - - // 验证密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return nil, errors.New("invalid credentials") - } - - return &user, nil -} - -// GetUserStats 获取用户统计信息 -func (s *UserService) GetUserStats(ctx context.Context) (*entity.UserStats, error) { - var stats entity.UserStats - - // 总用户数 - if err := s.db.WithContext(ctx).Model(&entity.User{}).Count(&stats.Total).Error; err != nil { - return nil, err - } - - // 活跃用户数 - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Where("is_active = ?", true).Count(&stats.Active).Error; err != nil { - return nil, err - } - - // 按角色统计 - var roleStats []struct { - Role string `json:"role"` - Count int64 `json:"count"` - } - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Select("role, COUNT(*) as count"). - Where("is_active = ?", true). - Group("role"). - Find(&roleStats).Error; err != nil { - return nil, err - } - - stats.RoleStats = make(map[string]int64) - for _, stat := range roleStats { - stats.RoleStats[stat.Role] = stat.Count - } - - // 本月新增用户 - startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1) - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Where("created_at >= ?", startOfMonth). - Count(&stats.ThisMonth).Error; err != nil { - return nil, err - } - - // 今日新增用户 - startOfDay := time.Now().Truncate(24 * time.Hour) - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Where("created_at >= ?", startOfDay). - Count(&stats.Today).Error; err != nil { - return nil, err - } - - return &stats, nil -} - -// IsUsernameAvailable 检查用户名是否可用 -func (s *UserService) IsUsernameAvailable(ctx context.Context, username string) (bool, error) { - var count int64 - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Where("username = ?", username).Count(&count).Error; err != nil { - return false, err - } - return count == 0, nil -} - -// IsEmailAvailable 检查邮箱是否可用 -func (s *UserService) IsEmailAvailable(ctx context.Context, email string) (bool, error) { - var count int64 - if err := s.db.WithContext(ctx).Model(&entity.User{}). - Where("email = ?", email).Count(&count).Error; err != nil { - return false, err - } - return count == 0, nil -} \ No newline at end of file diff --git a/backend-old/internal/utils/file.go b/backend-old/internal/utils/file.go deleted file mode 100644 index 594be2f..0000000 --- a/backend-old/internal/utils/file.go +++ /dev/null @@ -1,172 +0,0 @@ -package utils - -import ( - "crypto/md5" - "crypto/sha256" - "fmt" - "io" - "mime" - "os" - "path/filepath" - "strings" - "time" -) - -// GetFileExtension 获取文件扩展名 -func GetFileExtension(filename string) string { - return strings.ToLower(filepath.Ext(filename)) -} - -// GetMimeType 根据文件扩展名获取MIME类型 -func GetMimeType(filename string) string { - ext := GetFileExtension(filename) - mimeType := mime.TypeByExtension(ext) - if mimeType == "" { - return "application/octet-stream" - } - return mimeType -} - -// IsImageFile 检查是否为图片文件 -func IsImageFile(filename string) bool { - imageExtensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"} - ext := GetFileExtension(filename) - - for _, imageExt := range imageExtensions { - if ext == imageExt { - return true - } - } - - return false -} - -// GenerateUniqueFilename 生成唯一的文件名 -func GenerateUniqueFilename(originalFilename string) string { - ext := GetFileExtension(originalFilename) - timestamp := time.Now().Unix() - randomStr := GenerateRandomString(8) - - return fmt.Sprintf("%d_%s%s", timestamp, randomStr, ext) -} - -// GenerateFilePath 生成文件路径 -func GenerateFilePath(baseDir, subDir, filename string) string { - // 按日期组织文件夹 - now := time.Now() - dateDir := now.Format("2006/01/02") - - if subDir != "" { - return filepath.Join(baseDir, subDir, dateDir, filename) - } - - return filepath.Join(baseDir, dateDir, filename) -} - -// EnsureDir 确保目录存在 -func EnsureDir(dirPath string) error { - return os.MkdirAll(dirPath, 0755) -} - -// FileExists 检查文件是否存在 -func FileExists(filepath string) bool { - _, err := os.Stat(filepath) - return !os.IsNotExist(err) -} - -// GetFileSize 获取文件大小 -func GetFileSize(filepath string) (int64, error) { - info, err := os.Stat(filepath) - if err != nil { - return 0, err - } - return info.Size(), nil -} - -// CalculateFileMD5 计算文件MD5哈希 -func CalculateFileMD5(filepath string) (string, error) { - file, err := os.Open(filepath) - if err != nil { - return "", err - } - defer file.Close() - - hash := md5.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - - return fmt.Sprintf("%x", hash.Sum(nil)), nil -} - -// CalculateFileSHA256 计算文件SHA256哈希 -func CalculateFileSHA256(filepath string) (string, error) { - file, err := os.Open(filepath) - if err != nil { - return "", err - } - defer file.Close() - - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - - return fmt.Sprintf("%x", hash.Sum(nil)), nil -} - -// CopyFile 复制文件 -func CopyFile(src, dst string) error { - sourceFile, err := os.Open(src) - if err != nil { - return err - } - defer sourceFile.Close() - - // 确保目标目录存在 - if err := EnsureDir(filepath.Dir(dst)); err != nil { - return err - } - - destFile, err := os.Create(dst) - if err != nil { - return err - } - defer destFile.Close() - - _, err = io.Copy(destFile, sourceFile) - return err -} - -// DeleteFile 删除文件 -func DeleteFile(filepath string) error { - if !FileExists(filepath) { - return nil // 文件不存在,认为删除成功 - } - return os.Remove(filepath) -} - -// FormatFileSize 格式化文件大小为人类可读格式 -func FormatFileSize(bytes int64) string { - const unit = 1024 - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - - div, exp := int64(unit), 0 - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - - units := []string{"KB", "MB", "GB", "TB", "PB"} - return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp]) -} - -// GetImageDimensions 获取图片尺寸(需要额外的图片处理库) -// 这里只是占位符,实际实现需要使用如 github.com/disintegration/imaging 等库 -func GetImageDimensions(filepath string) (width, height int, err error) { - // TODO: 实现图片尺寸获取 - // 需要添加图片处理依赖 - return 0, 0, fmt.Errorf("not implemented") -} \ No newline at end of file diff --git a/backend-old/internal/utils/random.go b/backend-old/internal/utils/random.go deleted file mode 100644 index 9941833..0000000 --- a/backend-old/internal/utils/random.go +++ /dev/null @@ -1,77 +0,0 @@ -package utils - -import ( - "crypto/rand" - "encoding/base64" - "math/big" - "time" -) - -const ( - // 字符集 - alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - numbers = "0123456789" -) - -// GenerateRandomString 生成指定长度的随机字符串 -func GenerateRandomString(length int) string { - return generateRandomFromCharset(length, alphanumeric) -} - -// GenerateRandomLetters 生成指定长度的随机字母字符串 -func GenerateRandomLetters(length int) string { - return generateRandomFromCharset(length, letters) -} - -// GenerateRandomNumbers 生成指定长度的随机数字字符串 -func GenerateRandomNumbers(length int) string { - return generateRandomFromCharset(length, numbers) -} - -// generateRandomFromCharset 从指定字符集生成随机字符串 -func generateRandomFromCharset(length int, charset string) string { - result := make([]byte, length) - charsetLen := big.NewInt(int64(len(charset))) - - for i := 0; i < length; i++ { - randomIndex, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - // 如果加密随机数生成失败,回退到时间种子 - return generateRandomFallback(length, charset) - } - result[i] = charset[randomIndex.Int64()] - } - - return string(result) -} - -// generateRandomFallback 回退的随机生成方法 -func generateRandomFallback(length int, charset string) string { - // 使用时间作为种子的简单随机生成 - seed := time.Now().UnixNano() - result := make([]byte, length) - - for i := 0; i < length; i++ { - seed = seed*1103515245 + 12345 - result[i] = charset[(seed/65536)%int64(len(charset))] - } - - return string(result) -} - -// GenerateSecureToken 生成安全令牌 -func GenerateSecureToken(length int) (string, error) { - bytes := make([]byte, length) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(bytes), nil -} - -// GenerateID 生成唯一ID -func GenerateID() string { - timestamp := time.Now().UnixNano() - random := GenerateRandomString(8) - return base64.URLEncoding.EncodeToString([]byte(string(timestamp) + random))[:16] -} \ No newline at end of file diff --git a/backend-old/internal/utils/slug.go b/backend-old/internal/utils/slug.go deleted file mode 100644 index 1c02990..0000000 --- a/backend-old/internal/utils/slug.go +++ /dev/null @@ -1,68 +0,0 @@ -package utils - -import ( - "regexp" - "strconv" - "strings" - "unicode" -) - -// GenerateSlug 生成URL友好的slug -func GenerateSlug(text string) string { - // 转换为小写 - text = strings.ToLower(text) - - // 移除重音字符 - text = removeAccents(text) - - // 替换空格和特殊字符为连字符 - reg := regexp.MustCompile(`[^\p{L}\p{N}]+`) - text = reg.ReplaceAllString(text, "-") - - // 移除首尾的连字符 - text = strings.Trim(text, "-") - - // 移除连续的连字符 - reg = regexp.MustCompile(`-+`) - text = reg.ReplaceAllString(text, "-") - - return text -} - -// removeAccents 移除重音字符的转换函数 -func removeAccents(text string) string { - var result strings.Builder - for _, r := range text { - if !unicode.Is(unicode.Mn, r) { - result.WriteRune(r) - } - } - return result.String() -} - -// TruncateString 截断字符串到指定长度 -func TruncateString(s string, length int) string { - if len(s) <= length { - return s - } - return s[:length] -} - -// GenerateUniqueSlug 生成唯一的slug -func GenerateUniqueSlug(base string, existingCheck func(string) bool) string { - slug := GenerateSlug(base) - if !existingCheck(slug) { - return slug - } - - // 如果存在重复,添加数字后缀 - for i := 1; i <= 1000; i++ { - candidateSlug := slug + "-" + strconv.Itoa(i) - if !existingCheck(candidateSlug) { - return candidateSlug - } - } - - // 如果还是重复,使用时间戳 - return slug + "-" + GenerateRandomString(6) -} \ No newline at end of file diff --git a/backend-old/internal/utils/time.go b/backend-old/internal/utils/time.go deleted file mode 100644 index 2f65349..0000000 --- a/backend-old/internal/utils/time.go +++ /dev/null @@ -1,153 +0,0 @@ -package utils - -import ( - "fmt" - "time" -) - -// FormatTime 格式化时间为字符串 -func FormatTime(t time.Time, layout string) string { - if layout == "" { - layout = "2006-01-02 15:04:05" - } - return t.Format(layout) -} - -// ParseTime 解析时间字符串 -func ParseTime(timeStr, layout string) (time.Time, error) { - if layout == "" { - layout = "2006-01-02 15:04:05" - } - return time.Parse(layout, timeStr) -} - -// GetTimeAgo 获取相对时间描述 -func GetTimeAgo(t time.Time) string { - now := time.Now() - diff := now.Sub(t) - - if diff < time.Minute { - return "刚刚" - } - - if diff < time.Hour { - minutes := int(diff.Minutes()) - return fmt.Sprintf("%d分钟前", minutes) - } - - if diff < 24*time.Hour { - hours := int(diff.Hours()) - return fmt.Sprintf("%d小时前", hours) - } - - if diff < 30*24*time.Hour { - days := int(diff.Hours() / 24) - return fmt.Sprintf("%d天前", days) - } - - if diff < 365*24*time.Hour { - months := int(diff.Hours() / (24 * 30)) - return fmt.Sprintf("%d个月前", months) - } - - years := int(diff.Hours() / (24 * 365)) - return fmt.Sprintf("%d年前", years) -} - -// IsToday 检查时间是否为今天 -func IsToday(t time.Time) bool { - now := time.Now() - return t.Year() == now.Year() && t.YearDay() == now.YearDay() -} - -// IsThisWeek 检查时间是否为本周 -func IsThisWeek(t time.Time) bool { - now := time.Now() - year, week := now.ISOWeek() - tYear, tWeek := t.ISOWeek() - return year == tYear && week == tWeek -} - -// IsThisMonth 检查时间是否为本月 -func IsThisMonth(t time.Time) bool { - now := time.Now() - return t.Year() == now.Year() && t.Month() == now.Month() -} - -// IsThisYear 检查时间是否为今年 -func IsThisYear(t time.Time) bool { - now := time.Now() - return t.Year() == now.Year() -} - -// GetWeekRange 获取本周的开始和结束时间 -func GetWeekRange(t time.Time) (start, end time.Time) { - // 获取周一作为周开始 - weekday := int(t.Weekday()) - if weekday == 0 { - weekday = 7 // 周日为7 - } - - start = t.AddDate(0, 0, -(weekday-1)) - start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location()) - - end = start.AddDate(0, 0, 6) - end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, end.Location()) - - return start, end -} - -// GetMonthRange 获取本月的开始和结束时间 -func GetMonthRange(t time.Time) (start, end time.Time) { - start = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) - end = start.AddDate(0, 1, -1) - end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, end.Location()) - - return start, end -} - -// GetYearRange 获取本年的开始和结束时间 -func GetYearRange(t time.Time) (start, end time.Time) { - start = time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location()) - end = time.Date(t.Year(), 12, 31, 23, 59, 59, 999999999, t.Location()) - - return start, end -} - -// Timestamp 获取当前时间戳(秒) -func Timestamp() int64 { - return time.Now().Unix() -} - -// TimestampMilli 获取当前时间戳(毫秒) -func TimestampMilli() int64 { - return time.Now().UnixNano() / 1e6 -} - -// FromTimestamp 从时间戳创建时间对象 -func FromTimestamp(timestamp int64) time.Time { - return time.Unix(timestamp, 0) -} - -// FromTimestampMilli 从毫秒时间戳创建时间对象 -func FromTimestampMilli(timestamp int64) time.Time { - return time.Unix(0, timestamp*1e6) -} - -// FormatDuration 格式化持续时间 -func FormatDuration(d time.Duration) string { - if d < time.Minute { - return fmt.Sprintf("%.0f秒", d.Seconds()) - } - - if d < time.Hour { - return fmt.Sprintf("%.0f分钟", d.Minutes()) - } - - if d < 24*time.Hour { - return fmt.Sprintf("%.1f小时", d.Hours()) - } - - days := d.Hours() / 24 - return fmt.Sprintf("%.1f天", days) -} \ No newline at end of file diff --git a/backend-old/internal/utils/validation.go b/backend-old/internal/utils/validation.go deleted file mode 100644 index a8ddf36..0000000 --- a/backend-old/internal/utils/validation.go +++ /dev/null @@ -1,128 +0,0 @@ -package utils - -import ( - "regexp" - "strings" - "unicode" -) - -// IsValidEmail 验证邮箱格式 -func IsValidEmail(email string) bool { - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return emailRegex.MatchString(email) -} - -// IsValidUsername 验证用户名格式 -func IsValidUsername(username string) bool { - // 用户名长度3-20,只能包含字母、数字、下划线 - if len(username) < 3 || len(username) > 20 { - return false - } - - usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) - return usernameRegex.MatchString(username) -} - -// IsValidPassword 验证密码强度 -func IsValidPassword(password string) bool { - // 密码长度至少6位 - if len(password) < 6 { - return false - } - - // 检查是否包含字母和数字 - hasLetter := false - hasNumber := false - - for _, char := range password { - if unicode.IsLetter(char) { - hasLetter = true - } - if unicode.IsNumber(char) { - hasNumber = true - } - } - - return hasLetter && hasNumber -} - -// IsValidSlug 验证slug格式 -func IsValidSlug(slug string) bool { - // slug只能包含小写字母、数字和连字符 - if len(slug) == 0 || len(slug) > 100 { - return false - } - - slugRegex := regexp.MustCompile(`^[a-z0-9-]+$`) - return slugRegex.MatchString(slug) && !strings.HasPrefix(slug, "-") && !strings.HasSuffix(slug, "-") -} - -// IsValidHexColor 验证十六进制颜色代码 -func IsValidHexColor(color string) bool { - colorRegex := regexp.MustCompile(`^#[a-fA-F0-9]{6}$`) - return colorRegex.MatchString(color) -} - -// IsValidURL 验证URL格式 -func IsValidURL(url string) bool { - urlRegex := regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`) - return urlRegex.MatchString(url) -} - -// SanitizeString 清理字符串,移除HTML标签和特殊字符 -func SanitizeString(input string) string { - // 移除HTML标签 - htmlRegex := regexp.MustCompile(`<[^>]*>`) - cleaned := htmlRegex.ReplaceAllString(input, "") - - // 移除多余的空白字符 - whitespaceRegex := regexp.MustCompile(`\s+`) - cleaned = whitespaceRegex.ReplaceAllString(cleaned, " ") - - return strings.TrimSpace(cleaned) -} - -// ValidateImageFormat 验证图片格式 -func ValidateImageFormat(filename string) bool { - allowedExtensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} - lowerFilename := strings.ToLower(filename) - - for _, ext := range allowedExtensions { - if strings.HasSuffix(lowerFilename, ext) { - return true - } - } - - return false -} - -// ValidateFileSize 验证文件大小(字节) -func ValidateFileSize(size int64, maxSizeMB int64) bool { - maxSizeBytes := maxSizeMB * 1024 * 1024 - return size <= maxSizeBytes && size > 0 -} - -// NormalizeString 标准化字符串(去空格、转小写) -func NormalizeString(s string) string { - return strings.ToLower(strings.TrimSpace(s)) -} - -// ContainsOnlyASCII 检查字符串是否只包含ASCII字符 -func ContainsOnlyASCII(s string) bool { - for _, char := range s { - if char > 127 { - return false - } - } - return true -} - -// Contains 检查切片是否包含指定元素 -func Contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} \ No newline at end of file diff --git a/backend-old/migrations/001_create_users.sql b/backend-old/migrations/001_create_users.sql deleted file mode 100644 index 974ee78..0000000 --- a/backend-old/migrations/001_create_users.sql +++ /dev/null @@ -1,54 +0,0 @@ --- +migrate Up - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - email VARCHAR(100) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - name VARCHAR(100), - avatar VARCHAR(500), - bio TEXT, - website VARCHAR(200), - location VARCHAR(100), - role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin', 'photographer')), - is_active BOOLEAN DEFAULT true, - is_public BOOLEAN DEFAULT true, - email_verified BOOLEAN DEFAULT false, - last_login TIMESTAMP, - login_count INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 创建索引 -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_is_public ON users(is_public); -CREATE INDEX idx_users_email_verified ON users(email_verified); -CREATE INDEX idx_users_created_at ON users(created_at); -CREATE INDEX idx_users_deleted_at ON users(deleted_at) WHERE deleted_at IS NOT NULL; - --- 添加更新时间触发器 -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_users_updated_at BEFORE UPDATE ON users -FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- 插入默认管理员用户 (密码: admin123) -INSERT INTO users (username, email, password, name, role, email_verified) VALUES -('admin', 'admin@photography.com', '$2a$10$D4Zz6m3j1YJzp8Y7zW4l2OXcQ5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0', '管理员', 'admin', true); - --- +migrate Down - -DROP TRIGGER IF EXISTS update_users_updated_at ON users; -DROP FUNCTION IF EXISTS update_updated_at_column(); -DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/backend-old/migrations/002_create_categories.sql b/backend-old/migrations/002_create_categories.sql deleted file mode 100644 index c674c98..0000000 --- a/backend-old/migrations/002_create_categories.sql +++ /dev/null @@ -1,33 +0,0 @@ --- +migrate Up - -CREATE TABLE categories ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - description TEXT, - parent_id INTEGER REFERENCES categories(id), - color VARCHAR(7) DEFAULT '#3b82f6', - cover_image VARCHAR(500), - sort INTEGER DEFAULT 0, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_categories_parent_id ON categories(parent_id); -CREATE INDEX idx_categories_is_active ON categories(is_active); -CREATE INDEX idx_categories_sort ON categories(sort); -CREATE INDEX idx_categories_deleted_at ON categories(deleted_at); - --- 插入默认分类 -INSERT INTO categories (name, description, color, sort) VALUES -('风景摄影', '自然风景摄影作品', '#10b981', 1), -('人像摄影', '人物肖像摄影作品', '#f59e0b', 2), -('街头摄影', '街头纪实摄影作品', '#ef4444', 3), -('建筑摄影', '建筑和城市摄影作品', '#3b82f6', 4), -('抽象摄影', '抽象艺术摄影作品', '#8b5cf6', 5); - --- +migrate Down - -DROP TABLE IF EXISTS categories; \ No newline at end of file diff --git a/backend-old/migrations/003_create_tags.sql b/backend-old/migrations/003_create_tags.sql deleted file mode 100644 index 35e664f..0000000 --- a/backend-old/migrations/003_create_tags.sql +++ /dev/null @@ -1,35 +0,0 @@ --- +migrate Up - -CREATE TABLE tags ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) UNIQUE NOT NULL, - color VARCHAR(7) DEFAULT '#6b7280', - use_count INTEGER DEFAULT 0, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_tags_name ON tags(name); -CREATE INDEX idx_tags_use_count ON tags(use_count); -CREATE INDEX idx_tags_is_active ON tags(is_active); -CREATE INDEX idx_tags_deleted_at ON tags(deleted_at); - --- 插入默认标签 -INSERT INTO tags (name, color) VALUES -('自然', '#10b981'), -('人物', '#f59e0b'), -('城市', '#3b82f6'), -('夜景', '#1f2937'), -('黑白', '#6b7280'), -('色彩', '#ec4899'), -('构图', '#8b5cf6'), -('光影', '#f97316'), -('街头', '#ef4444'), -('建筑', '#0891b2'); - --- +migrate Down - -DROP TABLE IF EXISTS tags; \ No newline at end of file diff --git a/backend-old/migrations/004_create_photos.sql b/backend-old/migrations/004_create_photos.sql deleted file mode 100644 index 8686a7c..0000000 --- a/backend-old/migrations/004_create_photos.sql +++ /dev/null @@ -1,64 +0,0 @@ --- +migrate Up - -CREATE TABLE photos ( - id SERIAL PRIMARY KEY, - title VARCHAR(200) NOT NULL, - description TEXT, - filename VARCHAR(255) NOT NULL, - original_url VARCHAR(500) NOT NULL, - thumbnail_url VARCHAR(500), - medium_url VARCHAR(500), - large_url VARCHAR(500), - file_size BIGINT, - mime_type VARCHAR(100), - width INTEGER, - height INTEGER, - camera_make VARCHAR(100), - camera_model VARCHAR(100), - lens_model VARCHAR(100), - focal_length DECIMAL(5,2), - aperture DECIMAL(3,1), - shutter_speed VARCHAR(20), - iso INTEGER, - taken_at TIMESTAMP, - location_name VARCHAR(200), - latitude DECIMAL(10,8), - longitude DECIMAL(11,8), - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - album_id INTEGER, - category_id INTEGER, - is_public BOOLEAN DEFAULT true, - is_featured BOOLEAN DEFAULT false, - view_count INTEGER DEFAULT 0, - like_count INTEGER DEFAULT 0, - download_count INTEGER DEFAULT 0, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_photos_user_id ON photos(user_id); -CREATE INDEX idx_photos_album_id ON photos(album_id); -CREATE INDEX idx_photos_category_id ON photos(category_id); -CREATE INDEX idx_photos_is_public ON photos(is_public); -CREATE INDEX idx_photos_is_featured ON photos(is_featured); -CREATE INDEX idx_photos_taken_at ON photos(taken_at); -CREATE INDEX idx_photos_created_at ON photos(created_at); -CREATE INDEX idx_photos_view_count ON photos(view_count); -CREATE INDEX idx_photos_like_count ON photos(like_count); -CREATE INDEX idx_photos_sort_order ON photos(sort_order); -CREATE INDEX idx_photos_deleted_at ON photos(deleted_at) WHERE deleted_at IS NOT NULL; - --- 地理位置索引 -CREATE INDEX idx_photos_location ON photos(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; - --- 添加更新时间触发器 -CREATE TRIGGER update_photos_updated_at BEFORE UPDATE ON photos -FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- +migrate Down - -DROP TRIGGER IF EXISTS update_photos_updated_at ON photos; -DROP TABLE IF EXISTS photos; \ No newline at end of file diff --git a/backend-old/migrations/005_create_albums.sql b/backend-old/migrations/005_create_albums.sql deleted file mode 100644 index b1fe350..0000000 --- a/backend-old/migrations/005_create_albums.sql +++ /dev/null @@ -1,73 +0,0 @@ --- +migrate Up - -CREATE TABLE albums ( - id SERIAL PRIMARY KEY, - title VARCHAR(200) NOT NULL, - description TEXT, - slug VARCHAR(255) UNIQUE, - cover_photo_id INTEGER REFERENCES photos(id) ON DELETE SET NULL, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL, - is_public BOOLEAN DEFAULT true, - is_featured BOOLEAN DEFAULT false, - password VARCHAR(255), - view_count INTEGER DEFAULT 0, - like_count INTEGER DEFAULT 0, - photo_count INTEGER DEFAULT 0, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 创建索引 -CREATE INDEX idx_albums_user_id ON albums(user_id); -CREATE INDEX idx_albums_category_id ON albums(category_id); -CREATE INDEX idx_albums_cover_photo_id ON albums(cover_photo_id); -CREATE INDEX idx_albums_slug ON albums(slug); -CREATE INDEX idx_albums_is_public ON albums(is_public); -CREATE INDEX idx_albums_is_featured ON albums(is_featured); -CREATE INDEX idx_albums_created_at ON albums(created_at); -CREATE INDEX idx_albums_view_count ON albums(view_count); -CREATE INDEX idx_albums_like_count ON albums(like_count); -CREATE INDEX idx_albums_photo_count ON albums(photo_count); -CREATE INDEX idx_albums_sort_order ON albums(sort_order); -CREATE INDEX idx_albums_deleted_at ON albums(deleted_at) WHERE deleted_at IS NOT NULL; - --- 添加更新时间触发器 -CREATE TRIGGER update_albums_updated_at BEFORE UPDATE ON albums -FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- 添加 slug 自动生成触发器 -CREATE OR REPLACE FUNCTION generate_album_slug() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.slug IS NULL OR NEW.slug = '' THEN - NEW.slug = lower(regexp_replace(NEW.title, '[^a-zA-Z0-9]+', '-', 'g')); - NEW.slug = trim(both '-' from NEW.slug); - - -- 确保 slug 唯一 - DECLARE - counter INTEGER := 0; - base_slug VARCHAR(255); - BEGIN - base_slug := NEW.slug; - WHILE EXISTS (SELECT 1 FROM albums WHERE slug = NEW.slug AND id != COALESCE(NEW.id, 0)) LOOP - counter := counter + 1; - NEW.slug := base_slug || '-' || counter; - END LOOP; - END; - END IF; - RETURN NEW; -END; -$$ language 'plpgsql'; - -CREATE TRIGGER generate_albums_slug BEFORE INSERT OR UPDATE ON albums -FOR EACH ROW EXECUTE FUNCTION generate_album_slug(); - --- +migrate Down - -DROP TRIGGER IF EXISTS generate_albums_slug ON albums; -DROP FUNCTION IF EXISTS generate_album_slug(); -DROP TRIGGER IF EXISTS update_albums_updated_at ON albums; -DROP TABLE IF EXISTS albums; \ No newline at end of file diff --git a/backend-old/migrations/006_add_foreign_keys.sql b/backend-old/migrations/006_add_foreign_keys.sql deleted file mode 100644 index f911af8..0000000 --- a/backend-old/migrations/006_add_foreign_keys.sql +++ /dev/null @@ -1,111 +0,0 @@ --- +migrate Up - --- 添加照片表的外键约束 -ALTER TABLE photos -ADD CONSTRAINT fk_photos_album_id -FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE SET NULL; - -ALTER TABLE photos -ADD CONSTRAINT fk_photos_category_id -FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL; - --- 添加相册表的外键约束(如果还没有的话) --- 这些约束在创建相册表时可能已经存在,这里做一个保险 - --- 添加一些有用的触发器和函数 - --- 创建照片计数更新触发器(用于相册和分类) -CREATE OR REPLACE FUNCTION update_photo_counts() -RETURNS TRIGGER AS $$ -BEGIN - IF TG_OP = 'INSERT' THEN - -- 更新相册照片数量 - IF NEW.album_id IS NOT NULL THEN - UPDATE albums SET photo_count = photo_count + 1 WHERE id = NEW.album_id; - END IF; - -- 更新分类照片数量 - IF NEW.category_id IS NOT NULL THEN - UPDATE categories SET photo_count = photo_count + 1 WHERE id = NEW.category_id; - END IF; - RETURN NEW; - ELSIF TG_OP = 'DELETE' THEN - -- 更新相册照片数量 - IF OLD.album_id IS NOT NULL THEN - UPDATE albums SET photo_count = photo_count - 1 WHERE id = OLD.album_id; - END IF; - -- 更新分类照片数量 - IF OLD.category_id IS NOT NULL THEN - UPDATE categories SET photo_count = photo_count - 1 WHERE id = OLD.category_id; - END IF; - RETURN OLD; - ELSIF TG_OP = 'UPDATE' THEN - -- 处理相册变更 - IF OLD.album_id IS DISTINCT FROM NEW.album_id THEN - IF OLD.album_id IS NOT NULL THEN - UPDATE albums SET photo_count = photo_count - 1 WHERE id = OLD.album_id; - END IF; - IF NEW.album_id IS NOT NULL THEN - UPDATE albums SET photo_count = photo_count + 1 WHERE id = NEW.album_id; - END IF; - END IF; - -- 处理分类变更 - IF OLD.category_id IS DISTINCT FROM NEW.category_id THEN - IF OLD.category_id IS NOT NULL THEN - UPDATE categories SET photo_count = photo_count - 1 WHERE id = OLD.category_id; - END IF; - IF NEW.category_id IS NOT NULL THEN - UPDATE categories SET photo_count = photo_count + 1 WHERE id = NEW.category_id; - END IF; - END IF; - RETURN NEW; - END IF; - RETURN NULL; -END; -$$ language 'plpgsql'; - --- 创建触发器 -CREATE TRIGGER trigger_update_photo_counts -AFTER INSERT OR UPDATE OR DELETE ON photos -FOR EACH ROW EXECUTE FUNCTION update_photo_counts(); - --- 创建相册计数更新触发器(用于分类) -CREATE OR REPLACE FUNCTION update_album_counts() -RETURNS TRIGGER AS $$ -BEGIN - IF TG_OP = 'INSERT' THEN - IF NEW.category_id IS NOT NULL THEN - UPDATE categories SET album_count = album_count + 1 WHERE id = NEW.category_id; - END IF; - RETURN NEW; - ELSIF TG_OP = 'DELETE' THEN - IF OLD.category_id IS NOT NULL THEN - UPDATE categories SET album_count = album_count - 1 WHERE id = OLD.category_id; - END IF; - RETURN OLD; - ELSIF TG_OP = 'UPDATE' THEN - IF OLD.category_id IS DISTINCT FROM NEW.category_id THEN - IF OLD.category_id IS NOT NULL THEN - UPDATE categories SET album_count = album_count - 1 WHERE id = OLD.category_id; - END IF; - IF NEW.category_id IS NOT NULL THEN - UPDATE categories SET album_count = album_count + 1 WHERE id = NEW.category_id; - END IF; - END IF; - RETURN NEW; - END IF; - RETURN NULL; -END; -$$ language 'plpgsql'; - -CREATE TRIGGER trigger_update_album_counts -AFTER INSERT OR UPDATE OR DELETE ON albums -FOR EACH ROW EXECUTE FUNCTION update_album_counts(); - --- +migrate Down - -DROP TRIGGER IF EXISTS trigger_update_album_counts ON albums; -DROP FUNCTION IF EXISTS update_album_counts(); -DROP TRIGGER IF EXISTS trigger_update_photo_counts ON photos; -DROP FUNCTION IF EXISTS update_photo_counts(); -ALTER TABLE photos DROP CONSTRAINT IF EXISTS fk_photos_category_id; -ALTER TABLE photos DROP CONSTRAINT IF EXISTS fk_photos_album_id; \ No newline at end of file diff --git a/backend-old/pkg/CLAUDE.md b/backend-old/pkg/CLAUDE.md deleted file mode 100644 index ada801b..0000000 --- a/backend-old/pkg/CLAUDE.md +++ /dev/null @@ -1,793 +0,0 @@ -# Shared Package Layer - CLAUDE.md - -本文件为 Claude Code 在共享包模块中工作时提供指导。 - -## 🎯 模块概览 - -pkg 包提供可复用的工具和组件,供整个应用使用,遵循 Go 语言的包设计哲学。 - -### 主要职责 -- 📦 提供通用工具和实用函数 -- 📝 提供日志记录组件 -- 🔄 提供统一响应格式 -- ✅ 提供数据验证工具 -- 🛠️ 提供辅助功能函数 - -## 📁 模块结构 - -``` -pkg/ -├── CLAUDE.md # 📋 当前文件 - 公共工具包指导 -├── logger/ # 📝 日志工具 -│ ├── logger.go # 日志接口和实现 -│ ├── zap_logger.go # Zap 日志实现 -│ ├── config.go # 日志配置 -│ └── logger_test.go # 日志测试 -├── response/ # 🔄 响应格式 -│ ├── response.go # 统一响应结构 -│ ├── error.go # 错误响应 -│ ├── success.go # 成功响应 -│ └── pagination.go # 分页响应 -├── validator/ # ✅ 数据验证 -│ ├── validator.go # 验证器接口 -│ ├── gin_validator.go # Gin 验证器实现 -│ ├── custom_validators.go # 自定义验证规则 -│ └── validator_test.go # 验证器测试 -├── utils/ # 🛠️ 通用工具 -│ ├── string.go # 字符串工具 -│ ├── time.go # 时间工具 -│ ├── crypto.go # 加密工具 -│ ├── file.go # 文件工具 -│ ├── uuid.go # UUID 生成 -│ └── utils_test.go # 工具测试 -├── middleware/ # 🔗 中间件 -│ ├── cors.go # CORS 中间件 -│ ├── rate_limit.go # 限流中间件 -│ ├── request_id.go # 请求ID中间件 -│ └── recovery.go # 错误恢复中间件 -├── database/ # 🗄️ 数据库工具 -│ ├── connection.go # 数据库连接 -│ ├── migrate.go # 数据库迁移 -│ └── health.go # 健康检查 -└── config/ # ⚙️ 配置工具 - ├── config.go # 配置管理 - ├── env.go # 环境变量处理 - └── validation.go # 配置验证 -``` - -## 📝 日志组件 - -### 日志接口设计 -```go -// logger/logger.go - 日志接口 -package logger - -import ( - "context" -) - -// Logger 日志接口 -type Logger interface { - // 基础日志方法 - Debug(msg string, fields ...Field) - Info(msg string, fields ...Field) - Warn(msg string, fields ...Field) - Error(msg string, fields ...Field) - Fatal(msg string, fields ...Field) - - // 上下文日志方法 - DebugContext(ctx context.Context, msg string, fields ...Field) - InfoContext(ctx context.Context, msg string, fields ...Field) - WarnContext(ctx context.Context, msg string, fields ...Field) - ErrorContext(ctx context.Context, msg string, fields ...Field) - - // 结构化字段方法 - With(fields ...Field) Logger - WithError(err error) Logger - WithContext(ctx context.Context) Logger - - // 日志级别控制 - SetLevel(level Level) - GetLevel() Level -} - -// Field 日志字段 -type Field struct { - Key string - Value interface{} -} - -// Level 日志级别 -type Level int - -const ( - DebugLevel Level = iota - InfoLevel - WarnLevel - ErrorLevel - FatalLevel -) - -// 便捷字段构造函数 -func String(key, value string) Field { - return Field{Key: key, Value: value} -} - -func Int(key string, value int) Field { - return Field{Key: key, Value: value} -} - -func Uint(key string, value uint) Field { - return Field{Key: key, Value: value} -} - -func Error(err error) Field { - return Field{Key: "error", Value: err} -} - -func Any(key string, value interface{}) Field { - return Field{Key: key, Value: value} -} -``` - -### Zap 日志实现 -```go -// logger/zap_logger.go - Zap 日志实现 -package logger - -import ( - "context" - "os" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -// ZapLogger Zap 日志实现 -type ZapLogger struct { - logger *zap.Logger - config *Config -} - -// Config 日志配置 -type Config struct { - Level string `mapstructure:"level" json:"level"` - Format string `mapstructure:"format" json:"format"` // json, console - OutputPath string `mapstructure:"output_path" json:"output_path"` - ErrorPath string `mapstructure:"error_path" json:"error_path"` - MaxSize int `mapstructure:"max_size" json:"max_size"` - MaxAge int `mapstructure:"max_age" json:"max_age"` - MaxBackups int `mapstructure:"max_backups" json:"max_backups"` - Compress bool `mapstructure:"compress" json:"compress"` -} - -// NewZapLogger 创建 Zap 日志实例 -func NewZapLogger(config *Config) (Logger, error) { - zapConfig := zap.NewProductionConfig() - - // 设置日志级别 - level, err := zapcore.ParseLevel(config.Level) - if err != nil { - level = zapcore.InfoLevel - } - zapConfig.Level.SetLevel(level) - - // 设置输出格式 - if config.Format == "console" { - zapConfig.Encoding = "console" - zapConfig.EncoderConfig = zap.NewDevelopmentEncoderConfig() - } else { - zapConfig.Encoding = "json" - zapConfig.EncoderConfig = zap.NewProductionEncoderConfig() - } - - // 设置输出路径 - if config.OutputPath != "" { - zapConfig.OutputPaths = []string{config.OutputPath} - } - if config.ErrorPath != "" { - zapConfig.ErrorOutputPaths = []string{config.ErrorPath} - } - - logger, err := zapConfig.Build() - if err != nil { - return nil, err - } - - return &ZapLogger{ - logger: logger, - config: config, - }, nil -} - -// Debug 调试日志 -func (l *ZapLogger) Debug(msg string, fields ...Field) { - l.logger.Debug(msg, l.convertFields(fields...)...) -} - -// Info 信息日志 -func (l *ZapLogger) Info(msg string, fields ...Field) { - l.logger.Info(msg, l.convertFields(fields...)...) -} - -// Warn 警告日志 -func (l *ZapLogger) Warn(msg string, fields ...Field) { - l.logger.Warn(msg, l.convertFields(fields...)...) -} - -// Error 错误日志 -func (l *ZapLogger) Error(msg string, fields ...Field) { - l.logger.Error(msg, l.convertFields(fields...)...) -} - -// Fatal 致命错误日志 -func (l *ZapLogger) Fatal(msg string, fields ...Field) { - l.logger.Fatal(msg, l.convertFields(fields...)...) -} - -// With 添加字段 -func (l *ZapLogger) With(fields ...Field) Logger { - return &ZapLogger{ - logger: l.logger.With(l.convertFields(fields...)...), - config: l.config, - } -} - -// WithError 添加错误字段 -func (l *ZapLogger) WithError(err error) Logger { - return l.With(Error(err)) -} - -// convertFields 转换字段格式 -func (l *ZapLogger) convertFields(fields ...Field) []zap.Field { - zapFields := make([]zap.Field, len(fields)) - for i, field := range fields { - zapFields[i] = zap.Any(field.Key, field.Value) - } - return zapFields -} - -// NewNoop 创建空日志实例(用于测试) -func NewNoop() Logger { - return &ZapLogger{ - logger: zap.NewNop(), - config: &Config{}, - } -} -``` - -## 🔄 响应格式 - -### 统一响应结构 -```go -// response/response.go - 统一响应结构 -package response - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -// Response 基础响应结构 -type Response struct { - Success bool `json:"success"` - Data interface{} `json:"data,omitempty"` - Message string `json:"message,omitempty"` - Error *Error `json:"error,omitempty"` - Timestamp int64 `json:"timestamp"` - RequestID string `json:"request_id,omitempty"` -} - -// Error 错误信息结构 -type Error struct { - Code string `json:"code"` - Message string `json:"message"` - Details string `json:"details,omitempty"` -} - -// PaginatedResponse 分页响应结构 -type PaginatedResponse struct { - Response - Pagination *Pagination `json:"pagination,omitempty"` -} - -// Pagination 分页信息 -type Pagination struct { - Page int `json:"page"` - Limit int `json:"limit"` - Total int64 `json:"total"` - TotalPages int `json:"total_pages"` - HasNext bool `json:"has_next"` - HasPrev bool `json:"has_prev"` -} - -// Success 成功响应 -func Success(c *gin.Context, data interface{}, message ...string) { - msg := "Success" - if len(message) > 0 { - msg = message[0] - } - - response := Response{ - Success: true, - Data: data, - Message: msg, - Timestamp: time.Now().Unix(), - RequestID: getRequestID(c), - } - - c.JSON(http.StatusOK, response) -} - -// Error 错误响应 -func Error(c *gin.Context, code string, message string, details ...string) { - detail := "" - if len(details) > 0 { - detail = details[0] - } - - response := Response{ - Success: false, - Error: &Error{ - Code: code, - Message: message, - Details: detail, - }, - Timestamp: time.Now().Unix(), - RequestID: getRequestID(c), - } - - c.JSON(getHTTPStatusFromCode(code), response) -} - -// BadRequest 400 错误 -func BadRequest(c *gin.Context, message string, details ...string) { - Error(c, "BAD_REQUEST", message, details...) -} - -// Unauthorized 401 错误 -func Unauthorized(c *gin.Context, message string, details ...string) { - Error(c, "UNAUTHORIZED", message, details...) -} - -// Forbidden 403 错误 -func Forbidden(c *gin.Context, message string, details ...string) { - Error(c, "FORBIDDEN", message, details...) -} - -// NotFound 404 错误 -func NotFound(c *gin.Context, message string, details ...string) { - Error(c, "NOT_FOUND", message, details...) -} - -// InternalError 500 错误 -func InternalError(c *gin.Context, message string, details ...string) { - Error(c, "INTERNAL_ERROR", message, details...) -} - -// Paginated 分页响应 -func Paginated(c *gin.Context, data interface{}, page, limit int, total int64, message ...string) { - msg := "Success" - if len(message) > 0 { - msg = message[0] - } - - totalPages := int((total + int64(limit) - 1) / int64(limit)) - - pagination := &Pagination{ - Page: page, - Limit: limit, - Total: total, - TotalPages: totalPages, - HasNext: page < totalPages, - HasPrev: page > 1, - } - - response := PaginatedResponse{ - Response: Response{ - Success: true, - Data: data, - Message: msg, - Timestamp: time.Now().Unix(), - RequestID: getRequestID(c), - }, - Pagination: pagination, - } - - c.JSON(http.StatusOK, response) -} - -// getRequestID 获取请求ID -func getRequestID(c *gin.Context) string { - if requestID, exists := c.Get("X-Request-ID"); exists { - if id, ok := requestID.(string); ok { - return id - } - } - return "" -} - -// getHTTPStatusFromCode 根据错误码获取HTTP状态码 -func getHTTPStatusFromCode(code string) int { - switch code { - case "BAD_REQUEST", "INVALID_PARAMETER", "VALIDATION_ERROR": - return http.StatusBadRequest - case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED": - return http.StatusUnauthorized - case "FORBIDDEN", "PERMISSION_DENIED": - return http.StatusForbidden - case "NOT_FOUND", "USER_NOT_FOUND", "PHOTO_NOT_FOUND": - return http.StatusNotFound - case "CONFLICT", "USER_EXISTS", "DUPLICATE_KEY": - return http.StatusConflict - case "TOO_MANY_REQUESTS": - return http.StatusTooManyRequests - default: - return http.StatusInternalServerError - } -} -``` - -## ✅ 数据验证 - -### 自定义验证器 -```go -// validator/custom_validators.go - 自定义验证规则 -package validator - -import ( - "regexp" - "strings" - - "github.com/go-playground/validator/v10" -) - -// RegisterCustomValidators 注册自定义验证器 -func RegisterCustomValidators(v *validator.Validate) { - v.RegisterValidation("username", validateUsername) - v.RegisterValidation("password", validatePassword) - v.RegisterValidation("phone", validatePhone) - v.RegisterValidation("slug", validateSlug) - v.RegisterValidation("file_ext", validateFileExtension) -} - -// validateUsername 验证用户名 -func validateUsername(fl validator.FieldLevel) bool { - username := fl.Field().String() - - // 3-50个字符,只允许字母、数字、下划线、短横线 - matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, username) - return matched -} - -// validatePassword 验证密码强度 -func validatePassword(fl validator.FieldLevel) bool { - password := fl.Field().String() - - // 至少8个字符,包含字母、数字和特殊字符 - if len(password) < 8 { - return false - } - - hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password) - hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password) - hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]`).MatchString(password) - - return hasLetter && hasNumber && hasSpecial -} - -// validatePhone 验证手机号 -func validatePhone(fl validator.FieldLevel) bool { - phone := fl.Field().String() - - // 支持多种手机号格式 - patterns := []string{ - `^1[3-9]\d{9}$`, // 中国大陆 - `^\+1[2-9]\d{9}$`, // 美国 - `^\+44[1-9]\d{8,9}$`, // 英国 - } - - for _, pattern := range patterns { - if matched, _ := regexp.MatchString(pattern, phone); matched { - return true - } - } - - return false -} - -// validateSlug 验证 URL 友好字符串 -func validateSlug(fl validator.FieldLevel) bool { - slug := fl.Field().String() - matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug) - return matched -} - -// validateFileExtension 验证文件扩展名 -func validateFileExtension(fl validator.FieldLevel) bool { - filename := fl.Field().String() - ext := strings.ToLower(getFileExtension(filename)) - - // 允许的图片文件扩展名 - allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"} - - for _, allowedExt := range allowedExts { - if ext == allowedExt { - return true - } - } - - return false -} - -// getFileExtension 获取文件扩展名 -func getFileExtension(filename string) string { - lastDot := strings.LastIndex(filename, ".") - if lastDot == -1 { - return "" - } - return filename[lastDot:] -} - -// ValidationError 验证错误结构 -type ValidationError struct { - Field string `json:"field"` - Value string `json:"value"` - Tag string `json:"tag"` - Message string `json:"message"` -} - -// FormatValidationErrors 格式化验证错误 -func FormatValidationErrors(err error) []ValidationError { - var errors []ValidationError - - if validationErrors, ok := err.(validator.ValidationErrors); ok { - for _, e := range validationErrors { - errors = append(errors, ValidationError{ - Field: e.Field(), - Value: e.Value().(string), - Tag: e.Tag(), - Message: getErrorMessage(e), - }) - } - } - - return errors -} - -// getErrorMessage 获取错误消息 -func getErrorMessage(e validator.FieldError) string { - switch e.Tag() { - case "required": - return e.Field() + " is required" - case "email": - return e.Field() + " must be a valid email" - case "min": - return e.Field() + " must be at least " + e.Param() + " characters" - case "max": - return e.Field() + " must be at most " + e.Param() + " characters" - case "username": - return e.Field() + " must be 3-50 characters and contain only letters, numbers, underscores, and hyphens" - case "password": - return e.Field() + " must be at least 8 characters and contain letters, numbers, and special characters" - case "phone": - return e.Field() + " must be a valid phone number" - case "slug": - return e.Field() + " must be a valid URL slug" - case "file_ext": - return e.Field() + " must have a valid image file extension" - default: - return e.Field() + " is invalid" - } -} -``` - -## 🛠️ 通用工具 - -### 字符串工具 -```go -// utils/string.go - 字符串工具 -package utils - -import ( - "crypto/rand" - "math/big" - "regexp" - "strings" - "unicode" -) - -// GenerateRandomString 生成随机字符串 -func GenerateRandomString(length int) (string, error) { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - result := make([]byte, length) - - for i := range result { - index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - result[i] = charset[index.Int64()] - } - - return string(result), nil -} - -// ToSnakeCase 转换为蛇形命名 -func ToSnakeCase(str string) string { - var result []rune - - for i, r := range str { - if unicode.IsUpper(r) { - if i > 0 { - result = append(result, '_') - } - result = append(result, unicode.ToLower(r)) - } else { - result = append(result, r) - } - } - - return string(result) -} - -// ToCamelCase 转换为驼峰命名 -func ToCamelCase(str string) string { - parts := strings.Split(str, "_") - for i := 1; i < len(parts); i++ { - if len(parts[i]) > 0 { - parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:] - } - } - return strings.Join(parts, "") -} - -// Truncate 截断字符串 -func Truncate(str string, length int) string { - if len(str) <= length { - return str - } - return str[:length] + "..." -} - -// IsValidEmail 验证邮箱格式 -func IsValidEmail(email string) bool { - pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` - matched, _ := regexp.MatchString(pattern, email) - return matched -} - -// SanitizeFilename 清理文件名 -func SanitizeFilename(filename string) string { - // 移除或替换不安全的字符 - reg := regexp.MustCompile(`[<>:"/\\|?*]`) - return reg.ReplaceAllString(filename, "_") -} - -// Contains 检查字符串切片是否包含指定字符串 -func Contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} -``` - -### 加密工具 -```go -// utils/crypto.go - 加密工具 -package utils - -import ( - "crypto/md5" - "crypto/sha256" - "encoding/hex" - "golang.org/x/crypto/bcrypt" -) - -// HashPassword 加密密码 -func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(bytes), nil -} - -// CheckPassword 验证密码 -func CheckPassword(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -// MD5Hash 计算MD5哈希 -func MD5Hash(data string) string { - hash := md5.Sum([]byte(data)) - return hex.EncodeToString(hash[:]) -} - -// SHA256Hash 计算SHA256哈希 -func SHA256Hash(data string) string { - hash := sha256.Sum256([]byte(data)) - return hex.EncodeToString(hash[:]) -} -``` - -## 🔗 中间件 - -### CORS 中间件 -```go -// middleware/cors.go - CORS 中间件 -package middleware - -import ( - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -// CORS 创建CORS中间件 -func CORS() gin.HandlerFunc { - config := cors.DefaultConfig() - config.AllowOrigins = []string{"*"} - config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} - config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"} - config.ExposeHeaders = []string{"X-Request-ID"} - config.AllowCredentials = true - - return cors.New(config) -} -``` - -### 请求ID中间件 -```go -// middleware/request_id.go - 请求ID中间件 -package middleware - -import ( - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -// RequestID 请求ID中间件 -func RequestID() gin.HandlerFunc { - return func(c *gin.Context) { - requestID := c.GetHeader("X-Request-ID") - if requestID == "" { - requestID = uuid.New().String() - } - - c.Set("X-Request-ID", requestID) - c.Header("X-Request-ID", requestID) - c.Next() - } -} -``` - -## 💡 最佳实践 - -### 包设计原则 -1. **单一职责**: 每个包只负责一个明确的功能 -2. **接口导向**: 优先定义接口,便于测试和替换 -3. **零依赖**: 包应尽量减少外部依赖 -4. **文档完善**: 提供清晰的使用文档和示例 -5. **向后兼容**: 保持API的向后兼容性 - -### 代码质量 -1. **测试覆盖**: 为所有公共函数编写测试 -2. **错误处理**: 合理处理和传播错误 -3. **性能考虑**: 关注内存分配和性能影响 -4. **线程安全**: 确保并发使用的安全性 -5. **资源管理**: 及时释放资源 - -### 使用建议 -1. **导入路径**: 使用清晰的导入路径 -2. **命名规范**: 遵循 Go 语言命名约定 -3. **版本管理**: 使用语义化版本管理 -4. **依赖管理**: 合理管理第三方依赖 -5. **配置化**: 支持配置化使用 - -本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。 \ No newline at end of file diff --git a/backend-old/pkg/logger/logger.go b/backend-old/pkg/logger/logger.go deleted file mode 100644 index e1b9149..0000000 --- a/backend-old/pkg/logger/logger.go +++ /dev/null @@ -1,76 +0,0 @@ -package logger - -import ( - "os" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/natefinch/lumberjack.v2" - "photography-backend/internal/config" -) - -// InitLogger 初始化日志记录器 -func InitLogger(cfg *config.LoggerConfig) (*zap.Logger, error) { - // 设置日志级别 - var level zapcore.Level - switch cfg.Level { - case "debug": - level = zapcore.DebugLevel - case "info": - level = zapcore.InfoLevel - case "warn": - level = zapcore.WarnLevel - case "error": - level = zapcore.ErrorLevel - default: - level = zapcore.InfoLevel - } - - // 创建编码器配置 - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.TimeKey = "timestamp" - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder - - // 创建编码器 - var encoder zapcore.Encoder - if cfg.Format == "json" { - encoder = zapcore.NewJSONEncoder(encoderConfig) - } else { - encoder = zapcore.NewConsoleEncoder(encoderConfig) - } - - // 创建写入器 - var writers []zapcore.WriteSyncer - - // 控制台输出 - writers = append(writers, zapcore.AddSync(os.Stdout)) - - // 文件输出 - if cfg.Output == "file" && cfg.Filename != "" { - // 确保日志目录存在 - if err := os.MkdirAll("logs", 0755); err != nil { - return nil, err - } - - fileWriter := &lumberjack.Logger{ - Filename: cfg.Filename, - MaxSize: cfg.MaxSize, - MaxAge: cfg.MaxAge, - MaxBackups: 10, - LocalTime: true, - Compress: cfg.Compress, - } - writers = append(writers, zapcore.AddSync(fileWriter)) - } - - // 合并写入器 - writer := zapcore.NewMultiWriteSyncer(writers...) - - // 创建核心 - core := zapcore.NewCore(encoder, writer, level) - - // 创建日志记录器 - logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) - - return logger, nil -} \ No newline at end of file diff --git a/backend-old/pkg/response/response.go b/backend-old/pkg/response/response.go deleted file mode 100644 index 06fbf7e..0000000 --- a/backend-old/pkg/response/response.go +++ /dev/null @@ -1,165 +0,0 @@ -package response - -import ( - "net/http" - "time" -) - -// Response 统一响应结构 -type Response struct { - Success bool `json:"success"` - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` - Meta *Meta `json:"meta,omitempty"` -} - -// Meta 元数据 -type Meta struct { - Timestamp string `json:"timestamp"` - RequestID string `json:"request_id,omitempty"` -} - -// PaginatedResponse 分页响应 -type PaginatedResponse struct { - Success bool `json:"success"` - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data"` - Pagination *Pagination `json:"pagination"` - Meta *Meta `json:"meta,omitempty"` -} - -// Pagination 分页信息 -type Pagination struct { - Page int `json:"page"` - Limit int `json:"limit"` - Total int64 `json:"total"` - TotalPages int `json:"total_pages"` - HasNext bool `json:"has_next"` - HasPrev bool `json:"has_prev"` -} - -// Success 成功响应 -func Success(data interface{}) *Response { - return &Response{ - Success: true, - Code: http.StatusOK, - Message: "Success", - Data: data, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// Error 错误响应 -func Error(code int, message string) *Response { - return &Response{ - Success: false, - Code: code, - Message: message, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// Created 创建成功响应 -func Created(data interface{}) *Response { - return &Response{ - Success: true, - Code: http.StatusCreated, - Message: "Created successfully", - Data: data, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// Updated 更新成功响应 -func Updated(data interface{}) *Response { - return &Response{ - Success: true, - Code: http.StatusOK, - Message: "Updated successfully", - Data: data, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// Deleted 删除成功响应 -func Deleted() *Response { - return &Response{ - Success: true, - Code: http.StatusOK, - Message: "Deleted successfully", - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// Paginated 分页响应 -func Paginated(data interface{}, page, limit int, total int64) *PaginatedResponse { - totalPages := int((total + int64(limit) - 1) / int64(limit)) - - return &PaginatedResponse{ - Success: true, - Code: http.StatusOK, - Message: "Success", - Data: data, - Pagination: &Pagination{ - Page: page, - Limit: limit, - Total: total, - TotalPages: totalPages, - HasNext: page < totalPages, - HasPrev: page > 1, - }, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} - -// BadRequest 400错误 -func BadRequest(message string) *Response { - return Error(http.StatusBadRequest, message) -} - -// Unauthorized 401错误 -func Unauthorized(message string) *Response { - return Error(http.StatusUnauthorized, message) -} - -// Forbidden 403错误 -func Forbidden(message string) *Response { - return Error(http.StatusForbidden, message) -} - -// NotFound 404错误 -func NotFound(message string) *Response { - return Error(http.StatusNotFound, message) -} - -// InternalServerError 500错误 -func InternalServerError(message string) *Response { - return Error(http.StatusInternalServerError, message) -} - -// ValidationError 验证错误 -func ValidationError(errors map[string]string) *Response { - return &Response{ - Success: false, - Code: http.StatusUnprocessableEntity, - Message: "Validation failed", - Data: errors, - Meta: &Meta{ - Timestamp: time.Now().Format(time.RFC3339), - }, - } -} \ No newline at end of file diff --git a/backend-old/start.sh b/backend-old/start.sh deleted file mode 100755 index d1ca2c9..0000000 --- a/backend-old/start.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# 摄影作品集后端启动脚本 - -echo "=== 摄影作品集后端启动 ===" - -# 检查Go是否安装 -if ! command -v go &> /dev/null; then - echo "错误: Go 未安装,请先安装 Go 1.21 或更高版本" - exit 1 -fi - -# 检查Go版本 -GO_VERSION=$(go version | cut -d' ' -f3 | sed 's/go//') -echo "Go 版本: $GO_VERSION" - -# 进入项目目录 -cd "$(dirname "$0")" - -# 创建必要的目录 -echo "创建必要的目录..." -mkdir -p uploads/photos -mkdir -p uploads/thumbnails -mkdir -p uploads/temp -mkdir -p logs - -# 安装依赖 -echo "安装Go模块依赖..." -go mod tidy - -# 设置环境变量 -export CONFIG_PATH="configs/config.dev.yaml" - -# 启动服务器 -echo "启动后端服务器..." -echo "配置文件: $CONFIG_PATH" -echo "访问地址: http://localhost:8080" -echo "健康检查: http://localhost:8080/health" -echo "API文档: 请查看 test_api.http 文件" -echo "" -echo "按 Ctrl+C 停止服务器" -echo "" - -# 运行服务器 -go run cmd/server/main_simple.go \ No newline at end of file diff --git a/backend-old/test_api.http b/backend-old/test_api.http deleted file mode 100644 index 85e88cd..0000000 --- a/backend-old/test_api.http +++ /dev/null @@ -1,116 +0,0 @@ -# Photography Backend API 测试文件 - -### 健康检查 -GET http://localhost:8080/health - -### 获取数据库统计 -GET http://localhost:8080/stats - -### 用户注册 -POST http://localhost:8080/api/v1/auth/register -Content-Type: application/json - -{ - "username": "testuser", - "email": "test@example.com", - "password": "password123", - "name": "测试用户" -} - -### 用户登录 -POST http://localhost:8080/api/v1/auth/login -Content-Type: application/json - -{ - "username": "testuser", - "password": "password123" -} - -### 获取所有用户 -GET http://localhost:8080/api/v1/users - -### 创建用户 -POST http://localhost:8080/api/v1/users -Content-Type: application/json - -{ - "username": "newuser", - "email": "newuser@example.com", - "password": "password123", - "name": "新用户", - "role": "user" -} - -### 获取所有分类 -GET http://localhost:8080/api/v1/categories - -### 创建分类 -POST http://localhost:8080/api/v1/categories -Content-Type: application/json - -{ - "name": "风景摄影", - "description": "自然风光摄影作品", - "user_id": 1, - "color": "#10B981" -} - -### 获取所有标签 -GET http://localhost:8080/api/v1/tags - -### 创建标签 -POST http://localhost:8080/api/v1/tags -Content-Type: application/json - -{ - "name": "自然", - "description": "自然风光主题", - "user_id": 1, - "color": "#10B981" -} - -### 获取所有相册 -GET http://localhost:8080/api/v1/albums - -### 创建相册 -POST http://localhost:8080/api/v1/albums -Content-Type: application/json - -{ - "title": "我的摄影作品", - "description": "个人摄影作品集", - "user_id": 1, - "is_public": true -} - -### 获取所有照片 -GET http://localhost:8080/api/v1/photos - -### 创建照片记录 -POST http://localhost:8080/api/v1/photos -Content-Type: application/json - -{ - "title": "美丽风景", - "description": "在山顶拍摄的日出", - "filename": "sunrise.jpg", - "original_url": "http://localhost:8080/uploads/photos/sunrise.jpg", - "file_size": 1024000, - "mime_type": "image/jpeg", - "user_id": 1, - "category_id": 1 -} - -### 文件上传 (需要使用支持文件上传的客户端) -# POST http://localhost:8080/api/v1/upload/photo -# Content-Type: multipart/form-data -# Authorization: Bearer YOUR_JWT_TOKEN -# -# photo: [选择图片文件] -# title: 照片标题 -# description: 照片描述 -# category_id: 1 - -### 获取上传统计 (需要管理员权限) -# GET http://localhost:8080/api/v1/upload/stats -# Authorization: Bearer ADMIN_JWT_TOKEN \ No newline at end of file diff --git a/backend-old/tests/CLAUDE.md b/backend-old/tests/CLAUDE.md deleted file mode 100644 index dc78714..0000000 --- a/backend-old/tests/CLAUDE.md +++ /dev/null @@ -1,834 +0,0 @@ -# Test Module - CLAUDE.md - -本文件为 Claude Code 在测试模块中工作时提供指导。 - -## 🎯 模块概览 - -测试模块提供完整的测试策略和工具,确保代码质量和系统可靠性。 - -### 主要职责 -- 🧪 单元测试和集成测试 -- 🔧 测试工具和辅助函数 -- 📊 测试覆盖率报告 -- 🚀 性能测试和基准测试 -- 🎭 Mock 对象和测试数据 - -## 📁 模块结构 - -``` -tests/ -├── CLAUDE.md # 📋 当前文件 - 测试编写和执行指导 -├── unit/ # 🧪 单元测试 -│ ├── service/ # 服务层测试 -│ │ ├── user_service_test.go -│ │ ├── photo_service_test.go -│ │ └── auth_service_test.go -│ ├── repository/ # 仓储层测试 -│ │ ├── user_repository_test.go -│ │ └── photo_repository_test.go -│ ├── handler/ # 处理器测试 -│ │ ├── user_handler_test.go -│ │ └── photo_handler_test.go -│ └── utils/ # 工具函数测试 -│ ├── string_test.go -│ └── crypto_test.go -├── integration/ # 🔗 集成测试 -│ ├── api/ # API 集成测试 -│ │ ├── user_api_test.go -│ │ ├── photo_api_test.go -│ │ └── auth_api_test.go -│ ├── database/ # 数据库集成测试 -│ │ ├── migration_test.go -│ │ └── transaction_test.go -│ └── storage/ # 存储集成测试 -│ └── file_storage_test.go -├── e2e/ # 🎯 端到端测试 -│ ├── user_journey_test.go # 用户流程测试 -│ ├── photo_upload_test.go # 照片上传流程 -│ └── auth_flow_test.go # 认证流程测试 -├── benchmark/ # ⚡ 性能测试 -│ ├── service_bench_test.go # 服务性能测试 -│ ├── repository_bench_test.go # 仓储性能测试 -│ └── api_bench_test.go # API 性能测试 -├── fixtures/ # 📄 测试数据 -│ ├── users.json # 用户测试数据 -│ ├── photos.json # 照片测试数据 -│ └── categories.json # 分类测试数据 -├── mocks/ # 🎭 Mock 对象 -│ ├── service_mocks.go # 服务层 Mock -│ ├── repository_mocks.go # 仓储层 Mock -│ └── external_mocks.go # 外部服务 Mock -├── helpers/ # 🛠️ 测试辅助 -│ ├── database.go # 数据库测试助手 -│ ├── server.go # 测试服务器 -│ ├── assertions.go # 自定义断言 -│ └── fixtures.go # 测试数据加载 -└── config/ # ⚙️ 测试配置 - ├── test_config.yaml # 测试环境配置 - └── docker-compose.test.yml # 测试容器配置 -``` - -## 🧪 单元测试 - -### 服务层测试示例 -```go -// unit/service/user_service_test.go - 用户服务测试 -package service_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "photography-backend/internal/model/dto" - "photography-backend/internal/model/entity" - "photography-backend/internal/service/user" - "photography-backend/tests/mocks" - "photography-backend/pkg/logger" -) - -// UserServiceTestSuite 用户服务测试套件 -type UserServiceTestSuite struct { - suite.Suite - userService *user.UserService - mockRepo *mocks.MockUserRepository - mockLogger logger.Logger -} - -// SetupTest 测试前置设置 -func (suite *UserServiceTestSuite) SetupTest() { - suite.mockRepo = new(mocks.MockUserRepository) - suite.mockLogger = logger.NewNoop() - suite.userService = user.NewUserService(suite.mockRepo, suite.mockLogger) -} - -// TearDownTest 测试后置清理 -func (suite *UserServiceTestSuite) TearDownTest() { - suite.mockRepo.AssertExpectations(suite.T()) -} - -// TestCreateUser_Success 测试创建用户成功 -func (suite *UserServiceTestSuite) TestCreateUser_Success() { - // Arrange - ctx := context.Background() - req := &dto.CreateUserRequest{ - Username: "testuser", - Email: "test@example.com", - Password: "password123", - } - - expectedUser := &entity.User{ - ID: 1, - Username: req.Username, - Email: req.Email, - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - suite.mockRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound) - suite.mockRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")).Return(expectedUser, nil) - - // Act - user, err := suite.userService.CreateUser(ctx, req) - - // Assert - assert.NoError(suite.T(), err) - assert.NotNil(suite.T(), user) - assert.Equal(suite.T(), expectedUser.ID, user.ID) - assert.Equal(suite.T(), expectedUser.Username, user.Username) - assert.Equal(suite.T(), expectedUser.Email, user.Email) -} - -// TestCreateUser_UserExists 测试用户已存在 -func (suite *UserServiceTestSuite) TestCreateUser_UserExists() { - // Arrange - ctx := context.Background() - req := &dto.CreateUserRequest{ - Username: "testuser", - Email: "test@example.com", - Password: "password123", - } - - existingUser := &entity.User{ - ID: 1, - Email: req.Email, - } - - suite.mockRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil) - - // Act - user, err := suite.userService.CreateUser(ctx, req) - - // Assert - assert.Error(suite.T(), err) - assert.Nil(suite.T(), user) - assert.Contains(suite.T(), err.Error(), "user already exists") -} - -// TestCreateUser_InvalidInput 测试无效输入 -func (suite *UserServiceTestSuite) TestCreateUser_InvalidInput() { - ctx := context.Background() - - testCases := []struct { - name string - req *dto.CreateUserRequest - }{ - { - name: "empty email", - req: &dto.CreateUserRequest{ - Username: "testuser", - Email: "", - Password: "password123", - }, - }, - { - name: "empty username", - req: &dto.CreateUserRequest{ - Username: "", - Email: "test@example.com", - Password: "password123", - }, - }, - { - name: "weak password", - req: &dto.CreateUserRequest{ - Username: "testuser", - Email: "test@example.com", - Password: "123", - }, - }, - } - - for _, tc := range testCases { - suite.T().Run(tc.name, func(t *testing.T) { - // Act - user, err := suite.userService.CreateUser(ctx, tc.req) - - // Assert - assert.Error(t, err) - assert.Nil(t, user) - }) - } -} - -// 运行测试套件 -func TestUserServiceTestSuite(t *testing.T) { - suite.Run(t, new(UserServiceTestSuite)) -} -``` - -### 仓储层测试示例 -```go -// unit/repository/user_repository_test.go - 用户仓储测试 -package repository_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - - "photography-backend/internal/model/entity" - "photography-backend/internal/repository/postgres" - "photography-backend/pkg/logger" - "photography-backend/tests/helpers" -) - -// UserRepositoryTestSuite 用户仓储测试套件 -type UserRepositoryTestSuite struct { - suite.Suite - db *gorm.DB - repo *postgres.UserRepository -} - -// SetupSuite 套件前置设置 -func (suite *UserRepositoryTestSuite) SetupSuite() { - // 使用内存数据库 - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - suite.Require().NoError(err) - - // 自动迁移 - err = db.AutoMigrate(&entity.User{}) - suite.Require().NoError(err) - - suite.db = db - suite.repo = postgres.NewUserRepository(db, logger.NewNoop()).(*postgres.UserRepository) -} - -// SetupTest 每个测试前清理数据 -func (suite *UserRepositoryTestSuite) SetupTest() { - helpers.CleanupDatabase(suite.db) -} - -// TearDownSuite 套件后置清理 -func (suite *UserRepositoryTestSuite) TearDownSuite() { - sqlDB, _ := suite.db.DB() - sqlDB.Close() -} - -// TestCreateUser 测试创建用户 -func (suite *UserRepositoryTestSuite) TestCreateUser() { - ctx := context.Background() - - user := &entity.User{ - Username: "testuser", - Email: "test@example.com", - Password: "hashedpassword", - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - createdUser, err := suite.repo.Create(ctx, user) - - assert.NoError(suite.T(), err) - assert.NotZero(suite.T(), createdUser.ID) - assert.Equal(suite.T(), user.Username, createdUser.Username) - assert.Equal(suite.T(), user.Email, createdUser.Email) - assert.NotZero(suite.T(), createdUser.CreatedAt) - assert.NotZero(suite.T(), createdUser.UpdatedAt) -} - -// TestGetUserByEmail 测试根据邮箱获取用户 -func (suite *UserRepositoryTestSuite) TestGetUserByEmail() { - ctx := context.Background() - - // 创建测试用户 - user := helpers.CreateTestUser(suite.db, &entity.User{ - Username: "testuser", - Email: "test@example.com", - Password: "hashedpassword", - }) - - // 根据邮箱查找用户 - foundUser, err := suite.repo.GetByEmail(ctx, user.Email) - - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), user.ID, foundUser.ID) - assert.Equal(suite.T(), user.Email, foundUser.Email) -} - -// TestGetUserByEmail_NotFound 测试用户不存在 -func (suite *UserRepositoryTestSuite) TestGetUserByEmail_NotFound() { - ctx := context.Background() - - user, err := suite.repo.GetByEmail(ctx, "nonexistent@example.com") - - assert.Error(suite.T(), err) - assert.Nil(suite.T(), user) - assert.Equal(suite.T(), repository.ErrNotFound, err) -} - -// 运行测试套件 -func TestUserRepositoryTestSuite(t *testing.T) { - suite.Run(t, new(UserRepositoryTestSuite)) -} -``` - -## 🔗 集成测试 - -### API 集成测试示例 -```go -// integration/api/user_api_test.go - 用户API集成测试 -package api_test - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - - "photography-backend/internal/model/dto" - "photography-backend/tests/helpers" -) - -// UserAPITestSuite 用户API测试套件 -type UserAPITestSuite struct { - suite.Suite - server *helpers.TestServer - router *gin.Engine -} - -// SetupSuite 套件前置设置 -func (suite *UserAPITestSuite) SetupSuite() { - suite.server = helpers.NewTestServer() - suite.router = suite.server.Router() -} - -// SetupTest 每个测试前重置数据 -func (suite *UserAPITestSuite) SetupTest() { - suite.server.ResetDatabase() -} - -// TearDownSuite 套件后置清理 -func (suite *UserAPITestSuite) TearDownSuite() { - suite.server.Close() -} - -// TestCreateUser_Success 测试创建用户成功 -func (suite *UserAPITestSuite) TestCreateUser_Success() { - // Arrange - req := &dto.CreateUserRequest{ - Username: "testuser", - Email: "test@example.com", - Password: "password123", - } - - jsonData, _ := json.Marshal(req) - - // Act - w := httptest.NewRecorder() - httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) - httpReq.Header.Set("Content-Type", "application/json") - - suite.router.ServeHTTP(w, httpReq) - - // Assert - assert.Equal(suite.T(), http.StatusCreated, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - - assert.True(suite.T(), response["success"].(bool)) - assert.NotNil(suite.T(), response["data"]) - - userData := response["data"].(map[string]interface{}) - assert.Equal(suite.T(), req.Username, userData["username"]) - assert.Equal(suite.T(), req.Email, userData["email"]) - assert.NotZero(suite.T(), userData["id"]) -} - -// TestCreateUser_ValidationError 测试验证错误 -func (suite *UserAPITestSuite) TestCreateUser_ValidationError() { - // Arrange - req := &dto.CreateUserRequest{ - Username: "", // 空用户名应该失败 - Email: "invalid-email", // 无效邮箱 - Password: "123", // 密码过短 - } - - jsonData, _ := json.Marshal(req) - - // Act - w := httptest.NewRecorder() - httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) - httpReq.Header.Set("Content-Type", "application/json") - - suite.router.ServeHTTP(w, httpReq) - - // Assert - assert.Equal(suite.T(), http.StatusBadRequest, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - - assert.False(suite.T(), response["success"].(bool)) - assert.NotNil(suite.T(), response["error"]) -} - -// TestGetUser_Success 测试获取用户成功 -func (suite *UserAPITestSuite) TestGetUser_Success() { - // Arrange - 创建测试用户 - user := suite.server.CreateTestUser() - - // Act - w := httptest.NewRecorder() - httpReq, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/users/%d", user.ID), nil) - - suite.router.ServeHTTP(w, httpReq) - - // Assert - assert.Equal(suite.T(), http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - - assert.True(suite.T(), response["success"].(bool)) - - userData := response["data"].(map[string]interface{}) - assert.Equal(suite.T(), float64(user.ID), userData["id"]) - assert.Equal(suite.T(), user.Username, userData["username"]) - assert.Equal(suite.T(), user.Email, userData["email"]) -} - -// 运行测试套件 -func TestUserAPITestSuite(t *testing.T) { - suite.Run(t, new(UserAPITestSuite)) -} -``` - -## ⚡ 性能测试 - -### 基准测试示例 -```go -// benchmark/service_bench_test.go - 服务性能测试 -package benchmark_test - -import ( - "context" - "testing" - - "photography-backend/internal/model/dto" - "photography-backend/tests/helpers" -) - -// BenchmarkUserService_CreateUser 用户创建性能测试 -func BenchmarkUserService_CreateUser(b *testing.B) { - server := helpers.NewTestServer() - defer server.Close() - - userService := server.UserService() - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - req := &dto.CreateUserRequest{ - Username: fmt.Sprintf("user%d", i), - Email: fmt.Sprintf("user%d@example.com", i), - Password: "password123", - } - - _, err := userService.CreateUser(ctx, req) - if err != nil { - b.Fatal(err) - } - } -} - -// BenchmarkUserService_GetUser 用户查询性能测试 -func BenchmarkUserService_GetUser(b *testing.B) { - server := helpers.NewTestServer() - defer server.Close() - - // 预创建测试用户 - users := make([]*entity.User, 1000) - for i := 0; i < 1000; i++ { - users[i] = server.CreateTestUser() - } - - userService := server.UserService() - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - userID := users[i%1000].ID - _, err := userService.GetUser(ctx, userID) - if err != nil { - b.Fatal(err) - } - } -} - -// BenchmarkUserAPI_CreateUser API性能测试 -func BenchmarkUserAPI_CreateUser(b *testing.B) { - server := helpers.NewTestServer() - defer server.Close() - - router := server.Router() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - req := &dto.CreateUserRequest{ - Username: fmt.Sprintf("user%d", i), - Email: fmt.Sprintf("user%d@example.com", i), - Password: "password123", - } - - jsonData, _ := json.Marshal(req) - - w := httptest.NewRecorder() - httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) - httpReq.Header.Set("Content-Type", "application/json") - - router.ServeHTTP(w, httpReq) - - if w.Code != http.StatusCreated { - b.Fatalf("Expected status %d, got %d", http.StatusCreated, w.Code) - } - } -} -``` - -## 🛠️ 测试辅助工具 - -### 测试服务器 -```go -// helpers/server.go - 测试服务器 -package helpers - -import ( - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "github.com/gin-gonic/gin" - - "photography-backend/internal/model/entity" - "photography-backend/internal/service" - "photography-backend/internal/repository" - "photography-backend/internal/api/handlers" - "photography-backend/pkg/logger" -) - -// TestServer 测试服务器 -type TestServer struct { - db *gorm.DB - router *gin.Engine - services *Services -} - -// Services 服务集合 -type Services struct { - UserService service.UserServicer - PhotoService service.PhotoServicer - AuthService service.AuthServicer -} - -// NewTestServer 创建测试服务器 -func NewTestServer() *TestServer { - // 创建内存数据库 - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - if err != nil { - panic(err) - } - - // 自动迁移 - err = db.AutoMigrate( - &entity.User{}, - &entity.Photo{}, - &entity.Category{}, - &entity.Tag{}, - &entity.Album{}, - ) - if err != nil { - panic(err) - } - - // 创建logger - logger := logger.NewNoop() - - // 创建仓储 - userRepo := repository.NewUserRepository(db, logger) - photoRepo := repository.NewPhotoRepository(db, logger) - - // 创建服务 - userService := service.NewUserService(userRepo, logger) - photoService := service.NewPhotoService(photoRepo, userRepo, nil, logger) - authService := service.NewAuthService(userRepo, nil, logger) - - services := &Services{ - UserService: userService, - PhotoService: photoService, - AuthService: authService, - } - - // 创建路由 - gin.SetMode(gin.TestMode) - router := gin.New() - - // 注册路由 - v1 := router.Group("/api/v1") - { - userHandler := handlers.NewUserHandler(userService, logger) - photoHandler := handlers.NewPhotoHandler(photoService, logger) - authHandler := handlers.NewAuthHandler(authService, logger) - - v1.POST("/users", userHandler.Create) - v1.GET("/users/:id", userHandler.GetByID) - v1.PUT("/users/:id", userHandler.Update) - v1.DELETE("/users/:id", userHandler.Delete) - - v1.POST("/auth/login", authHandler.Login) - v1.POST("/auth/register", authHandler.Register) - - v1.POST("/photos", photoHandler.Create) - v1.GET("/photos/:id", photoHandler.GetByID) - } - - return &TestServer{ - db: db, - router: router, - services: services, - } -} - -// Router 获取路由器 -func (ts *TestServer) Router() *gin.Engine { - return ts.router -} - -// UserService 获取用户服务 -func (ts *TestServer) UserService() service.UserServicer { - return ts.services.UserService -} - -// CreateTestUser 创建测试用户 -func (ts *TestServer) CreateTestUser() *entity.User { - user := &entity.User{ - Username: "testuser", - Email: "test@example.com", - Password: "hashedpassword", - Role: entity.UserRoleUser, - Status: entity.UserStatusActive, - } - - result := ts.db.Create(user) - if result.Error != nil { - panic(result.Error) - } - - return user -} - -// ResetDatabase 重置数据库 -func (ts *TestServer) ResetDatabase() { - CleanupDatabase(ts.db) -} - -// Close 关闭测试服务器 -func (ts *TestServer) Close() { - sqlDB, _ := ts.db.DB() - sqlDB.Close() -} -``` - -### 数据库辅助工具 -```go -// helpers/database.go - 数据库测试助手 -package helpers - -import ( - "gorm.io/gorm" - "photography-backend/internal/model/entity" -) - -// CleanupDatabase 清理数据库 -func CleanupDatabase(db *gorm.DB) { - // 按外键依赖顺序删除 - db.Exec("DELETE FROM album_photos") - db.Exec("DELETE FROM photo_tags") - db.Exec("DELETE FROM photo_categories") - db.Exec("DELETE FROM photos") - db.Exec("DELETE FROM albums") - db.Exec("DELETE FROM tags") - db.Exec("DELETE FROM categories") - db.Exec("DELETE FROM users") - - // 重置自增ID - db.Exec("DELETE FROM sqlite_sequence") -} - -// CreateTestUser 创建测试用户 -func CreateTestUser(db *gorm.DB, user *entity.User) *entity.User { - if user.Role == "" { - user.Role = entity.UserRoleUser - } - if user.Status == "" { - user.Status = entity.UserStatusActive - } - - result := db.Create(user) - if result.Error != nil { - panic(result.Error) - } - - return user -} - -// CreateTestPhoto 创建测试照片 -func CreateTestPhoto(db *gorm.DB, photo *entity.Photo) *entity.Photo { - if photo.Status == "" { - photo.Status = entity.PhotoStatusActive - } - - result := db.Create(photo) - if result.Error != nil { - panic(result.Error) - } - - return photo -} -``` - -## 🎯 测试策略 - -### 测试金字塔 -1. **单元测试 (70%)** - - 测试单个函数或方法 - - 使用 Mock 对象隔离依赖 - - 快速执行,高覆盖率 - -2. **集成测试 (20%)** - - 测试模块间交互 - - 使用真实数据库 - - 验证接口契约 - -3. **端到端测试 (10%)** - - 测试完整用户流程 - - 使用真实环境 - - 验证业务逻辑 - -### 测试执行命令 -```bash -# 运行所有测试 -make test - -# 运行单元测试 -make test-unit - -# 运行集成测试 -make test-integration - -# 运行性能测试 -make test-benchmark - -# 生成测试覆盖率报告 -make test-coverage - -# 运行特定包的测试 -go test ./tests/unit/service/... - -# 运行性能测试 -go test -bench=. ./tests/benchmark/... - -# 生成详细的测试报告 -go test -v -race -coverprofile=coverage.out ./... -go tool cover -html=coverage.out -o coverage.html -``` - -## 💡 测试最佳实践 - -### 测试编写原则 -1. **AAA 模式**: Arrange, Act, Assert -2. **独立性**: 测试间不相互依赖 -3. **可重复**: 测试结果一致 -4. **快速执行**: 单元测试应快速完成 -5. **描述性**: 测试名称清晰描述意图 - -### Mock 使用建议 -1. **接口隔离**: 对外部依赖使用 Mock -2. **行为验证**: 验证方法调用和参数 -3. **状态验证**: 验证返回值和状态变化 -4. **Mock 生成**: 使用工具自动生成 Mock - -### 测试数据管理 -1. **固定数据**: 使用 fixtures 文件 -2. **随机数据**: 使用测试数据生成器 -3. **数据清理**: 每个测试后清理数据 -4. **事务回滚**: 使用事务确保数据隔离 - -本模块确保了代码质量和系统可靠性,是持续集成和交付的重要基础。 \ No newline at end of file diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 255b037..397c575 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -1,245 +1,73 @@ -# Backend API Service - CLAUDE.md +# Backend API - 开发指南 -本文件为 Claude Code 在后端 API 服务模块中工作时提供指导。 - -## 🎯 模块概览 - -这是一个基于 Go + go-zero 框架的 REST API 后端服务,利用 go-zero 生态工具快速开发,采用 go-zero 推荐的项目架构。 - -### 主要特性 -- 🏗️ go-zero 标准架构 (API → Logic → Model) -- 🚀 代码自动生成 (通过 .api 文件和 goctl 工具) -- 📊 多数据库支持 (PostgreSQL/SQLite 通过配置切换 + Redis) -- 🔐 JWT 认证 + 中间件系统 -- 📁 文件上传和存储管理 -- 🔗 链路追踪和监控 -- 🛡️ 熔断、限流、负载均衡 -- 📚 API 文档自动生成 - -### 技术栈 -- **语言**: Go 1.23+ -- **框架**: go-zero v1.8.0+ -- **数据库**: PostgreSQL/SQLite (通过配置切换) + Redis (缓存) -- **ORM**: GORM v1.30.0 (统一数据访问) -- **认证**: JWT (内置 JWT 支持) -- **日志**: go-zero 内置日志系统 -- **配置**: go-zero 配置系统 -- **工具**: goctl (代码生成工具) -- **容器化**: Docker + Docker Compose - -## 📁 go-zero 项目结构 (标准版) -``` -backend/ -├── CLAUDE.md # 📋 当前文件 - 后端总览 -├── go.mod # Go 模块文件 -├── go.sum # 依赖锁定文件 -├── Makefile # 构建脚本 -├── cmd/ # 🚀 应用入口目录 -│ ├── api/ # API 服务入口 -│ │ └── main.go # API 服务主函数 -│ ├── rpc/ # RPC 服务入口 (未来扩展) -│ │ └── main.go # RPC 服务主函数 -│ └── job/ # 任务服务入口 (未来扩展) -│ └── main.go # 任务服务主函数 -├── api/ # 🌐 API 定义目录 -│ ├── CLAUDE.md # API 模块开发指导 -│ └── desc/ # 📝 API 定义文件目录 -│ ├── photography.api # 📋 主 API 文件 (导入其他模块) -│ ├── user.api # 用户接口定义 -│ ├── photo.api # 照片接口定义 -│ ├── category.api # 分类接口定义 -│ ├── auth.api # 认证接口定义 -│ └── common.api # 公共类型定义 -├── etc/ # ⚙️ 配置文件目录 -│ ├── photographyapi-api.yaml # API 服务配置 -│ ├── photography-dev.yaml # 开发环境配置 -│ ├── photography-prod.yaml # 生产环境配置 -│ ├── photographyrpc.yaml # RPC 服务配置 (未来) -│ └── photographyjob.yaml # 任务服务配置 (未来) -├── internal/ # 📦 内部模块 (goctl 自动生成) -│ ├── config/ # 配置结构 -│ │ └── config.go # 配置定义 -│ ├── handler/ # 🎯 处理器 (goctl 自动生成) -│ ├── logic/ # 🧠 业务逻辑 (goctl 自动生成) -│ ├── svc/ # 🔧 服务上下文 -│ │ └── servicecontext.go # 服务上下文定义 -│ ├── types/ # 📦 类型定义 (goctl 自动生成) -│ │ └── types.go # 请求/响应类型 -│ ├── middleware/ # 🛡️ 中间件 -│ │ ├── auth.go # 认证中间件 -│ │ ├── cors.go # CORS 中间件 -│ │ └── logger.go # 日志中间件 -│ └── model/ # 📊 数据模型模块 (内部) -│ ├── CLAUDE.md # 数据模型开发指导 -│ ├── sql/ # SQL 定义文件 -│ │ ├── user.sql # 用户表结构 -│ │ ├── photo.sql # 照片表结构 -│ │ └── category.sql # 分类表结构 -│ ├── user.go # 用户模型 (goctl 自动生成) -│ ├── photo.go # 照片模型 (goctl 自动生成) -│ ├── category.go # 分类模型 (goctl 自动生成) -│ └── vars.go # 模型变量定义 -├── pkg/ # 📦 可导出包 (业务解耦) -│ ├── CLAUDE.md # 公共包开发指导 -│ ├── errorx/ # 错误处理包 -│ │ └── errorx.go # 统一错误定义 -│ ├── response/ # 响应处理包 -│ │ └── response.go # 统一响应格式 -│ ├── utils/ # 通用工具包 -│ │ ├── jwt/ # JWT 工具 -│ │ │ └── jwt.go # JWT 实现 -│ │ ├── hash/ # 哈希工具 -│ │ │ └── hash.go # 哈希实现 -│ │ ├── file/ # 文件处理工具 -│ │ │ └── file.go # 文件处理实现 -│ │ └── database/ # 数据库工具 -│ │ └── database.go # 数据库连接工厂 -│ └── constants/ # 常量定义包 -│ └── constants.go # 全局常量定义 -├── configs/ # 📋 静态配置文件目录 -│ ├── sql/ # SQL 初始化文件 -│ │ ├── init.sql # 数据库初始化 -│ │ └── seed.sql # 种子数据 -│ └── docker/ # Docker 相关配置 -│ ├── Dockerfile # Docker 镜像定义 -│ ├── docker-compose.yml # 本地开发环境 -│ └── docker-compose.prod.yml # 生产环境配置 -├── scripts/ # 🛠️ 脚本目录 -│ ├── build.sh # 构建脚本 -│ ├── deploy.sh # 部署脚本 -│ └── gen-code.sh # 代码生成脚本 -├── deploy/ # 🚀 部署配置 -│ ├── k8s/ # Kubernetes 配置 -│ └── systemd/ # Systemd 配置 -└── tests/ # 🧪 测试模块 (简化) - ├── CLAUDE.md # 测试开发指导 - ├── api_test.go # API 集成测试 - └── benchmark_test.go # 性能测试 -``` +> 🔧 Go + go-zero 后端API服务 ## 🚀 快速开始 -### 环境准备 ```bash -# 1. 安装 Go 1.23+ -go version +# 1. 进入目录 +cd backend/ -# 2. 安装 goctl 工具 -go install github.com/zeromicro/go-zero/tools/goctl@latest - -# 3. 验证安装 -goctl --version +# 2. 启动服务 +make run # 本地开发 +make docker-run # Docker运行 ``` -### 项目初始化 -```bash -# 1. 创建项目目录 -mkdir photography-backend && cd photography-backend - -# 2. 初始化 Go 模块 -go mod init photography-backend - -# 3. 创建 API 定义文件 -mkdir -p api/desc -``` - -### 快速开发流程 -```bash -# 1. 定义 API 接口 (api/desc/photography.api) -# 2. 生成 API 服务代码 -goctl api go -api api/desc/photography.api -dir ./ - -# 3. 定义数据库表结构 (internal/model/sql/*.sql) -# 4. 生成数据模型代码 -goctl model mysql ddl -src internal/model/sql/user.sql -dir internal/model/ - -# 5. 启动开发服务器 -go run cmd/api/main.go -f etc/photographyapi-api.yaml -``` - -### 多模块扩展 -当项目需要拆分为多个服务时,可以在 cmd 目录下添加不同的服务入口: - -```bash -# API 服务 (HTTP 接口) -go run cmd/api/main.go -f etc/photographyapi-api.yaml - -# RPC 服务 (内部调用) -go run cmd/rpc/main.go -f etc/photographyrpc.yaml - -# 任务服务 (定时任务/队列处理) -go run cmd/job/main.go -f etc/photographyjob.yaml -``` - -### 服务职责划分 -- **API 服务**: 对外提供 HTTP 接口,处理用户请求 -- **RPC 服务**: 内部服务间通信,提供核心业务逻辑 -- **任务服务**: 异步任务处理,如图片处理、邮件发送等 - -### 开发模式 -- **快速开发**: 使用 SQLite 进行本地开发,无需额外数据库 -- **生产模式**: 使用 PostgreSQL + Redis,完整的生产环境配置 -- **测试模式**: 使用内存数据库,用于单元测试和集成测试 - -## 🗄️ 数据库迁移系统 - -### 迁移系统架构 - -项目采用自定义的数据库迁移系统,提供完整的版本控制和回滚机制: +## 📁 核心目录 ``` -pkg/migration/ -├── migration.go # 迁移管理器核心逻辑 -├── migrations.go # 所有迁移定义 -└── README.md # 迁移开发指南 - -cmd/migrate/ -└── main.go # 命令行迁移工具 - -scripts/ -├── production-migrate.sh # 生产环境迁移脚本 -└── init-production-db.sh # 生产环境初始化脚本 - -docs/ -└── DATABASE_MIGRATION.md # 完整迁移文档 +backend/ +├── cmd/api/main.go # 服务入口 +├── api/desc/ # API定义文件 +├── internal/ # 业务代码 +│ ├── handler/ # 请求处理器 +│ ├── logic/ # 业务逻辑 +│ ├── model/ # 数据模型 +│ └── middleware/ # 中间件 +├── etc/ # 配置文件 +├── tests/ # 测试文件 +└── pkg/ # 公共库 ``` -### 快速迁移命令 +## ⚙️ 配置文件 -```bash -# 开发环境 -make migrate-status # 查看迁移状态 -make migrate-up # 运行所有迁移 -make migrate-down STEPS=1 # 回滚1步迁移 -make migrate-create NAME=xxx # 创建新迁移 -make db-backup # 创建备份 -make db-restore BACKUP=file # 恢复备份 - -# 生产环境 -./scripts/production-migrate.sh status # 查看状态 -./scripts/production-migrate.sh migrate # 执行迁移 -./scripts/production-migrate.sh -d migrate # 预览模式 -./scripts/init-production-db.sh # 全新初始化 +### 本地开发 (`etc/photographyapi-api.yaml`) +```yaml +Database: + driver: sqlite + file_path: ./data/photography.db ``` -### 迁移开发流程 +### 生产环境 (`etc/photography-api.yaml`) +```yaml +Database: + driver: postgres + host: postgres_db + port: 5432 + ssl_mode: disable +``` -1. **创建迁移**: `make migrate-create NAME="add_user_field"` -2. **编辑迁移**: 在 `pkg/migration/migrations.go` 中添加迁移定义 -3. **测试迁移**: `make migrate-up` 和 `make migrate-down STEPS=1` -4. **部署生产**: `./scripts/production-migrate.sh migrate` +## 🛠️ 常用命令 -### 特性 +| 命令 | 用途 | +|---|---| +| `make run` | 启动开发服务器 | +| `make build` | 构建二进制 | +| `make docker-build` | 构建Docker镜像 | +| `make migrate-up` | 运行数据库迁移 | +| `make test` | 运行测试 | -- ✅ **版本控制**: 时间戳版本号,确保迁移顺序 -- ✅ **回滚支持**: 每个迁移都支持安全回滚 -- ✅ **自动备份**: 生产环境迁移前自动备份 -- ✅ **SQLite 优化**: 针对 SQLite 限制的特殊处理 -- ✅ **事务安全**: 每个迁移在事务中执行 -- ✅ **状态跟踪**: 完整的迁移状态记录 -- ✅ **预览模式**: 支持预览迁移而不实际执行 -- ✅ **日志记录**: 详细的迁移日志和错误追踪 +## 🔗 API文档 +- 定义文件: `api/desc/` 目录 +- 在线测试: 启动后访问 http://localhost:8080 -详细使用方法请参考: [数据库迁移文档](docs/DATABASE_MIGRATION.md) +## 🗄️ 数据库 +- **开发**: SQLite (无需安装) +- **生产**: PostgreSQL +- **迁移**: 自动迁移系统 -本 CLAUDE.md 文件为后端开发提供了全面的指导,遵循 go-zero 框架的最佳实践。 \ No newline at end of file +## 📋 开发流程 +1. 修改API定义 → `api/desc/` +2. 生成代码 → `make generate` +3. 实现逻辑 → `internal/logic/` +4. 运行测试 → `make test` \ No newline at end of file diff --git a/backend/api/desc/CLAUDE.md b/backend/api/desc/CLAUDE.md new file mode 100644 index 0000000..b0bc85f --- /dev/null +++ b/backend/api/desc/CLAUDE.md @@ -0,0 +1,37 @@ +# API定义模块 + +## 📋 文件结构 +``` +api/desc/ +├── photography.api # 主API文件(聚合) +├── auth.api # 认证接口 +├── photo.api # 照片接口 +├── category.api # 分类接口 +├── user.api # 用户接口 +└── frontend/ # 前端专用接口 + ├── auth.api # 前端认证 + └── public.api # 公开接口 +``` + +## 🚀 开发流程 +1. **新增接口**: 编辑对应.api文件 +2. **生成代码**: `make generate-api` +3. **实现逻辑**: 在internal/logic/对应目录 + +## 🎯 文件作用 +| 文件 | 用途 | 对应功能 | +|---|---|---| +| `auth.api` | 登录/注册 | JWT认证 | +| `photo.api` | 照片CRUD | 上传/管理 | +| `category.api` | 分类管理 | 相册分类 | +| `frontend/auth.api` | 前端登录 | 用户认证 | +| `frontend/public.api` | 公开接口 | 无需认证 | + +## ⚙️ 生成命令 +```bash +# 生成所有API代码 +goctl api go -api api/desc/photography.api -dir ./ + +# 生成单个模块 +goctl api go -api api/desc/photo.api -dir ./internal/handler/photo +``` \ No newline at end of file diff --git a/backend/configs/CLAUDE.md b/backend/configs/CLAUDE.md new file mode 100644 index 0000000..4f93b32 --- /dev/null +++ b/backend/configs/CLAUDE.md @@ -0,0 +1,24 @@ +# 配置目录 + +## 📁 目录结构 +``` +configs/ +├── sql/ # 数据库脚本 +│ ├── init.sql # 初始化表结构 +│ └── seed.sql # 测试数据 +└── docker/ # Docker配置 + ├── Dockerfile + └── docker-compose.*.yml +``` + +## 🎯 用途说明 +- **sql/**: 数据库初始化相关 +- **docker/**: 容器化配置 + +## 🚀 快速使用 +```bash +# 运行SQL脚本 +cd configs/sql +make init-db # 初始化数据库 +make seed-data # 插入测试数据 +``` \ No newline at end of file diff --git a/backend/configs/sql/CLAUDE.md b/backend/configs/sql/CLAUDE.md new file mode 100644 index 0000000..eb7cd73 --- /dev/null +++ b/backend/configs/sql/CLAUDE.md @@ -0,0 +1,18 @@ +# SQL配置目录 + +## 📋 目录内容 +- `init.sql` - 数据库初始化脚本 +- `seed.sql` - 测试数据插入脚本 + +## 🚀 使用方法 +```bash +# 初始化数据库 +psql -U photography -d photography -f init.sql + +# 插入测试数据 +psql -U photography -d photography -f seed.sql +``` + +## 🎯 文件作用 +- **init.sql**: 创建初始表结构 +- **seed.sql**: 插入默认分类和测试照片数据 \ No newline at end of file diff --git a/backend/deploy/CLAUDE.md b/backend/deploy/CLAUDE.md new file mode 100644 index 0000000..7d42d1d --- /dev/null +++ b/backend/deploy/CLAUDE.md @@ -0,0 +1,32 @@ +# 部署配置目录 + +## 📁 目录结构 +``` +deploy/ +├── k8s/ # Kubernetes配置 +└── systemd/ # Systemd服务配置 +``` + +## 🎯 部署方式选择 +| 方式 | 目录 | 适用场景 | +|---|---|---| +| **Docker** | 根目录docker-compose.yml | 本地开发/测试 | +| **K8s** | deploy/k8s/ | 云原生部署 | +| **Systemd** | deploy/systemd/ | 裸机部署 | + +## 🚀 快速部署 +```bash +# 查看部署文档 +cd docs/deployment/ +cat CLAUDE.md + +# 使用Docker部署 +docker-compose up -d + +# 使用Systemd部署 +sudo systemctl start photography-api +``` + +## ⚙️ 配置文件 +- **k8s/**: Kubernetes YAML文件 +- **systemd/**: 系统服务配置模板 \ No newline at end of file diff --git a/backend/etc/photography-api.yaml b/backend/etc/photography-api.yaml index 5111a73..8e8bf0e 100644 --- a/backend/etc/photography-api.yaml +++ b/backend/etc/photography-api.yaml @@ -11,7 +11,7 @@ Database: Username: photography Password: fc34ewdc.d3we-s Charset: utf8mb4 - SSLMode: disable + ssl_mode: disable MaxOpenConns: 100 MaxIdleConns: 10 diff --git a/backend/internal/handler/CLAUDE.md b/backend/internal/handler/CLAUDE.md new file mode 100644 index 0000000..81d62ec --- /dev/null +++ b/backend/internal/handler/CLAUDE.md @@ -0,0 +1,40 @@ +# Handler层 - 请求处理器 + +## 📋 目录结构 +``` +internal/handler/ +├── auth/ # 认证处理器 +├── category/ # 分类处理器 +├── photo/ # 照片处理器 +├── user/ # 用户处理器 +├── health/ # 健康检查 +└── routes.go # 路由注册 +``` + +## 🎯 处理器职责 +- **接收请求**: HTTP请求入口 +- **参数验证**: 请求参数校验 +- **调用逻辑**: 调用logic层处理 +- **返回响应**: 格式化响应 + +## 🚀 开发流程 +1. **新增处理器**: 在对应目录创建handler文件 +2. **注册路由**: 在routes.go中添加路由 +3. **绑定逻辑**: 调用对应的logic层方法 + +## 📊 代码模板 +```go +// 示例处理器结构 +func (h *CreatePhotoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 1. 解析请求参数 + // 2. 调用logic层 + // 3. 返回响应 +} +``` + +## 🔗 对应关系 +| Handler | Logic | Model | +|---|---|---| +| `auth/` | `logic/auth/` | `model/user/` | +| `photo/` | `logic/photo/` | `model/photo/` | +| `category/` | `logic/category/` | `model/category/` | \ No newline at end of file diff --git a/backend/internal/logic/CLAUDE.md b/backend/internal/logic/CLAUDE.md new file mode 100644 index 0000000..db57ae3 --- /dev/null +++ b/backend/internal/logic/CLAUDE.md @@ -0,0 +1,43 @@ +# Logic层 - 业务逻辑 + +## 📋 目录结构 +``` +internal/logic/ +├── auth/ # 认证逻辑 +├── category/ # 分类业务逻辑 +├── photo/ # 照片业务逻辑 +├── user/ # 用户业务逻辑 +└── health/ # 健康检查逻辑 +``` + +## 🎯 逻辑层职责 +- **业务处理**: 核心业务逻辑实现 +- **数据验证**: 业务规则验证 +- **事务管理**: 数据库事务控制 +- **错误处理**: 业务异常处理 + +## 🚀 开发流程 +1. **实现接口**: 根据API定义实现逻辑 +2. **调用模型**: 使用model层操作数据 +3. **处理异常**: 返回标准化错误 + +## 📊 代码结构 +```go +// 业务逻辑模板 +func (l *CreatePhotoLogic) CreatePhoto(req types.CreatePhotoRequest) (*types.Photo, error) { + // 1. 业务验证 + // 2. 数据操作 + // 3. 返回结果 +} +``` + +## 🔗 层级关系 +``` +Handler → Logic → Model → Database +``` + +## 🎯 最佳实践 +- **单一职责**: 每个logic方法只做一件事 +- **错误处理**: 使用pkg/errorx定义错误 +- **日志记录**: 关键操作添加日志 +- **事务边界**: 复杂操作使用事务 \ No newline at end of file diff --git a/backend/internal/middleware/CLAUDE.md b/backend/internal/middleware/CLAUDE.md new file mode 100644 index 0000000..ed688fc --- /dev/null +++ b/backend/internal/middleware/CLAUDE.md @@ -0,0 +1,46 @@ +# Middleware层 - 中间件 + +## 📋 文件结构 +``` +internal/middleware/ +├── auth.go # JWT认证中间件 +├── cors.go # 跨域处理中间件 +├── logger.go # 日志中间件 +├── error.go # 错误处理中间件 +└── middleware.go # 中间件管理器 +``` + +## 🎯 中间件职责 +- **认证授权**: JWT令牌验证 +- **跨域处理**: CORS配置 +- **日志记录**: 请求日志 +- **错误处理**: 统一错误响应 +- **限流熔断**: 请求限流 + +## 🚀 中间件顺序 +``` +请求 → Logger → CORS → Auth → RateLimit → Handler +``` + +## 📊 配置参数 +| 中间件 | 配置项 | 默认值 | +|---|---|---| +| **CORS** | 允许域名 | localhost:3000 | +| **Logger** | 日志级别 | info | +| **Auth** | 过期时间 | 24小时 | + +## 🎯 使用方式 +```go +// 在路由中使用 +r.Use(middleware.Cors()) +r.Use(middleware.Logger()) +r.Use(middleware.Auth()) +``` + +## 🔄 扩展中间件 +1. **新增文件**: 创建新的中间件文件 +2. **注册使用**: 在routes.go中添加 +3. **配置参数**: 在配置文件中设置 + +## ⚙️ 配置位置 +- 配置项: `etc/photography-api.yaml` -> `middleware` 部分 \ No newline at end of file diff --git a/backend/internal/model/CLAUDE.md b/backend/internal/model/CLAUDE.md new file mode 100644 index 0000000..e519524 --- /dev/null +++ b/backend/internal/model/CLAUDE.md @@ -0,0 +1,49 @@ +# Model层 - 数据模型 + +## 📋 目录结构 +``` +internal/model/ +├── user.go # 用户模型 +├── photo.go # 照片模型 +├── category.go # 分类模型 +├── vars.go # 模型变量 +├── sql/ # SQL定义文件 +│ ├── user.sql # 用户表结构 +│ ├── photo.sql # 照片表结构 +│ └── category.sql # 分类表结构 +└── *_gen.go # 自动生成代码 +``` + +## 🎯 模型职责 +- **数据定义**: 数据库表结构 +- **CRUD操作**: 基础数据操作 +- **关联关系**: 表间关系定义 +- **验证规则**: 数据验证 + +## 🚀 开发流程 +1. **定义SQL**: 在sql/目录创建表结构 +2. **生成模型**: `make generate-model` +3. **扩展方法**: 在对应模型文件添加自定义方法 + +## 📊 表结构关系 +``` +User 1:N Photo (用户拥有多张照片) +Category 1:N Photo (分类包含多张照片) +``` + +## 🎯 文件说明 +| 文件 | 用途 | 修改频率 | +|---|---|---| +| `user.sql` | 用户表定义 | 低 | +| `photo.sql` | 照片表定义 | 中 | +| `category.sql` | 分类表定义 | 低 | +| `*_gen.go` | 自动生成代码 | 不修改 | + +## 🔄 生成命令 +```bash +# 生成所有模型 +make generate-model + +# 生成单个模型 +goctl model mysql ddl -src sql/user.sql -dir ./internal/model/ +``` \ No newline at end of file diff --git a/backend/internal/svc/CLAUDE.md b/backend/internal/svc/CLAUDE.md new file mode 100644 index 0000000..521b919 --- /dev/null +++ b/backend/internal/svc/CLAUDE.md @@ -0,0 +1,39 @@ +# Service Context - 服务上下文 + +## 📁 文件结构 +``` +internal/svc/ +└── servicecontext.go # 服务上下文定义 +``` + +## 🎯 核心职责 +- **服务初始化**: 集中管理所有服务依赖 +- **依赖注入**: 为handler提供所需组件 +- **生命周期**: 管理服务生命周期 + +## 🔧 组件管理 +```go +type ServiceContext struct { + Config config.Config // 配置 + DB *gorm.DB // 数据库连接 + UserModel model.UserModel // 用户模型 + PhotoModel model.PhotoModel // 照片模型 + CategoryModel model.CategoryModel // 分类模型 + Middleware *middleware.MiddlewareManager // 中间件管理 +} +``` + +## 🚀 初始化流程 +``` +启动服务 → 加载配置 → 初始化数据库 → 创建模型 → 注册中间件 → 启动HTTP服务 +``` + +## 📋 扩展方式 +1. **新增模型**: 在ServiceContext中添加新字段 +2. **初始化**: 在NewServiceContext中初始化 +3. **使用**: 在handler中通过svc访问 + +## 🎯 设计模式 +- **单例模式**: 整个服务生命周期只创建一次 +- **依赖注入**: 通过构造函数传递依赖 +- **门面模式**: 为上层提供统一的访问接口 \ No newline at end of file diff --git a/backend/pkg/CLAUDE.md b/backend/pkg/CLAUDE.md new file mode 100644 index 0000000..9a980b6 --- /dev/null +++ b/backend/pkg/CLAUDE.md @@ -0,0 +1,34 @@ +# 公共库目录 + +## 📦 包结构 +``` +pkg/ +├── constants/ # 全局常量 +├── errorx/ # 错误处理 +├── response/ # 响应格式 +└── utils/ # 工具集合 + ├── jwt/ # JWT工具 + ├── hash/ # 哈希工具 + ├── file/ # 文件处理 + └── database/ # 数据库工具 +``` + +## 🎯 设计原则 +- **可复用**: 项目间共享的通用功能 +- **无依赖**: 不依赖内部业务代码 +- **易测试**: 独立可测试的单元 + +## 🚀 快速使用 +```go +// 使用错误处理 +import "photography-backend/pkg/errorx" +err := errorx.New(errorx.CodeInvalidArgument) + +// 使用JWT工具 +import "photography-backend/pkg/utils/jwt" +token, err := jwt.GenerateToken(userID) + +// 使用响应格式 +import "photography-backend/pkg/response" +response.Success(c, data) +``` \ No newline at end of file diff --git a/backend/scripts/CLAUDE.md b/backend/scripts/CLAUDE.md new file mode 100644 index 0000000..95ae3fa --- /dev/null +++ b/backend/scripts/CLAUDE.md @@ -0,0 +1,34 @@ +# 脚本目录 + +## 🛠️ 脚本列表 +``` +scripts/ +├── init-production-db.sh # 生产数据库初始化 +├── production-db-setup.sh # 数据库配置 +├── production-migrate.sh # 生产环境迁移 +└── test-migration.sh # 测试迁移 +``` + +## 🎯 脚本用途 +| 脚本 | 用途 | +|---|---| +| `init-production-db.sh` | 首次部署数据库 | +| `production-migrate.sh` | 执行数据库迁移 | +| `test-migration.sh` | 本地测试迁移 | + +## 🚀 使用方式 +```bash +# 生产环境初始化 +./scripts/init-production-db.sh + +# 执行迁移 +./scripts/production-migrate.sh up + +# 测试迁移 +./scripts/test-migration.sh +``` + +## ⚠️ 注意事项 +- 生产脚本需要数据库连接权限 +- 迁移前建议备份数据 +- 本地测试使用SQLite \ No newline at end of file