feat: 完善模块化 CLAUDE.md 文档体系
- 新增 admin/CLAUDE.md - 管理后台开发指导文档 - 修正技术栈为 React + TypeScript + shadcn/ui - 提供完整的管理后台架构设计 - 包含照片管理、分类管理、日志管理等核心功能 - 详细的开发环境配置和部署指南 - 新增 backend/CLAUDE.md - 后端开发指导文档 - 基于 Golang + Gin + GORM 技术栈 - 完整的 API 接口设计和数据库架构 - 包含认证、权限、文件存储等核心功能 - 详细的部署和监控配置 - 新增 ui/CLAUDE.md - UI 备份模块管理文档 - 支持组件备份和 A/B 测试功能 - 详细的同步策略和实验环境配置 - 完整的版本管理和协作流程 - 更新 CLAUDE.md 根目录文档 - 完善模块选择指南和协调机制 - 新增模块间通信和依赖关系说明 - 优化文档维护和使用建议 - 建立完整的模块化开发规范 通过模块化设计最大限度减少 AI 幻觉,提高开发效率。
This commit is contained in:
107
CLAUDE.md
107
CLAUDE.md
@ -40,9 +40,14 @@ photography/
|
|||||||
├── .gitea/workflows/ # ⚙️ CI/CD 模块
|
├── .gitea/workflows/ # ⚙️ CI/CD 模块
|
||||||
│ ├── CLAUDE.md # CI/CD 配置指导
|
│ ├── CLAUDE.md # CI/CD 配置指导
|
||||||
│ └── deploy-frontend.yml # 自动部署工作流
|
│ └── deploy-frontend.yml # 自动部署工作流
|
||||||
├── admin/ # 📋 管理后台 (预留)
|
├── admin/ # 📋 管理后台模块
|
||||||
├── backend/ # 🔧 后端 API (预留)
|
│ └── CLAUDE.md # 管理后台开发指导
|
||||||
└── ui/ # 🎨 UI 组件备份
|
├── backend/ # 🔧 后端 API 模块
|
||||||
|
│ └── CLAUDE.md # 后端开发指导
|
||||||
|
├── ui/ # 🎨 UI 备份模块
|
||||||
|
│ └── CLAUDE.md # UI 备份模块管理
|
||||||
|
└── scripts/ # 🛠️ 工具脚本
|
||||||
|
└── README.md # 脚本说明
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 模块化工作指南
|
## 🎯 模块化工作指南
|
||||||
@ -73,6 +78,30 @@ cd .gitea/workflows/
|
|||||||
```
|
```
|
||||||
**适用场景**: 自动部署、构建流程、环境配置、工作流优化
|
**适用场景**: 自动部署、构建流程、环境配置、工作流优化
|
||||||
|
|
||||||
|
#### 📋 管理后台开发
|
||||||
|
```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
|
```bash
|
||||||
# 在根目录工作
|
# 在根目录工作
|
||||||
@ -168,6 +197,9 @@ git push origin main # 触发自动部署
|
|||||||
- **前端问题**: 查看 `frontend/CLAUDE.md`
|
- **前端问题**: 查看 `frontend/CLAUDE.md`
|
||||||
- **部署问题**: 查看 `docs/deployment/CLAUDE.md`
|
- **部署问题**: 查看 `docs/deployment/CLAUDE.md`
|
||||||
- **CI/CD 问题**: 查看 `.gitea/workflows/CLAUDE.md`
|
- **CI/CD 问题**: 查看 `.gitea/workflows/CLAUDE.md`
|
||||||
|
- **管理后台问题**: 查看 `admin/CLAUDE.md`
|
||||||
|
- **后端问题**: 查看 `backend/CLAUDE.md`
|
||||||
|
- **UI 备份问题**: 查看 `ui/CLAUDE.md`
|
||||||
|
|
||||||
### 通用问题
|
### 通用问题
|
||||||
```bash
|
```bash
|
||||||
@ -190,11 +222,16 @@ cd frontend && make clean && make install
|
|||||||
- ✅ 代码质量控制 (ESLint + Prettier + TypeScript)
|
- ✅ 代码质量控制 (ESLint + Prettier + TypeScript)
|
||||||
- ✅ Pre-commit hooks
|
- ✅ Pre-commit hooks
|
||||||
|
|
||||||
|
### 开发中功能
|
||||||
|
- 📋 管理后台 (架构设计完成)
|
||||||
|
- 📋 后端 API (架构设计完成)
|
||||||
|
- 📋 UI 备份系统 (已实现)
|
||||||
|
|
||||||
### 计划中功能
|
### 计划中功能
|
||||||
- 📋 管理后台
|
|
||||||
- 📋 后端 API
|
|
||||||
- 📋 多环境部署
|
- 📋 多环境部署
|
||||||
- 📋 性能监控
|
- 📋 性能监控
|
||||||
|
- 📋 AI 功能集成
|
||||||
|
- 📋 数据分析系统
|
||||||
|
|
||||||
## 🎨 模块协调原则
|
## 🎨 模块协调原则
|
||||||
|
|
||||||
@ -202,12 +239,18 @@ cd frontend && make clean && make install
|
|||||||
1. **前端模块** (`frontend/CLAUDE.md`): 组件、样式、前端逻辑变更时
|
1. **前端模块** (`frontend/CLAUDE.md`): 组件、样式、前端逻辑变更时
|
||||||
2. **部署模块** (`docs/deployment/CLAUDE.md`): 服务器、配置、部署流程变更时
|
2. **部署模块** (`docs/deployment/CLAUDE.md`): 服务器、配置、部署流程变更时
|
||||||
3. **CI/CD 模块** (`.gitea/workflows/CLAUDE.md`): 工作流、构建流程变更时
|
3. **CI/CD 模块** (`.gitea/workflows/CLAUDE.md`): 工作流、构建流程变更时
|
||||||
4. **根目录** (`CLAUDE.md`): 项目架构、模块关系变更时
|
4. **管理后台模块** (`admin/CLAUDE.md`): 后台功能、权限管理变更时
|
||||||
|
5. **后端模块** (`backend/CLAUDE.md`): API 接口、数据库架构变更时
|
||||||
|
6. **UI 备份模块** (`ui/CLAUDE.md`): 组件备份、实验功能变更时
|
||||||
|
7. **根目录** (`CLAUDE.md`): 项目架构、模块关系变更时
|
||||||
|
|
||||||
### 模块间通信
|
### 模块间通信
|
||||||
- 前端构建产物 → 部署模块使用
|
- **前端 ↔ 后端**: API 接口调用和数据交换
|
||||||
- CI/CD 协调 → 所有模块的构建和部署
|
- **管理后台 ↔ 后端**: 管理接口和数据操作
|
||||||
- 配置变更 → 相关模块的 CLAUDE.md 同步更新
|
- **UI 备份 ↔ 前端**: 组件同步和实验功能验证
|
||||||
|
- **前端构建产物 → 部署模块**: 静态文件部署
|
||||||
|
- **CI/CD 协调 → 所有模块**: 构建和部署流程
|
||||||
|
- **配置变更 → 相关模块**: CLAUDE.md 同步更新
|
||||||
|
|
||||||
## 🔄 最佳实践
|
## 🔄 最佳实践
|
||||||
|
|
||||||
@ -219,11 +262,45 @@ cd frontend && make clean && make install
|
|||||||
5. 提交代码触发自动部署
|
5. 提交代码触发自动部署
|
||||||
|
|
||||||
### 文档维护
|
### 文档维护
|
||||||
- 每个模块的 CLAUDE.md 保持独立和聚焦
|
- **模块独立**: 每个模块的 CLAUDE.md 保持独立和聚焦
|
||||||
- 模块间的依赖关系在根目录 CLAUDE.md 中说明
|
- **架构统一**: 模块间的依赖关系在根目录 CLAUDE.md 中说明
|
||||||
- 重要的全局配置统一在根目录管理
|
- **配置集中**: 重要的全局配置统一在根目录管理
|
||||||
|
- **及时更新**: 功能变更后立即更新对应的 CLAUDE.md
|
||||||
|
- **一致性**: 保持各模块文档的格式和风格一致
|
||||||
|
|
||||||
### 上下文优化
|
### 上下文优化
|
||||||
- Claude 工作时只需关注单个模块的 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. **测试集成功能**: 确保模块间的集成功能正常工作
|
||||||
690
admin/CLAUDE.md
Normal file
690
admin/CLAUDE.md
Normal file
@ -0,0 +1,690 @@
|
|||||||
|
# 管理后台模块 - CLAUDE.md
|
||||||
|
|
||||||
|
此文件为 Claude Code 在管理后台模块工作时提供指导。
|
||||||
|
|
||||||
|
## 🎯 模块概览
|
||||||
|
|
||||||
|
管理后台是摄影作品集项目的管理界面,用于内容管理、用户管理和系统配置。
|
||||||
|
|
||||||
|
### 功能特性
|
||||||
|
- 📸 照片管理:上传、编辑、删除照片
|
||||||
|
- 🏷️ 分类管理:相册分类、标签管理
|
||||||
|
- 👥 用户管理:用户权限、访问控制
|
||||||
|
- 📊 数据统计:访问统计、照片数据
|
||||||
|
- ⚙️ 系统配置:站点设置、主题配置
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **前端**: React + TypeScript + Vite
|
||||||
|
- **组件**: shadcn/ui + Radix UI
|
||||||
|
- **状态管理**: Zustand / React Query
|
||||||
|
- **路由**: React Router
|
||||||
|
- **认证**: JWT Token
|
||||||
|
- **构建**: Vite + TypeScript
|
||||||
|
|
||||||
|
## 📁 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
admin/
|
||||||
|
├── CLAUDE.md # 🔍 当前文件 - 管理后台指导
|
||||||
|
├── src/ # 源代码目录
|
||||||
|
│ ├── components/ # 组件目录
|
||||||
|
│ │ ├── ui/ # shadcn/ui 基础组件
|
||||||
|
│ │ ├── layout/ # 布局组件
|
||||||
|
│ │ ├── photo/ # 照片管理组件
|
||||||
|
│ │ ├── category/ # 分类管理组件
|
||||||
|
│ │ ├── logs/ # 日志管理组件
|
||||||
|
│ │ └── common/ # 通用组件
|
||||||
|
│ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── dashboard/ # 仪表板
|
||||||
|
│ │ ├── photos/ # 照片管理
|
||||||
|
│ │ ├── categories/ # 分类管理
|
||||||
|
│ │ ├── logs/ # 日志管理
|
||||||
|
│ │ └── settings/ # 系统设置
|
||||||
|
│ ├── hooks/ # 自定义 Hooks
|
||||||
|
│ ├── services/ # API 服务
|
||||||
|
│ ├── store/ # 状态管理
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ └── types/ # TypeScript 类型定义
|
||||||
|
├── public/ # 静态资源
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── vite.config.ts # Vite 配置
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
|
├── tailwind.config.js # Tailwind 配置
|
||||||
|
└── README.md # 模块说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 开发环境配置
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- **Node.js**: 18+
|
||||||
|
- **包管理器**: npm/yarn/pnpm
|
||||||
|
- **编辑器**: VS Code (推荐)
|
||||||
|
- **后端依赖**: Golang 后端 API 服务
|
||||||
|
|
||||||
|
### 初始化项目
|
||||||
|
```bash
|
||||||
|
# 进入管理后台目录
|
||||||
|
cd admin/
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
```bash
|
||||||
|
# .env.development
|
||||||
|
VITE_APP_TITLE=摄影作品集管理后台
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
VITE_UPLOAD_URL=http://localhost:8080/api/upload
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
VITE_APP_TITLE=摄影作品集管理后台
|
||||||
|
VITE_API_BASE_URL=https://api.photography.iriver.top
|
||||||
|
VITE_UPLOAD_URL=https://api.photography.iriver.top/upload
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 项目架构
|
||||||
|
|
||||||
|
### 目录结构详解
|
||||||
|
|
||||||
|
#### 🧩 组件架构
|
||||||
|
```
|
||||||
|
src/components/
|
||||||
|
├── ui/ # shadcn/ui 基础组件
|
||||||
|
│ ├── button.tsx # 按钮组件
|
||||||
|
│ ├── input.tsx # 输入框组件
|
||||||
|
│ ├── table.tsx # 表格组件
|
||||||
|
│ ├── modal.tsx # 弹窗组件
|
||||||
|
│ ├── card.tsx # 卡片组件
|
||||||
|
│ └── form.tsx # 表单组件
|
||||||
|
├── layout/ # 布局组件
|
||||||
|
│ ├── header.tsx # 顶部导航
|
||||||
|
│ ├── sidebar.tsx # 侧边栏
|
||||||
|
│ └── main-layout.tsx # 主布局
|
||||||
|
├── photo/ # 照片管理组件
|
||||||
|
│ ├── photo-list.tsx # 照片列表
|
||||||
|
│ ├── photo-form.tsx # 照片表单
|
||||||
|
│ ├── photo-upload.tsx # 照片上传
|
||||||
|
│ └── photo-detail.tsx # 照片详情
|
||||||
|
├── category/ # 分类管理组件
|
||||||
|
│ ├── category-tree.tsx # 分类树
|
||||||
|
│ ├── category-form.tsx # 分类表单
|
||||||
|
│ └── category-stats.tsx # 分类统计
|
||||||
|
├── logs/ # 日志管理组件
|
||||||
|
│ ├── log-viewer.tsx # 日志查看器
|
||||||
|
│ ├── log-filter.tsx # 日志过滤器
|
||||||
|
│ └── log-detail.tsx # 日志详情
|
||||||
|
└── common/ # 通用组件
|
||||||
|
├── loading.tsx # 加载组件
|
||||||
|
├── error-boundary.tsx # 错误边界
|
||||||
|
└── confirmation.tsx # 确认对话框
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📱 页面结构
|
||||||
|
```
|
||||||
|
src/pages/
|
||||||
|
├── dashboard/ # 仪表板
|
||||||
|
│ └── index.tsx # 首页
|
||||||
|
├── photos/ # 照片管理
|
||||||
|
│ ├── index.tsx # 照片列表
|
||||||
|
│ ├── edit.tsx # 照片编辑
|
||||||
|
│ └── upload.tsx # 照片上传
|
||||||
|
├── categories/ # 分类管理
|
||||||
|
│ └── index.tsx # 分类管理
|
||||||
|
├── tags/ # 标签管理
|
||||||
|
│ └── index.tsx # 标签管理
|
||||||
|
├── logs/ # 日志管理
|
||||||
|
│ ├── index.tsx # 日志列表
|
||||||
|
│ └── detail.tsx # 日志详情
|
||||||
|
└── settings/ # 系统设置
|
||||||
|
└── index.tsx # 系统设置
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔄 状态管理
|
||||||
|
```
|
||||||
|
src/store/
|
||||||
|
├── auth.ts # 认证状态 (Zustand)
|
||||||
|
├── photo.ts # 照片状态
|
||||||
|
├── logs.ts # 日志状态
|
||||||
|
└── ui.ts # UI 状态
|
||||||
|
|
||||||
|
src/hooks/
|
||||||
|
├── useAuth.ts # 认证 Hook
|
||||||
|
├── usePhotos.ts # 照片数据 (React Query)
|
||||||
|
├── useCategories.ts # 分类数据
|
||||||
|
├── useLogs.ts # 日志数据
|
||||||
|
└── useUpload.ts # 上传功能
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 API 集成
|
||||||
|
|
||||||
|
### API 接口规范
|
||||||
|
```typescript
|
||||||
|
// src/services/photo.ts
|
||||||
|
interface PhotoAPI {
|
||||||
|
// 获取照片列表
|
||||||
|
getPhotos(params: PhotoListParams): Promise<PhotoListResponse>
|
||||||
|
|
||||||
|
// 上传照片
|
||||||
|
uploadPhoto(file: File, metadata: PhotoMetadata): Promise<Photo>
|
||||||
|
|
||||||
|
// 更新照片信息
|
||||||
|
updatePhoto(id: string, data: PhotoUpdateData): Promise<Photo>
|
||||||
|
|
||||||
|
// 删除照片
|
||||||
|
deletePhoto(id: string): Promise<void>
|
||||||
|
|
||||||
|
// 批量操作
|
||||||
|
batchUpdatePhotos(ids: string[], data: Partial<Photo>): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/services/category.ts
|
||||||
|
interface CategoryAPI {
|
||||||
|
getCategories(): Promise<Category[]>
|
||||||
|
getCategoryTree(): Promise<CategoryTree[]>
|
||||||
|
createCategory(data: CategoryCreateData): Promise<Category>
|
||||||
|
updateCategory(id: string, data: CategoryUpdateData): Promise<Category>
|
||||||
|
deleteCategory(id: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/services/logs.ts
|
||||||
|
interface LogsAPI {
|
||||||
|
getLogs(params: LogListParams): Promise<LogListResponse>
|
||||||
|
getLogDetail(traceId: string): Promise<LogDetail>
|
||||||
|
getLogStats(): Promise<LogStats>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据类型定义
|
||||||
|
```typescript
|
||||||
|
// src/types/photo.ts
|
||||||
|
interface Photo {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
originalFilename: string
|
||||||
|
fileSize: number
|
||||||
|
status: 'processing' | 'published' | 'draft' | 'archived'
|
||||||
|
categories: Category[]
|
||||||
|
tags: Tag[]
|
||||||
|
formats: PhotoFormat[]
|
||||||
|
camera?: string
|
||||||
|
lens?: string
|
||||||
|
iso?: number
|
||||||
|
aperture?: string
|
||||||
|
shutterSpeed?: string
|
||||||
|
focalLength?: string
|
||||||
|
takenAt?: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/types/category.ts
|
||||||
|
interface Category {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
description: string
|
||||||
|
parentId?: string
|
||||||
|
sortOrder: number
|
||||||
|
isActive: boolean
|
||||||
|
photoCount: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/types/logs.ts
|
||||||
|
interface LogEntry {
|
||||||
|
id: string
|
||||||
|
timestamp: string
|
||||||
|
level: 'error' | 'warn' | 'info' | 'debug'
|
||||||
|
message: string
|
||||||
|
traceId: string
|
||||||
|
userId?: string
|
||||||
|
action?: string
|
||||||
|
details?: Record<string, any>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI 设计规范
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
- **一致性**: 保持界面元素的一致性
|
||||||
|
- **易用性**: 简洁直观的操作流程
|
||||||
|
- **响应式**: 适配不同屏幕尺寸
|
||||||
|
- **可访问性**: 支持键盘导航和屏幕阅读器
|
||||||
|
|
||||||
|
### 主题配置
|
||||||
|
```typescript
|
||||||
|
// tailwind.config.js
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#d4af37',
|
||||||
|
50: '#fefce8',
|
||||||
|
500: '#d4af37',
|
||||||
|
900: '#713f12'
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
muted: 'hsl(var(--muted))',
|
||||||
|
'muted-foreground': 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-animate')]
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/lib/utils.ts
|
||||||
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 权限管理
|
||||||
|
|
||||||
|
### 角色权限设计
|
||||||
|
```typescript
|
||||||
|
// src/types/permission.ts
|
||||||
|
interface Permission {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
permissions: Permission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
role: Role
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由保护
|
||||||
|
```typescript
|
||||||
|
// src/components/auth/ProtectedRoute.tsx
|
||||||
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
requiredRole?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
|
||||||
|
const { user, isLoading } = useAuth()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredRole && user.role !== requiredRole) {
|
||||||
|
return <Navigate to="/403" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/hooks/useAuth.ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null
|
||||||
|
token: string | null
|
||||||
|
login: (token: string, user: User) => void
|
||||||
|
logout: () => void
|
||||||
|
isLoggedIn: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isLoggedIn: false,
|
||||||
|
login: (token, user) => set({ token, user, isLoggedIn: true }),
|
||||||
|
logout: () => set({ token: null, user: null, isLoggedIn: false })
|
||||||
|
}),
|
||||||
|
{ name: 'auth-storage' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 构建和部署
|
||||||
|
|
||||||
|
### 开发命令
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 预览构建结果
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
### 部署配置
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
base: '/admin/', // 部署路径
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'static',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
||||||
|
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-select'],
|
||||||
|
'utils-vendor': ['clsx', 'tailwind-merge', 'date-fns']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发指南
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 使用 TypeScript 进行类型约束
|
||||||
|
- 遵循 React Hooks 最佳实践
|
||||||
|
- 组件命名采用 PascalCase
|
||||||
|
- 文件命名采用 kebab-case
|
||||||
|
- 使用 ESLint + Prettier 进行代码格式化
|
||||||
|
|
||||||
|
### 组件开发
|
||||||
|
```tsx
|
||||||
|
// src/components/photo/photo-list.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { usePhotos } from '@/hooks/usePhotos'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
|
interface PhotoListProps {
|
||||||
|
onPhotoSelect?: (photo: Photo) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhotoList({ onPhotoSelect }: PhotoListProps) {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
|
const { data: photosData, isLoading, error } = usePhotos({
|
||||||
|
search,
|
||||||
|
page,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
if (error) return <div>Error loading photos</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索照片..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<Button>+ 上传照片</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 照片网格 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{photosData?.photos.map((photo) => (
|
||||||
|
<Card
|
||||||
|
key={photo.id}
|
||||||
|
className="cursor-pointer hover:shadow-lg transition-shadow"
|
||||||
|
onClick={() => onPhotoSelect?.(photo)}
|
||||||
|
>
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<div className="aspect-square bg-gray-100 rounded-t-lg" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<CardTitle className="text-sm truncate">{photo.title}</CardTitle>
|
||||||
|
<div className="flex gap-1 mt-2">
|
||||||
|
<Badge variant={photo.status === 'published' ? 'default' : 'secondary'}>
|
||||||
|
{photo.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 请求封装
|
||||||
|
```typescript
|
||||||
|
// src/services/api.ts
|
||||||
|
import axios from 'axios'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const { token } = useAuthStore.getState()
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response.data,
|
||||||
|
(error) => {
|
||||||
|
const message = error.response?.data?.message || '请求失败'
|
||||||
|
toast.error(message)
|
||||||
|
|
||||||
|
// 401 错误自动跳转登录
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default api
|
||||||
|
|
||||||
|
// src/hooks/usePhotos.ts
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { photoService } from '@/services/photo'
|
||||||
|
|
||||||
|
export function usePhotos(params: PhotoListParams) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['photos', params],
|
||||||
|
queryFn: () => photoService.getPhotos(params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreatePhoto() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: photoService.createPhoto,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||||
|
toast.success('照片创建成功')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 与其他模块的集成
|
||||||
|
|
||||||
|
### 与前端展示的关系
|
||||||
|
- 管理后台修改的数据通过 API 影响前端展示
|
||||||
|
- 照片和相册的增删改查直接影响用户访问体验
|
||||||
|
- 主题设置和配置修改需要前端重新获取
|
||||||
|
|
||||||
|
### 与后端 API 的关系
|
||||||
|
- 依赖后端提供的 REST API 接口
|
||||||
|
- 需要后端提供认证和权限控制
|
||||||
|
- 文件上传需要后端存储支持
|
||||||
|
|
||||||
|
### 与部署模块的关系
|
||||||
|
- 构建产物需要部署到 Web 服务器
|
||||||
|
- 需要配置反向代理到管理后台路径
|
||||||
|
- 与前端项目共享域名和 SSL 证书
|
||||||
|
|
||||||
|
## 🐛 问题排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **登录失败**: 检查 API 地址和 Token 存储
|
||||||
|
2. **图片上传失败**: 检查文件大小和格式限制
|
||||||
|
3. **权限错误**: 检查用户角色和权限配置
|
||||||
|
4. **页面空白**: 检查路由配置和组件导入
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
```bash
|
||||||
|
# 查看开发环境变量
|
||||||
|
npm run dev --debug
|
||||||
|
|
||||||
|
# 查看构建详情
|
||||||
|
npm run build --debug
|
||||||
|
|
||||||
|
# 分析构建产物
|
||||||
|
npm run analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 性能优化
|
||||||
|
|
||||||
|
### 代码分割
|
||||||
|
```typescript
|
||||||
|
// 路由懒加载
|
||||||
|
const PhotoList = () => import('@/views/photos/PhotoList.vue')
|
||||||
|
const AlbumList = () => import('@/views/albums/AlbumList.vue')
|
||||||
|
|
||||||
|
// 组件懒加载
|
||||||
|
const LazyComponent = defineAsyncComponent(() => import('./Component.vue'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存策略
|
||||||
|
```typescript
|
||||||
|
// src/lib/query-client.ts
|
||||||
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5 * 60 * 1000, // 5分钟缓存
|
||||||
|
cacheTime: 10 * 60 * 1000, // 10分钟
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: (failureCount, error: any) => {
|
||||||
|
if (error?.response?.status === 404) return false
|
||||||
|
return failureCount < 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
export function usePhotos(params: PhotoListParams) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['photos', params],
|
||||||
|
queryFn: () => photoService.getPhotos(params),
|
||||||
|
staleTime: 5 * 60 * 1000 // 5分钟缓存
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 未来规划
|
||||||
|
|
||||||
|
### 功能扩展
|
||||||
|
- 📊 高级数据分析和报表
|
||||||
|
- 🔄 批量操作和导入导出
|
||||||
|
- 📱 移动端管理应用
|
||||||
|
- 🎨 可视化主题编辑器
|
||||||
|
- 🔌 插件系统支持
|
||||||
|
|
||||||
|
### 技术升级
|
||||||
|
- 升级到 React 19 最新特性
|
||||||
|
- 引入 React Server Components
|
||||||
|
- 支持 PWA 离线访问
|
||||||
|
- 集成 AI 辅助功能
|
||||||
|
- 支持 Micro Frontend 架构
|
||||||
|
|
||||||
|
## 📚 参考资料
|
||||||
|
|
||||||
|
- [React 官方文档](https://react.dev/)
|
||||||
|
- [shadcn/ui 文档](https://ui.shadcn.com/)
|
||||||
|
- [Radix UI 文档](https://radix-ui.com/)
|
||||||
|
- [Tailwind CSS 文档](https://tailwindcss.com/)
|
||||||
|
- [React Query 文档](https://tanstack.com/query/)
|
||||||
|
- [Zustand 文档](https://zustand-demo.pmnd.rs/)
|
||||||
|
- [Vite 配置指南](https://vitejs.dev/)
|
||||||
|
- [TypeScript 最佳实践](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
💡 **开发提示**: 在开始开发前,请确保已经阅读根目录的 CLAUDE.md 文件,了解项目整体架构和模块间的关系。开发过程中遇到问题,可以参考对应模块的 CLAUDE.md 文件寻找解决方案。
|
||||||
994
backend/CLAUDE.md
Normal file
994
backend/CLAUDE.md
Normal file
@ -0,0 +1,994 @@
|
|||||||
|
# 后端模块 - CLAUDE.md
|
||||||
|
|
||||||
|
此文件为 Claude Code 在后端模块工作时提供指导。
|
||||||
|
|
||||||
|
## 🎯 模块概览
|
||||||
|
|
||||||
|
后端模块提供摄影作品集项目的 API 服务,包括照片管理、用户认证、数据存储等核心功能。
|
||||||
|
|
||||||
|
### 功能特性
|
||||||
|
- 🔐 用户认证和授权管理
|
||||||
|
- 📸 照片和相册 CRUD 操作
|
||||||
|
- 📁 文件上传和存储管理
|
||||||
|
- 🎨 主题和配置管理
|
||||||
|
- 📊 访问统计和数据分析
|
||||||
|
- 🔄 数据同步和缓存
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **语言**: Go 1.21+
|
||||||
|
- **框架**: Gin + GORM
|
||||||
|
- **数据库**: PostgreSQL / MySQL
|
||||||
|
- **缓存**: Redis
|
||||||
|
- **存储**: MinIO / AWS S3
|
||||||
|
- **认证**: JWT Token
|
||||||
|
- **部署**: Docker + Docker Compose
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── CLAUDE.md # 🔍 当前文件 - 后端开发指导
|
||||||
|
├── cmd/ # 应用入口
|
||||||
|
│ └── server/ # 服务器启动
|
||||||
|
│ └── main.go
|
||||||
|
├── internal/ # 内部模块
|
||||||
|
│ ├── api/ # API 层
|
||||||
|
│ │ ├── handlers/ # 请求处理器
|
||||||
|
│ │ ├── middleware/ # 中间件
|
||||||
|
│ │ └── routes/ # 路由定义
|
||||||
|
│ ├── service/ # 业务逻辑层
|
||||||
|
│ │ ├── auth/ # 认证服务
|
||||||
|
│ │ ├── photo/ # 照片服务
|
||||||
|
│ │ ├── album/ # 相册服务
|
||||||
|
│ │ └── user/ # 用户服务
|
||||||
|
│ ├── repository/ # 数据访问层
|
||||||
|
│ │ ├── postgres/ # PostgreSQL 实现
|
||||||
|
│ │ └── redis/ # Redis 实现
|
||||||
|
│ ├── models/ # 数据模型
|
||||||
|
│ ├── config/ # 配置管理
|
||||||
|
│ └── utils/ # 工具函数
|
||||||
|
├── pkg/ # 公共包
|
||||||
|
│ ├── logger/ # 日志包
|
||||||
|
│ ├── validator/ # 验证包
|
||||||
|
│ └── response/ # 响应包
|
||||||
|
├── migrations/ # 数据库迁移
|
||||||
|
├── docs/ # API 文档
|
||||||
|
├── scripts/ # 部署脚本
|
||||||
|
├── configs/ # 配置文件
|
||||||
|
│ ├── config.yaml # 主配置
|
||||||
|
│ ├── config.dev.yaml # 开发环境
|
||||||
|
│ └── config.prod.yaml # 生产环境
|
||||||
|
├── docker-compose.yml # 容器编排
|
||||||
|
├── Dockerfile # 容器构建
|
||||||
|
├── Makefile # 构建脚本
|
||||||
|
├── go.mod # Go 模块定义
|
||||||
|
├── go.sum # 依赖校验
|
||||||
|
└── README.md # 模块说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 开发环境配置
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- **Go**: 1.21+
|
||||||
|
- **Docker**: 20.10+
|
||||||
|
- **Docker Compose**: 2.0+
|
||||||
|
- **PostgreSQL**: 14+
|
||||||
|
- **Redis**: 6.2+
|
||||||
|
|
||||||
|
### 快速启动
|
||||||
|
```bash
|
||||||
|
# 进入后端目录
|
||||||
|
cd backend/
|
||||||
|
|
||||||
|
# 启动开发环境 (Docker)
|
||||||
|
make dev-up
|
||||||
|
|
||||||
|
# 运行数据库迁移
|
||||||
|
make migrate
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# 或者直接运行
|
||||||
|
go run cmd/server/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_NAME=photography
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
|
||||||
|
# 文件存储配置
|
||||||
|
STORAGE_TYPE=local # local, s3, minio
|
||||||
|
STORAGE_PATH=./uploads
|
||||||
|
AWS_REGION=us-east-1
|
||||||
|
AWS_BUCKET=photography-bucket
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 项目架构
|
||||||
|
|
||||||
|
### 分层架构
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ API Layer │ ← handlers, middleware, routes
|
||||||
|
├─────────────────┤
|
||||||
|
│ Service Layer │ ← business logic
|
||||||
|
├─────────────────┤
|
||||||
|
│Repository Layer │ ← data access
|
||||||
|
├─────────────────┤
|
||||||
|
│ Models Layer │ ← data structures
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 目录结构详解
|
||||||
|
|
||||||
|
#### 🎯 API 层
|
||||||
|
```
|
||||||
|
internal/api/
|
||||||
|
├── handlers/ # 请求处理器
|
||||||
|
│ ├── auth.go # 认证相关
|
||||||
|
│ ├── photo.go # 照片管理
|
||||||
|
│ ├── album.go # 相册管理
|
||||||
|
│ ├── user.go # 用户管理
|
||||||
|
│ └── upload.go # 文件上传
|
||||||
|
├── middleware/ # 中间件
|
||||||
|
│ ├── auth.go # 认证中间件
|
||||||
|
│ ├── cors.go # CORS 处理
|
||||||
|
│ ├── logger.go # 日志中间件
|
||||||
|
│ └── rate_limit.go # 限流中间件
|
||||||
|
└── routes/ # 路由定义
|
||||||
|
├── api_v1.go # API v1 路由
|
||||||
|
└── admin.go # 管理后台路由
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 💼 业务逻辑层
|
||||||
|
```
|
||||||
|
internal/service/
|
||||||
|
├── auth/ # 认证服务
|
||||||
|
│ ├── auth.go # 认证逻辑
|
||||||
|
│ ├── jwt.go # JWT 处理
|
||||||
|
│ └── permission.go # 权限控制
|
||||||
|
├── photo/ # 照片服务
|
||||||
|
│ ├── photo.go # 照片操作
|
||||||
|
│ ├── metadata.go # 元数据处理
|
||||||
|
│ └── thumbnail.go # 缩略图生成
|
||||||
|
├── album/ # 相册服务
|
||||||
|
│ ├── album.go # 相册操作
|
||||||
|
│ └── organization.go # 组织管理
|
||||||
|
└── user/ # 用户服务
|
||||||
|
├── user.go # 用户管理
|
||||||
|
└── profile.go # 用户资料
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🗄️ 数据访问层
|
||||||
|
```
|
||||||
|
internal/repository/
|
||||||
|
├── postgres/ # PostgreSQL 实现
|
||||||
|
│ ├── photo.go # 照片数据操作
|
||||||
|
│ ├── album.go # 相册数据操作
|
||||||
|
│ ├── user.go # 用户数据操作
|
||||||
|
│ └── migration.go # 数据迁移
|
||||||
|
└── redis/ # Redis 实现
|
||||||
|
├── cache.go # 缓存操作
|
||||||
|
└── session.go # 会话管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗃️ 数据库设计
|
||||||
|
|
||||||
|
### 数据模型定义
|
||||||
|
```go
|
||||||
|
// internal/models/photo.go
|
||||||
|
type Photo struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Title string `gorm:"not null" json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
URL string `gorm:"not null" json:"url"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
AlbumID uint `json:"album_id"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Tags []Tag `gorm:"many2many:photo_tags" json:"tags"`
|
||||||
|
Metadata PhotoMetadata `gorm:"type:jsonb" json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/models/album.go
|
||||||
|
type Album struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CoverPhotoID *uint `json:"cover_photo_id"`
|
||||||
|
CoverPhoto *Photo `gorm:"foreignKey:CoverPhotoID" json:"cover_photo"`
|
||||||
|
Photos []Photo `gorm:"foreignKey:AlbumID" json:"photos"`
|
||||||
|
IsPublic bool `gorm:"default:true" json:"is_public"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/models/user.go
|
||||||
|
type User struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Username string `gorm:"unique;not null" json:"username"`
|
||||||
|
Email string `gorm:"unique;not null" json:"email"`
|
||||||
|
Password string `gorm:"not null" json:"-"`
|
||||||
|
Role string `gorm:"default:user" json:"role"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
Profile UserProfile `gorm:"foreignKey:UserID" json:"profile"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
```sql
|
||||||
|
-- migrations/001_create_users.sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
role VARCHAR(20) DEFAULT 'user',
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrations/002_create_albums.sql
|
||||||
|
CREATE TABLE albums (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
cover_photo_id INTEGER,
|
||||||
|
is_public BOOLEAN DEFAULT true,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrations/003_create_photos.sql
|
||||||
|
CREATE TABLE photos (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
url VARCHAR(500) NOT NULL,
|
||||||
|
thumbnail_url VARCHAR(500),
|
||||||
|
album_id INTEGER REFERENCES albums(id),
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
metadata JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 API 接口设计
|
||||||
|
|
||||||
|
### RESTful API 规范
|
||||||
|
```go
|
||||||
|
// internal/api/handlers/photo.go
|
||||||
|
type PhotoHandler struct {
|
||||||
|
photoService *service.PhotoService
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/photos
|
||||||
|
func (h *PhotoHandler) GetPhotos(c *gin.Context) {
|
||||||
|
params := &service.PhotoListParams{}
|
||||||
|
if err := c.ShouldBindQuery(params); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
photos, total, err := h.photoService.GetPhotos(params)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"data": photos,
|
||||||
|
"total": total,
|
||||||
|
"page": params.Page,
|
||||||
|
"per_page": params.PerPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/photos
|
||||||
|
func (h *PhotoHandler) CreatePhoto(c *gin.Context) {
|
||||||
|
var req service.CreatePhotoRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.GetUint("user_id")
|
||||||
|
req.UserID = userID
|
||||||
|
|
||||||
|
photo, err := h.photoService.CreatePhoto(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(201, gin.H{"data": photo})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/v1/photos/:id
|
||||||
|
func (h *PhotoHandler) UpdatePhoto(c *gin.Context) {
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
|
||||||
|
var req service.UpdatePhotoRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
photo, err := h.photoService.UpdatePhoto(uint(id), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"data": photo})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/photos/:id
|
||||||
|
func (h *PhotoHandler) DeletePhoto(c *gin.Context) {
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
|
||||||
|
err := h.photoService.DeletePhoto(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(204, nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 路由配置
|
||||||
|
```go
|
||||||
|
// internal/api/routes/api_v1.go
|
||||||
|
func SetupAPIV1Routes(r *gin.Engine, handlers *Handlers) {
|
||||||
|
api := r.Group("/api/v1")
|
||||||
|
|
||||||
|
// 公开接口
|
||||||
|
api.GET("/photos", handlers.Photo.GetPhotos)
|
||||||
|
api.GET("/photos/:id", handlers.Photo.GetPhoto)
|
||||||
|
api.GET("/albums", handlers.Album.GetAlbums)
|
||||||
|
api.GET("/albums/:id", handlers.Album.GetAlbum)
|
||||||
|
|
||||||
|
// 认证相关
|
||||||
|
auth := api.Group("/auth")
|
||||||
|
{
|
||||||
|
auth.POST("/login", handlers.Auth.Login)
|
||||||
|
auth.POST("/register", handlers.Auth.Register)
|
||||||
|
auth.POST("/refresh", handlers.Auth.RefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要认证的接口
|
||||||
|
protected := api.Group("/", middleware.AuthRequired())
|
||||||
|
{
|
||||||
|
protected.POST("/photos", handlers.Photo.CreatePhoto)
|
||||||
|
protected.PUT("/photos/:id", handlers.Photo.UpdatePhoto)
|
||||||
|
protected.DELETE("/photos/:id", handlers.Photo.DeletePhoto)
|
||||||
|
protected.POST("/upload", handlers.Upload.UploadFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员接口
|
||||||
|
admin := api.Group("/admin", middleware.AdminRequired())
|
||||||
|
{
|
||||||
|
admin.GET("/users", handlers.User.GetUsers)
|
||||||
|
admin.PUT("/users/:id", handlers.User.UpdateUser)
|
||||||
|
admin.DELETE("/users/:id", handlers.User.DeleteUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 认证和授权
|
||||||
|
|
||||||
|
### JWT 认证实现
|
||||||
|
```go
|
||||||
|
// internal/service/auth/jwt.go
|
||||||
|
type JWTService struct {
|
||||||
|
secretKey []byte
|
||||||
|
expiresIn time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTService(secretKey string, expiresIn time.Duration) *JWTService {
|
||||||
|
return &JWTService{
|
||||||
|
secretKey: []byte(secretKey),
|
||||||
|
expiresIn: expiresIn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWTService) GenerateToken(userID uint, role string) (string, error) {
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"user_id": userID,
|
||||||
|
"role": role,
|
||||||
|
"exp": time.Now().Add(s.expiresIn).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(s.secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWTService) ValidateToken(tokenString string) (*jwt.Token, error) {
|
||||||
|
return jwt.Parse(tokenString, 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限控制中间件
|
||||||
|
```go
|
||||||
|
// internal/api/middleware/auth.go
|
||||||
|
func AuthRequired() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
tokenString := c.GetHeader("Authorization")
|
||||||
|
if tokenString == "" {
|
||||||
|
c.JSON(401, gin.H{"error": "Authorization header required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 "Bearer " 前缀
|
||||||
|
if strings.HasPrefix(tokenString, "Bearer ") {
|
||||||
|
tokenString = tokenString[7:]
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwtService.ValidateToken(tokenString)
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(401, gin.H{"error": "Invalid token claims"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("user_id", uint(claims["user_id"].(float64)))
|
||||||
|
c.Set("role", claims["role"].(string))
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminRequired() gin.HandlerFunc {
|
||||||
|
return gin.HandlerFunc(func(c *gin.Context) {
|
||||||
|
AuthRequired()(c)
|
||||||
|
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
role := c.GetString("role")
|
||||||
|
if role != "admin" {
|
||||||
|
c.JSON(403, gin.H{"error": "Admin access required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 文件存储管理
|
||||||
|
|
||||||
|
### 存储接口设计
|
||||||
|
```go
|
||||||
|
// internal/service/storage/interface.go
|
||||||
|
type StorageService interface {
|
||||||
|
Upload(file multipart.File, filename string) (string, error)
|
||||||
|
Delete(filename string) error
|
||||||
|
GetURL(filename string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/service/storage/local.go
|
||||||
|
type LocalStorage struct {
|
||||||
|
basePath string
|
||||||
|
baseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocalStorage) Upload(file multipart.File, filename string) (string, error) {
|
||||||
|
filepath := path.Join(s.basePath, filename)
|
||||||
|
|
||||||
|
out, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetURL(filename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocalStorage) GetURL(filename string) string {
|
||||||
|
return fmt.Sprintf("%s/%s", s.baseURL, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/service/storage/s3.go
|
||||||
|
type S3Storage struct {
|
||||||
|
client *s3.Client
|
||||||
|
bucket string
|
||||||
|
region string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) Upload(file multipart.File, filename string) (string, error) {
|
||||||
|
_, err := s.client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(filename),
|
||||||
|
Body: file,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetURL(filename), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 图片处理
|
||||||
|
```go
|
||||||
|
// internal/service/photo/thumbnail.go
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateThumbnail(src image.Image, width, height int) (image.Image, error) {
|
||||||
|
bounds := src.Bounds()
|
||||||
|
srcWidth := bounds.Max.X
|
||||||
|
srcHeight := bounds.Max.Y
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
scaleX := float64(width) / float64(srcWidth)
|
||||||
|
scaleY := float64(height) / float64(srcHeight)
|
||||||
|
scale := math.Min(scaleX, scaleY)
|
||||||
|
|
||||||
|
newWidth := int(float64(srcWidth) * scale)
|
||||||
|
newHeight := int(float64(srcHeight) * scale)
|
||||||
|
|
||||||
|
// 创建新图像
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||||||
|
|
||||||
|
// 缩放图像
|
||||||
|
draw.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
|
||||||
|
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 构建和部署
|
||||||
|
|
||||||
|
### Makefile 配置
|
||||||
|
```makefile
|
||||||
|
# Makefile
|
||||||
|
.PHONY: build run test clean docker-build docker-run
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
build:
|
||||||
|
go build -o bin/server cmd/server/main.go
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
run:
|
||||||
|
go run cmd/server/main.go
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
||||||
|
|
||||||
|
# 数据库迁移
|
||||||
|
migrate:
|
||||||
|
migrate -path migrations -database "postgres://user:pass@localhost/dbname?sslmode=disable" up
|
||||||
|
|
||||||
|
# Docker 构建
|
||||||
|
docker-build:
|
||||||
|
docker build -t photography-backend .
|
||||||
|
|
||||||
|
# Docker 运行
|
||||||
|
docker-run:
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 开发环境
|
||||||
|
dev-up:
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
dev-down:
|
||||||
|
docker-compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# 生产环境
|
||||||
|
prod-up:
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
prod-down:
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 配置
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
COPY --from=builder /app/configs ./configs
|
||||||
|
COPY --from=builder /app/migrations ./migrations
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["./main"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose 配置
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- DB_HOST=postgres
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_USER=postgres
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
- DB_NAME=photography
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./configs:/app/configs
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:14
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=photography
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6.2
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发指南
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 使用 Go 标准格式化工具 `go fmt`
|
||||||
|
- 遵循 Go 命名规范
|
||||||
|
- 使用 `golangci-lint` 进行代码检查
|
||||||
|
- 编写单元测试,覆盖率不低于 80%
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
```go
|
||||||
|
// pkg/response/response.go
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Success(data interface{}) *Response {
|
||||||
|
return &Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: "success",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(code int, message string) *Response {
|
||||||
|
return &Response{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/utils/errors.go
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
ErrPhotoNotFound = errors.New("photo not found")
|
||||||
|
ErrAlbumNotFound = errors.New("album not found")
|
||||||
|
ErrInvalidPassword = errors.New("invalid password")
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
ErrPermissionDenied = errors.New("permission denied")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志管理
|
||||||
|
```go
|
||||||
|
// pkg/logger/logger.go
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Logger *zap.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config := zap.NewProductionConfig()
|
||||||
|
config.EncoderConfig.TimeKey = "timestamp"
|
||||||
|
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||||
|
|
||||||
|
Logger, _ = config.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(msg string, fields ...zap.Field) {
|
||||||
|
Logger.Info(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string, fields ...zap.Field) {
|
||||||
|
Logger.Error(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(msg string, fields ...zap.Field) {
|
||||||
|
Logger.Debug(msg, fields...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试编写
|
||||||
|
```go
|
||||||
|
// internal/service/photo/photo_test.go
|
||||||
|
func TestPhotoService_CreatePhoto(t *testing.T) {
|
||||||
|
mockRepo := &MockPhotoRepository{}
|
||||||
|
service := NewPhotoService(mockRepo)
|
||||||
|
|
||||||
|
req := &CreatePhotoRequest{
|
||||||
|
Title: "Test Photo",
|
||||||
|
Description: "Test Description",
|
||||||
|
URL: "https://example.com/photo.jpg",
|
||||||
|
AlbumID: 1,
|
||||||
|
UserID: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPhoto := &models.Photo{
|
||||||
|
ID: 1,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
URL: req.URL,
|
||||||
|
AlbumID: req.AlbumID,
|
||||||
|
UserID: req.UserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockRepo.On("Create", mock.AnythingOfType("*models.Photo")).Return(expectedPhoto, nil)
|
||||||
|
|
||||||
|
photo, err := service.CreatePhoto(req)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedPhoto.Title, photo.Title)
|
||||||
|
assert.Equal(t, expectedPhoto.Description, photo.Description)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 与其他模块的集成
|
||||||
|
|
||||||
|
### 与前端的集成
|
||||||
|
- 提供 RESTful API 供前端调用
|
||||||
|
- 支持 CORS 跨域请求
|
||||||
|
- 返回标准化的 JSON 响应格式
|
||||||
|
- 提供 API 文档 (Swagger)
|
||||||
|
|
||||||
|
### 与管理后台的集成
|
||||||
|
- 提供管理员专用的 API 接口
|
||||||
|
- 支持批量操作和数据导入导出
|
||||||
|
- 提供系统监控和统计信息
|
||||||
|
- 支持权限管理和用户管理
|
||||||
|
|
||||||
|
### 与部署模块的集成
|
||||||
|
- 提供 Docker 容器化部署
|
||||||
|
- 支持负载均衡和水平扩展
|
||||||
|
- 集成健康检查和监控
|
||||||
|
- 支持优雅关闭和重启
|
||||||
|
|
||||||
|
## 🐛 问题排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **数据库连接失败**: 检查数据库配置和网络连接
|
||||||
|
2. **JWT 认证失败**: 检查密钥配置和 Token 格式
|
||||||
|
3. **文件上传失败**: 检查存储配置和权限设置
|
||||||
|
4. **API 响应慢**: 检查数据库查询和缓存配置
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
```bash
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f backend
|
||||||
|
|
||||||
|
# 进入容器
|
||||||
|
docker-compose exec backend sh
|
||||||
|
|
||||||
|
# 数据库查询
|
||||||
|
docker-compose exec postgres psql -U postgres -d photography
|
||||||
|
|
||||||
|
# Redis 查询
|
||||||
|
docker-compose exec redis redis-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能优化
|
||||||
|
|
||||||
|
### 数据库优化
|
||||||
|
```sql
|
||||||
|
-- 添加索引
|
||||||
|
CREATE INDEX idx_photos_album_id ON photos(album_id);
|
||||||
|
CREATE INDEX idx_photos_user_id ON photos(user_id);
|
||||||
|
CREATE INDEX idx_photos_created_at ON photos(created_at);
|
||||||
|
|
||||||
|
-- 查询优化
|
||||||
|
EXPLAIN ANALYZE SELECT * FROM photos WHERE album_id = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存策略
|
||||||
|
```go
|
||||||
|
// internal/service/photo/photo.go
|
||||||
|
func (s *PhotoService) GetPhoto(id uint) (*models.Photo, error) {
|
||||||
|
cacheKey := fmt.Sprintf("photo:%d", id)
|
||||||
|
|
||||||
|
// 从缓存获取
|
||||||
|
if cached, err := s.cache.Get(cacheKey); err == nil {
|
||||||
|
var photo models.Photo
|
||||||
|
if err := json.Unmarshal(cached, &photo); err == nil {
|
||||||
|
return &photo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取
|
||||||
|
photo, err := s.repo.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存入缓存
|
||||||
|
if data, err := json.Marshal(photo); err == nil {
|
||||||
|
s.cache.Set(cacheKey, data, 5*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
return photo, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 监控和日志
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
```go
|
||||||
|
// internal/api/handlers/health.go
|
||||||
|
func (h *HealthHandler) Check(c *gin.Context) {
|
||||||
|
status := gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库连接
|
||||||
|
if err := h.db.Ping(); err != nil {
|
||||||
|
status["database"] = "error"
|
||||||
|
status["status"] = "error"
|
||||||
|
} else {
|
||||||
|
status["database"] = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 Redis 连接
|
||||||
|
if err := h.redis.Ping().Err(); err != nil {
|
||||||
|
status["redis"] = "error"
|
||||||
|
status["status"] = "error"
|
||||||
|
} else {
|
||||||
|
status["redis"] = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
if status["status"] == "error" {
|
||||||
|
c.JSON(500, status)
|
||||||
|
} else {
|
||||||
|
c.JSON(200, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 指标收集
|
||||||
|
```go
|
||||||
|
// internal/middleware/metrics.go
|
||||||
|
func PrometheusMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
status := c.Writer.Status()
|
||||||
|
|
||||||
|
// 记录请求指标
|
||||||
|
requestDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration.Seconds())
|
||||||
|
requestCount.WithLabelValues(c.Request.Method, c.FullPath(), fmt.Sprintf("%d", status)).Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 未来规划
|
||||||
|
|
||||||
|
### 功能扩展
|
||||||
|
- 📊 高级搜索和过滤
|
||||||
|
- 🔄 数据同步和备份
|
||||||
|
- 📱 移动端 API 优化
|
||||||
|
- 🤖 AI 图像识别和标记
|
||||||
|
- 📈 实时分析和推荐
|
||||||
|
|
||||||
|
### 技术升级
|
||||||
|
- 迁移到微服务架构
|
||||||
|
- 集成消息队列 (RabbitMQ/Kafka)
|
||||||
|
- 支持分布式存储
|
||||||
|
- 集成 Elasticsearch 搜索
|
||||||
|
- 支持 GraphQL API
|
||||||
|
|
||||||
|
## 📚 参考资料
|
||||||
|
|
||||||
|
- [Go 官方文档](https://golang.org/doc/)
|
||||||
|
- [Gin 框架文档](https://gin-gonic.com/docs/)
|
||||||
|
- [GORM 文档](https://gorm.io/docs/)
|
||||||
|
- [PostgreSQL 文档](https://www.postgresql.org/docs/)
|
||||||
|
- [Docker 最佳实践](https://docs.docker.com/develop/dev-best-practices/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
💡 **开发提示**: 开始开发前,请确保已经阅读根目录的 CLAUDE.md 文件,了解项目整体架构。开发过程中建议先实现核心功能,再逐步完善其他特性。务必编写单元测试和集成测试,确保代码质量。
|
||||||
604
ui/CLAUDE.md
Normal file
604
ui/CLAUDE.md
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
# UI 备份模块 - CLAUDE.md
|
||||||
|
|
||||||
|
此文件为 Claude Code 在 UI 备份模块工作时提供指导。
|
||||||
|
|
||||||
|
## 🎯 模块概览
|
||||||
|
|
||||||
|
UI 备份模块是主前端模块 (`frontend/`) 的备份和实验环境,用于保存稳定版本、测试新功能和组件开发。
|
||||||
|
|
||||||
|
### 模块用途
|
||||||
|
- 🔄 主前端代码的备份版本
|
||||||
|
- 🧪 新功能和组件的实验环境
|
||||||
|
- 📦 组件库的独立维护
|
||||||
|
- 🔧 A/B 测试和版本对比
|
||||||
|
- 🛠️ 紧急回滚的备用版本
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **前端**: Next.js 15 + React 19 + TypeScript
|
||||||
|
- **组件**: shadcn/ui + Radix UI
|
||||||
|
- **样式**: Tailwind CSS
|
||||||
|
- **构建**: Next.js 内置构建系统
|
||||||
|
- **包管理**: pnpm (与主前端区分)
|
||||||
|
|
||||||
|
## 📁 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ui/
|
||||||
|
├── CLAUDE.md # 🔍 当前文件 - UI 备份模块指导
|
||||||
|
├── app/ # Next.js 应用目录
|
||||||
|
│ ├── globals.css # 全局样式
|
||||||
|
│ ├── layout.tsx # 根布局
|
||||||
|
│ └── page.tsx # 主页
|
||||||
|
├── components/ # 组件目录
|
||||||
|
│ ├── ui/ # shadcn/ui 组件
|
||||||
|
│ │ ├── button.tsx # 按钮组件
|
||||||
|
│ │ ├── card.tsx # 卡片组件
|
||||||
|
│ │ ├── dialog.tsx # 对话框组件
|
||||||
|
│ │ └── ... # 其他 UI 组件
|
||||||
|
│ ├── about-view.tsx # 关于页面组件
|
||||||
|
│ ├── contact-view.tsx # 联系页面组件
|
||||||
|
│ ├── filter-bar.tsx # 过滤栏组件
|
||||||
|
│ ├── loading-spinner.tsx # 加载组件
|
||||||
|
│ ├── navigation.tsx # 导航组件
|
||||||
|
│ ├── photo-gallery.tsx # 照片画廊组件
|
||||||
|
│ ├── photo-modal.tsx # 照片弹窗组件
|
||||||
|
│ ├── theme-provider.tsx # 主题提供器
|
||||||
|
│ ├── timeline-stats.tsx # 时间线统计
|
||||||
|
│ └── timeline-view.tsx # 时间线视图
|
||||||
|
├── hooks/ # 自定义 Hook
|
||||||
|
│ ├── use-mobile.tsx # 移动端检测
|
||||||
|
│ └── use-toast.ts # 提示信息
|
||||||
|
├── lib/ # 工具库
|
||||||
|
│ └── utils.ts # 工具函数
|
||||||
|
├── public/ # 静态资源
|
||||||
|
│ ├── placeholder-logo.png # 占位图标
|
||||||
|
│ ├── placeholder-user.jpg # 占位头像
|
||||||
|
│ └── placeholder.jpg # 占位图片
|
||||||
|
├── styles/ # 样式文件
|
||||||
|
│ └── globals.css # 全局样式
|
||||||
|
├── components.json # shadcn/ui 配置
|
||||||
|
├── next.config.mjs # Next.js 配置
|
||||||
|
├── package.json # 依赖配置
|
||||||
|
├── pnpm-lock.yaml # pnpm 锁文件
|
||||||
|
├── postcss.config.mjs # PostCSS 配置
|
||||||
|
├── tailwind.config.ts # Tailwind 配置
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
|
└── README.md # 模块说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 与主前端的关系
|
||||||
|
|
||||||
|
### 同步策略
|
||||||
|
```bash
|
||||||
|
# 从主前端同步到 UI 备份
|
||||||
|
# 1. 同步核心组件
|
||||||
|
cp -r frontend/components/ui/* ui/components/ui/
|
||||||
|
|
||||||
|
# 2. 同步配置文件
|
||||||
|
cp frontend/tailwind.config.ts ui/
|
||||||
|
cp frontend/components.json ui/
|
||||||
|
|
||||||
|
# 3. 同步样式文件
|
||||||
|
cp frontend/app/globals.css ui/app/
|
||||||
|
cp frontend/styles/globals.css ui/styles/
|
||||||
|
|
||||||
|
# 4. 同步工具函数
|
||||||
|
cp frontend/lib/utils.ts ui/lib/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 版本控制
|
||||||
|
```bash
|
||||||
|
# 创建备份分支
|
||||||
|
git checkout -b ui-backup-v1.0.0
|
||||||
|
|
||||||
|
# 标记稳定版本
|
||||||
|
git tag ui-v1.0.0
|
||||||
|
|
||||||
|
# 推送备份版本
|
||||||
|
git push origin ui-backup-v1.0.0
|
||||||
|
git push origin ui-v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 实验环境配置
|
||||||
|
|
||||||
|
### 开发环境设置
|
||||||
|
```bash
|
||||||
|
# 进入 UI 备份目录
|
||||||
|
cd ui/
|
||||||
|
|
||||||
|
# 安装依赖 (使用 pnpm)
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 启动生产预览
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||||
|
NEXT_PUBLIC_MOCK_API=true
|
||||||
|
NEXT_PUBLIC_EXPERIMENTAL_FEATURES=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实验性功能开关
|
||||||
|
```typescript
|
||||||
|
// lib/experimental.ts
|
||||||
|
export const experimentalFeatures = {
|
||||||
|
// 新组件测试
|
||||||
|
newPhotoGallery: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES === 'true',
|
||||||
|
|
||||||
|
// A/B 测试
|
||||||
|
alternativeNavigation: false,
|
||||||
|
|
||||||
|
// 性能优化
|
||||||
|
optimizedImages: true,
|
||||||
|
|
||||||
|
// 新主题
|
||||||
|
darkModeV2: false,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
import { experimentalFeatures } from '@/lib/experimental'
|
||||||
|
|
||||||
|
export default function PhotoGallery() {
|
||||||
|
if (experimentalFeatures.newPhotoGallery) {
|
||||||
|
return <NewPhotoGallery />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OriginalPhotoGallery />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 组件库管理
|
||||||
|
|
||||||
|
### 组件分类
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── ui/ # 基础 UI 组件
|
||||||
|
│ ├── button.tsx # 按钮 - 基础交互
|
||||||
|
│ ├── input.tsx # 输入框 - 表单控件
|
||||||
|
│ ├── card.tsx # 卡片 - 内容容器
|
||||||
|
│ ├── dialog.tsx # 对话框 - 模态框
|
||||||
|
│ ├── dropdown-menu.tsx # 下拉菜单 - 菜单控件
|
||||||
|
│ ├── navigation-menu.tsx # 导航菜单 - 导航控件
|
||||||
|
│ ├── tabs.tsx # 标签页 - 切换控件
|
||||||
|
│ ├── toast.tsx # 提示信息 - 反馈组件
|
||||||
|
│ └── ... # 其他基础组件
|
||||||
|
├── business/ # 业务组件
|
||||||
|
│ ├── photo-gallery.tsx # 照片画廊
|
||||||
|
│ ├── photo-modal.tsx # 照片弹窗
|
||||||
|
│ ├── timeline-view.tsx # 时间线视图
|
||||||
|
│ ├── filter-bar.tsx # 过滤栏
|
||||||
|
│ └── navigation.tsx # 站点导航
|
||||||
|
├── layout/ # 布局组件
|
||||||
|
│ ├── header.tsx # 页头
|
||||||
|
│ ├── footer.tsx # 页脚
|
||||||
|
│ └── sidebar.tsx # 侧边栏
|
||||||
|
└── experimental/ # 实验性组件
|
||||||
|
├── new-photo-gallery.tsx # 新照片画廊
|
||||||
|
├── advanced-filter.tsx # 高级过滤器
|
||||||
|
└── ai-search.tsx # AI 搜索
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件开发规范
|
||||||
|
```typescript
|
||||||
|
// components/experimental/new-photo-gallery.tsx
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface Photo {
|
||||||
|
id: string
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewPhotoGalleryProps {
|
||||||
|
photos: Photo[]
|
||||||
|
onPhotoClick?: (photo: Photo) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewPhotoGallery({
|
||||||
|
photos,
|
||||||
|
onPhotoClick,
|
||||||
|
className
|
||||||
|
}: NewPhotoGalleryProps) {
|
||||||
|
const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null)
|
||||||
|
|
||||||
|
const handlePhotoClick = useCallback((photo: Photo) => {
|
||||||
|
setSelectedPhoto(photo)
|
||||||
|
onPhotoClick?.(photo)
|
||||||
|
}, [onPhotoClick])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ${className}`}>
|
||||||
|
{photos.map((photo) => (
|
||||||
|
<Card key={photo.id} className="overflow-hidden hover:shadow-lg transition-shadow">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div
|
||||||
|
className="relative aspect-square cursor-pointer"
|
||||||
|
onClick={() => handlePhotoClick(photo)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={photo.src}
|
||||||
|
alt={photo.alt}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/0 hover:bg-black/20 transition-colors" />
|
||||||
|
</div>
|
||||||
|
{photo.title && (
|
||||||
|
<div className="p-3">
|
||||||
|
<h3 className="font-semibold text-sm">{photo.title}</h3>
|
||||||
|
{photo.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{photo.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 版本管理和同步
|
||||||
|
|
||||||
|
### 版本管理策略
|
||||||
|
```bash
|
||||||
|
# 1. 创建功能分支
|
||||||
|
git checkout -b feature/new-component
|
||||||
|
|
||||||
|
# 2. 开发和测试
|
||||||
|
# ... 组件开发 ...
|
||||||
|
|
||||||
|
# 3. 合并到 UI 备份主分支
|
||||||
|
git checkout main
|
||||||
|
git merge feature/new-component
|
||||||
|
|
||||||
|
# 4. 测试稳定后,同步到主前端
|
||||||
|
cp ui/components/experimental/new-component.tsx frontend/components/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动同步脚本
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/sync-ui-to-frontend.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
UI_DIR="ui"
|
||||||
|
FRONTEND_DIR="frontend"
|
||||||
|
|
||||||
|
echo "🔄 开始同步 UI 备份到主前端..."
|
||||||
|
|
||||||
|
# 同步基础组件
|
||||||
|
echo "📦 同步基础组件..."
|
||||||
|
rsync -av --exclude='experimental' $UI_DIR/components/ui/ $FRONTEND_DIR/components/ui/
|
||||||
|
|
||||||
|
# 同步配置文件
|
||||||
|
echo "⚙️ 同步配置文件..."
|
||||||
|
cp $UI_DIR/tailwind.config.ts $FRONTEND_DIR/
|
||||||
|
cp $UI_DIR/components.json $FRONTEND_DIR/
|
||||||
|
|
||||||
|
# 同步样式文件
|
||||||
|
echo "🎨 同步样式文件..."
|
||||||
|
cp $UI_DIR/app/globals.css $FRONTEND_DIR/app/
|
||||||
|
cp $UI_DIR/styles/globals.css $FRONTEND_DIR/styles/
|
||||||
|
|
||||||
|
# 同步工具函数
|
||||||
|
echo "🛠️ 同步工具函数..."
|
||||||
|
cp $UI_DIR/lib/utils.ts $FRONTEND_DIR/lib/
|
||||||
|
|
||||||
|
echo "✅ 同步完成!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 反向同步脚本
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/sync-frontend-to-ui.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRONTEND_DIR="frontend"
|
||||||
|
UI_DIR="ui"
|
||||||
|
|
||||||
|
echo "🔄 开始同步主前端到 UI 备份..."
|
||||||
|
|
||||||
|
# 创建备份
|
||||||
|
echo "💾 创建备份..."
|
||||||
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||||
|
cp -r $UI_DIR $UI_DIR.backup.$timestamp
|
||||||
|
|
||||||
|
# 同步核心文件
|
||||||
|
echo "📦 同步核心文件..."
|
||||||
|
rsync -av --exclude='experimental' --exclude='backup.*' $FRONTEND_DIR/ $UI_DIR/
|
||||||
|
|
||||||
|
# 保留实验性功能
|
||||||
|
echo "🧪 保留实验性功能..."
|
||||||
|
if [ -d "$UI_DIR.backup.$timestamp/components/experimental" ]; then
|
||||||
|
cp -r $UI_DIR.backup.$timestamp/components/experimental $UI_DIR/components/
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ 同步完成!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 A/B 测试和实验
|
||||||
|
|
||||||
|
### A/B 测试框架
|
||||||
|
```typescript
|
||||||
|
// lib/ab-testing.ts
|
||||||
|
interface ABTest {
|
||||||
|
name: string
|
||||||
|
variants: {
|
||||||
|
control: React.ComponentType<any>
|
||||||
|
treatment: React.ComponentType<any>
|
||||||
|
}
|
||||||
|
percentage: number // 0-100
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class ABTestManager {
|
||||||
|
private tests: Map<string, ABTest> = new Map()
|
||||||
|
|
||||||
|
registerTest(test: ABTest) {
|
||||||
|
this.tests.set(test.name, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariant(testName: string): 'control' | 'treatment' | null {
|
||||||
|
const test = this.tests.get(testName)
|
||||||
|
if (!test || !test.enabled) return null
|
||||||
|
|
||||||
|
// 基于用户 ID 或 session 的一致性分组
|
||||||
|
const hash = this.hashString(testName + this.getUserId())
|
||||||
|
const percentage = hash % 100
|
||||||
|
|
||||||
|
return percentage < test.percentage ? 'treatment' : 'control'
|
||||||
|
}
|
||||||
|
|
||||||
|
renderComponent(testName: string, props: any) {
|
||||||
|
const test = this.tests.get(testName)
|
||||||
|
if (!test) return null
|
||||||
|
|
||||||
|
const variant = this.getVariant(testName)
|
||||||
|
if (variant === 'treatment') {
|
||||||
|
return <test.variants.treatment {...props} />
|
||||||
|
}
|
||||||
|
return <test.variants.control {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashString(str: string): number {
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i)
|
||||||
|
hash = ((hash << 5) - hash) + char
|
||||||
|
hash = hash & hash // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserId(): string {
|
||||||
|
// 实现用户 ID 获取逻辑
|
||||||
|
return sessionStorage.getItem('userId') || 'anonymous'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const abTestManager = new ABTestManager()
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
abTestManager.registerTest({
|
||||||
|
name: 'photo-gallery-layout',
|
||||||
|
variants: {
|
||||||
|
control: OriginalPhotoGallery,
|
||||||
|
treatment: NewPhotoGallery
|
||||||
|
},
|
||||||
|
percentage: 50,
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实验性功能管理
|
||||||
|
```typescript
|
||||||
|
// components/experimental/feature-flag.tsx
|
||||||
|
import { experimentalFeatures } from '@/lib/experimental'
|
||||||
|
|
||||||
|
interface FeatureFlagProps {
|
||||||
|
feature: keyof typeof experimentalFeatures
|
||||||
|
children: React.ReactNode
|
||||||
|
fallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureFlag({ feature, children, fallback = null }: FeatureFlagProps) {
|
||||||
|
if (experimentalFeatures[feature]) {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{fallback}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
<FeatureFlag feature="newPhotoGallery" fallback={<OriginalPhotoGallery />}>
|
||||||
|
<NewPhotoGallery />
|
||||||
|
</FeatureFlag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 构建和部署
|
||||||
|
|
||||||
|
### 构建配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3002",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start -p 3002",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"export": "next build && next export",
|
||||||
|
"sync-from-frontend": "./scripts/sync-frontend-to-ui.sh",
|
||||||
|
"sync-to-frontend": "./scripts/sync-ui-to-frontend.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next.js 配置
|
||||||
|
```typescript
|
||||||
|
// next.config.mjs
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
// 输出静态文件
|
||||||
|
output: 'export',
|
||||||
|
|
||||||
|
// 基础路径 (用于部署到子路径)
|
||||||
|
basePath: '/ui',
|
||||||
|
|
||||||
|
// 禁用图片优化 (静态导出)
|
||||||
|
images: {
|
||||||
|
unoptimized: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// 实验性功能
|
||||||
|
experimental: {
|
||||||
|
appDir: true,
|
||||||
|
serverComponentsExternalPackages: []
|
||||||
|
},
|
||||||
|
|
||||||
|
// 环境变量
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_UI_VERSION: process.env.npm_package_version || '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
### 部署配置
|
||||||
|
```bash
|
||||||
|
# 构建并部署到 UI 预览环境
|
||||||
|
npm run build
|
||||||
|
npm run export
|
||||||
|
|
||||||
|
# 部署到独立的 UI 预览域名
|
||||||
|
rsync -av out/ user@server:/var/www/ui-preview/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 与其他模块的集成
|
||||||
|
|
||||||
|
### 与主前端的协作
|
||||||
|
- 新功能在 UI 备份中实验和测试
|
||||||
|
- 稳定后同步到主前端
|
||||||
|
- 主前端的 bug 修复反向同步到 UI 备份
|
||||||
|
|
||||||
|
### 与管理后台的关系
|
||||||
|
- 共享基础组件库
|
||||||
|
- 管理后台可以复用 UI 备份中的组件
|
||||||
|
- 保持设计系统的一致性
|
||||||
|
|
||||||
|
### 与后端的集成
|
||||||
|
- 使用相同的 API 接口
|
||||||
|
- 支持 Mock API 进行独立开发
|
||||||
|
- 与后端版本保持兼容
|
||||||
|
|
||||||
|
## 🐛 问题排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **组件不同步**: 检查同步脚本和版本控制
|
||||||
|
2. **样式冲突**: 检查 Tailwind 配置和全局样式
|
||||||
|
3. **依赖版本不一致**: 检查 package.json 和锁文件
|
||||||
|
4. **构建失败**: 检查 TypeScript 配置和依赖
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
```bash
|
||||||
|
# 比较两个版本的差异
|
||||||
|
diff -r frontend/components ui/components
|
||||||
|
|
||||||
|
# 检查依赖差异
|
||||||
|
diff frontend/package.json ui/package.json
|
||||||
|
|
||||||
|
# 清理缓存
|
||||||
|
rm -rf ui/.next ui/node_modules
|
||||||
|
cd ui && pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 监控和分析
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
```typescript
|
||||||
|
// lib/performance.ts
|
||||||
|
export function measurePerformance(name: string, fn: () => void) {
|
||||||
|
const start = performance.now()
|
||||||
|
fn()
|
||||||
|
const end = performance.now()
|
||||||
|
|
||||||
|
console.log(`${name} took ${end - start} milliseconds`)
|
||||||
|
|
||||||
|
// 发送到分析服务
|
||||||
|
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||||
|
(window as any).gtag('event', 'timing_complete', {
|
||||||
|
name: name,
|
||||||
|
value: Math.round(end - start)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实验数据收集
|
||||||
|
```typescript
|
||||||
|
// lib/analytics.ts
|
||||||
|
export function trackExperiment(testName: string, variant: string) {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||||
|
(window as any).gtag('event', 'experiment_view', {
|
||||||
|
experiment_id: testName,
|
||||||
|
variant_id: variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackConversion(testName: string, variant: string, action: string) {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||||
|
(window as any).gtag('event', 'experiment_conversion', {
|
||||||
|
experiment_id: testName,
|
||||||
|
variant_id: variant,
|
||||||
|
action: action
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 未来规划
|
||||||
|
|
||||||
|
### 功能扩展
|
||||||
|
- 🔧 可视化组件编辑器
|
||||||
|
- 📊 实验结果分析面板
|
||||||
|
- 🎨 设计系统自动化
|
||||||
|
- 🤖 AI 辅助组件生成
|
||||||
|
- 📱 响应式设计测试工具
|
||||||
|
|
||||||
|
### 工作流优化
|
||||||
|
- 自动化同步流程
|
||||||
|
- 持续集成测试
|
||||||
|
- 可视化差异对比
|
||||||
|
- 自动化 A/B 测试报告
|
||||||
|
|
||||||
|
## 📚 参考资料
|
||||||
|
|
||||||
|
- [Next.js 文档](https://nextjs.org/docs)
|
||||||
|
- [React 19 新特性](https://react.dev/blog/2024/04/25/react-19)
|
||||||
|
- [shadcn/ui 文档](https://ui.shadcn.com/)
|
||||||
|
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
|
||||||
|
- [A/B 测试最佳实践](https://www.optimizely.com/optimization-glossary/ab-testing/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
💡 **使用提示**: UI 备份模块主要用于实验和备份,不建议在生产环境中直接使用。所有稳定的功能都应该同步到主前端模块。在进行实验时,请确保做好数据备份,避免影响主前端的稳定性。
|
||||||
Reference in New Issue
Block a user