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:
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 文件,了解项目整体架构。开发过程中建议先实现核心功能,再逐步完善其他特性。务必编写单元测试和集成测试,确保代码质量。
|
||||
Reference in New Issue
Block a user