565 lines
16 KiB
Markdown
565 lines
16 KiB
Markdown
# HTTP 接口层 - CLAUDE.md
|
|
|
|
本文件为 Claude Code 在 HTTP 接口层模块中工作时提供指导。
|
|
|
|
## 🎯 模块概览
|
|
|
|
HTTP 接口层负责处理所有 HTTP 请求,包括路由定义、请求处理、响应格式化、中间件管理等。
|
|
|
|
### 职责范围
|
|
- 🌐 HTTP 路由定义和管理
|
|
- 📝 请求处理和响应格式化
|
|
- 🛡️ 中间件管理和应用
|
|
- ✅ 请求参数验证
|
|
- 📊 HTTP 状态码管理
|
|
|
|
### 文件结构
|
|
```
|
|
internal/api/
|
|
├── CLAUDE.md # 📋 当前文件 - API 接口指导
|
|
├── handlers/ # 🎯 HTTP 处理器
|
|
│ ├── user.go # 用户相关处理器
|
|
│ ├── photo.go # 照片相关处理器
|
|
│ ├── category.go # 分类相关处理器
|
|
│ ├── tag.go # 标签相关处理器
|
|
│ ├── auth.go # 认证相关处理器
|
|
│ ├── upload.go # 上传相关处理器
|
|
│ └── health.go # 健康检查处理器
|
|
├── middleware/ # 🛡️ 中间件
|
|
│ ├── auth.go # 认证中间件
|
|
│ ├── cors.go # CORS 中间件
|
|
│ ├── logger.go # 日志中间件
|
|
│ ├── rate_limit.go # 限流中间件
|
|
│ ├── recovery.go # 错误恢复中间件
|
|
│ └── validator.go # 验证中间件
|
|
├── routes/ # 🗺️ 路由定义
|
|
│ ├── v1.go # API v1 路由
|
|
│ ├── auth.go # 认证路由
|
|
│ ├── public.go # 公共路由
|
|
│ └── admin.go # 管理员路由
|
|
└── validators/ # ✅ 请求验证器
|
|
├── user.go # 用户验证器
|
|
├── photo.go # 照片验证器
|
|
├── category.go # 分类验证器
|
|
└── common.go # 通用验证器
|
|
```
|
|
|
|
## 🎯 处理器模式
|
|
|
|
### 标准处理器结构
|
|
```go
|
|
type UserHandler struct {
|
|
userService service.UserServiceInterface
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func NewUserHandler(userService service.UserServiceInterface, logger *zap.Logger) *UserHandler {
|
|
return &UserHandler{
|
|
userService: userService,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// 创建用户
|
|
func (h *UserHandler) Create(c *gin.Context) {
|
|
var req validators.CreateUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "请求参数无效", err)
|
|
return
|
|
}
|
|
|
|
user, err := h.userService.Create(c.Request.Context(), req)
|
|
if err != nil {
|
|
h.logger.Error("创建用户失败", zap.Error(err))
|
|
response.Error(c, http.StatusInternalServerError, "CREATE_USER_FAILED", "创建用户失败", err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, user, "用户创建成功")
|
|
}
|
|
```
|
|
|
|
### 处理器职责
|
|
1. **请求绑定**: 解析 HTTP 请求参数
|
|
2. **参数验证**: 验证请求参数的有效性
|
|
3. **服务调用**: 调用应用服务层处理业务逻辑
|
|
4. **响应格式化**: 格式化响应数据
|
|
5. **错误处理**: 处理和记录错误信息
|
|
|
|
## 🗺️ 路由设计
|
|
|
|
### API 版本管理
|
|
```go
|
|
// v1 路由组
|
|
v1 := router.Group("/api/v1")
|
|
{
|
|
// 公共路由 (无需认证)
|
|
v1.POST("/auth/login", authHandler.Login)
|
|
v1.POST("/auth/register", authHandler.Register)
|
|
v1.GET("/photos/public", photoHandler.GetPublicPhotos)
|
|
|
|
// 需要认证的路由
|
|
authenticated := v1.Group("")
|
|
authenticated.Use(middleware.AuthRequired())
|
|
{
|
|
authenticated.GET("/users/profile", userHandler.GetProfile)
|
|
authenticated.PUT("/users/profile", userHandler.UpdateProfile)
|
|
authenticated.POST("/photos", photoHandler.Create)
|
|
authenticated.GET("/photos", photoHandler.List)
|
|
}
|
|
|
|
// 管理员路由
|
|
admin := v1.Group("/admin")
|
|
admin.Use(middleware.AuthRequired(), middleware.AdminRequired())
|
|
{
|
|
admin.GET("/users", userHandler.ListUsers)
|
|
admin.DELETE("/users/:id", userHandler.DeleteUser)
|
|
admin.GET("/photos/all", photoHandler.ListAllPhotos)
|
|
}
|
|
}
|
|
```
|
|
|
|
### RESTful API 设计
|
|
```go
|
|
// 用户资源
|
|
GET /api/v1/users # 获取用户列表
|
|
POST /api/v1/users # 创建用户
|
|
GET /api/v1/users/:id # 获取用户详情
|
|
PUT /api/v1/users/:id # 更新用户
|
|
DELETE /api/v1/users/:id # 删除用户
|
|
|
|
// 照片资源
|
|
GET /api/v1/photos # 获取照片列表
|
|
POST /api/v1/photos # 创建照片
|
|
GET /api/v1/photos/:id # 获取照片详情
|
|
PUT /api/v1/photos/:id # 更新照片
|
|
DELETE /api/v1/photos/:id # 删除照片
|
|
|
|
// 分类资源
|
|
GET /api/v1/categories # 获取分类列表
|
|
POST /api/v1/categories # 创建分类
|
|
GET /api/v1/categories/:id # 获取分类详情
|
|
PUT /api/v1/categories/:id # 更新分类
|
|
DELETE /api/v1/categories/:id # 删除分类
|
|
```
|
|
|
|
## 🛡️ 中间件管理
|
|
|
|
### 认证中间件
|
|
```go
|
|
func AuthRequired() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
token := c.GetHeader("Authorization")
|
|
if token == "" {
|
|
response.Error(c, http.StatusUnauthorized, "MISSING_TOKEN", "缺少认证令牌", nil)
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// 验证 JWT Token
|
|
claims, err := jwt.VerifyToken(token)
|
|
if err != nil {
|
|
response.Error(c, http.StatusUnauthorized, "INVALID_TOKEN", "无效的认证令牌", err)
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// 设置用户信息到上下文
|
|
c.Set("user_id", claims.UserID)
|
|
c.Set("user_role", claims.Role)
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 日志中间件
|
|
```go
|
|
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
c.Next()
|
|
duration := time.Since(start)
|
|
|
|
logger.Info("HTTP Request",
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("query", c.Request.URL.RawQuery),
|
|
zap.Int("status", c.Writer.Status()),
|
|
zap.Duration("duration", duration),
|
|
zap.String("user_agent", c.Request.UserAgent()),
|
|
zap.String("ip", c.ClientIP()),
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### CORS 中间件
|
|
```go
|
|
func CORS() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
c.Header("Access-Control-Max-Age", "86400")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(204)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 限流中间件
|
|
```go
|
|
func RateLimit(limit int, window time.Duration) gin.HandlerFunc {
|
|
limiter := rate.NewLimiter(rate.Every(window), limit)
|
|
|
|
return func(c *gin.Context) {
|
|
if !limiter.Allow() {
|
|
response.Error(c, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "请求过于频繁", nil)
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
## ✅ 请求验证
|
|
|
|
### 验证器结构
|
|
```go
|
|
type CreateUserRequest struct {
|
|
Username string `json:"username" binding:"required,min=3,max=20"`
|
|
Email string `json:"email" binding:"required,email"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
Role string `json:"role" binding:"oneof=user editor admin"`
|
|
}
|
|
|
|
type UpdateUserRequest struct {
|
|
Username string `json:"username,omitempty" binding:"omitempty,min=3,max=20"`
|
|
Email string `json:"email,omitempty" binding:"omitempty,email"`
|
|
Role string `json:"role,omitempty" binding:"omitempty,oneof=user editor admin"`
|
|
}
|
|
```
|
|
|
|
### 自定义验证器
|
|
```go
|
|
func ValidateUsername(fl validator.FieldLevel) bool {
|
|
username := fl.Field().String()
|
|
// 用户名只能包含字母、数字和下划线
|
|
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
|
|
return matched
|
|
}
|
|
|
|
func ValidatePhotoFormat(fl validator.FieldLevel) bool {
|
|
format := fl.Field().String()
|
|
allowedFormats := []string{"jpg", "jpeg", "png", "gif", "webp"}
|
|
for _, allowed := range allowedFormats {
|
|
if format == allowed {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
```
|
|
|
|
### 验证错误处理
|
|
```go
|
|
func FormatValidationErrors(err error) map[string]string {
|
|
errors := make(map[string]string)
|
|
|
|
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
|
for _, e := range validationErrors {
|
|
field := strings.ToLower(e.Field())
|
|
switch e.Tag() {
|
|
case "required":
|
|
errors[field] = "此字段为必填项"
|
|
case "email":
|
|
errors[field] = "请输入有效的邮箱地址"
|
|
case "min":
|
|
errors[field] = fmt.Sprintf("最小长度为 %s", e.Param())
|
|
case "max":
|
|
errors[field] = fmt.Sprintf("最大长度为 %s", e.Param())
|
|
default:
|
|
errors[field] = "字段值无效"
|
|
}
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
```
|
|
|
|
## 📊 响应格式
|
|
|
|
### 统一响应结构
|
|
```go
|
|
type Response struct {
|
|
Success bool `json:"success"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Message string `json:"message"`
|
|
Error *ErrorInfo `json:"error,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
type ErrorInfo struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details interface{} `json:"details,omitempty"`
|
|
}
|
|
```
|
|
|
|
### 响应帮助函数
|
|
```go
|
|
func Success(c *gin.Context, data interface{}, message string) {
|
|
c.JSON(http.StatusOK, Response{
|
|
Success: true,
|
|
Data: data,
|
|
Message: message,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
|
|
func Error(c *gin.Context, statusCode int, errorCode, message string, details interface{}) {
|
|
c.JSON(statusCode, Response{
|
|
Success: false,
|
|
Message: message,
|
|
Error: &ErrorInfo{
|
|
Code: errorCode,
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
```
|
|
|
|
### 分页响应
|
|
```go
|
|
type PaginatedResponse struct {
|
|
Data interface{} `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
type Pagination struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Total int64 `json:"total"`
|
|
TotalPages int `json:"total_pages"`
|
|
HasNext bool `json:"has_next"`
|
|
HasPrevious bool `json:"has_previous"`
|
|
}
|
|
```
|
|
|
|
## 🔍 错误处理
|
|
|
|
### 错误分类
|
|
```go
|
|
const (
|
|
// 请求错误
|
|
ErrInvalidRequest = "INVALID_REQUEST"
|
|
ErrMissingParameter = "MISSING_PARAMETER"
|
|
ErrInvalidParameter = "INVALID_PARAMETER"
|
|
|
|
// 认证错误
|
|
ErrMissingToken = "MISSING_TOKEN"
|
|
ErrInvalidToken = "INVALID_TOKEN"
|
|
ErrTokenExpired = "TOKEN_EXPIRED"
|
|
ErrInsufficientPermission = "INSUFFICIENT_PERMISSION"
|
|
|
|
// 业务错误
|
|
ErrUserNotFound = "USER_NOT_FOUND"
|
|
ErrUserAlreadyExists = "USER_ALREADY_EXISTS"
|
|
ErrPhotoNotFound = "PHOTO_NOT_FOUND"
|
|
ErrCategoryNotFound = "CATEGORY_NOT_FOUND"
|
|
|
|
// 系统错误
|
|
ErrInternalServer = "INTERNAL_SERVER_ERROR"
|
|
ErrDatabaseError = "DATABASE_ERROR"
|
|
ErrStorageError = "STORAGE_ERROR"
|
|
)
|
|
```
|
|
|
|
### 错误恢复中间件
|
|
```go
|
|
func Recovery(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
logger.Error("Panic recovered",
|
|
zap.Any("error", err),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
)
|
|
|
|
response.Error(c, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "服务器内部错误", nil)
|
|
}
|
|
}()
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
## 📊 性能优化
|
|
|
|
### 响应压缩
|
|
```go
|
|
func Compression() gin.HandlerFunc {
|
|
return gzip.Gzip(gzip.DefaultCompression)
|
|
}
|
|
```
|
|
|
|
### 缓存控制
|
|
```go
|
|
func CacheControl(maxAge int) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", maxAge))
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 请求大小限制
|
|
```go
|
|
func LimitRequestSize(maxSize int64) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
|
c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🧪 测试策略
|
|
|
|
### 处理器测试
|
|
```go
|
|
func TestUserHandler_Create(t *testing.T) {
|
|
// 模拟服务
|
|
mockService := &MockUserService{}
|
|
handler := NewUserHandler(mockService, zap.NewNop())
|
|
|
|
// 创建测试请求
|
|
reqBody := `{"username":"test","email":"test@example.com","password":"123456"}`
|
|
req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// 执行请求
|
|
w := httptest.NewRecorder()
|
|
router := gin.New()
|
|
router.POST("/api/v1/users", handler.Create)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// 验证结果
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "success")
|
|
}
|
|
```
|
|
|
|
### 中间件测试
|
|
```go
|
|
func TestAuthMiddleware(t *testing.T) {
|
|
middleware := AuthRequired()
|
|
|
|
// 测试缺少 token
|
|
req, _ := http.NewRequest("GET", "/api/v1/users", nil)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = req
|
|
|
|
middleware(c)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
assert.Contains(t, w.Body.String(), "MISSING_TOKEN")
|
|
}
|
|
```
|
|
|
|
## 📚 API 文档
|
|
|
|
### Swagger 注释
|
|
```go
|
|
// CreateUser 创建用户
|
|
// @Summary 创建用户
|
|
// @Description 创建新用户账户
|
|
// @Tags 用户管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param user body validators.CreateUserRequest true "用户信息"
|
|
// @Success 200 {object} response.Response{data=models.User} "成功创建用户"
|
|
// @Failure 400 {object} response.Response "请求参数错误"
|
|
// @Failure 500 {object} response.Response "服务器内部错误"
|
|
// @Router /api/v1/users [post]
|
|
func (h *UserHandler) Create(c *gin.Context) {
|
|
// 处理器实现
|
|
}
|
|
```
|
|
|
|
### API 文档生成
|
|
```bash
|
|
# 安装 swag
|
|
go install github.com/swaggo/swag/cmd/swag@latest
|
|
|
|
# 生成文档
|
|
swag init -g cmd/server/main.go
|
|
|
|
# 启动时访问文档
|
|
# http://localhost:8080/swagger/index.html
|
|
```
|
|
|
|
## 🔧 开发工具
|
|
|
|
### 路由调试
|
|
```go
|
|
func PrintRoutes(router *gin.Engine) {
|
|
routes := router.Routes()
|
|
for _, route := range routes {
|
|
fmt.Printf("[%s] %s -> %s\n", route.Method, route.Path, route.Handler)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 请求日志
|
|
```go
|
|
func RequestLogger() gin.HandlerFunc {
|
|
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
|
return fmt.Sprintf("[%s] %s %s %d %s\n",
|
|
param.TimeStamp.Format("2006-01-02 15:04:05"),
|
|
param.Method,
|
|
param.Path,
|
|
param.StatusCode,
|
|
param.Latency,
|
|
)
|
|
})
|
|
}
|
|
```
|
|
|
|
## 💡 最佳实践
|
|
|
|
### 处理器设计
|
|
- 保持处理器轻量,业务逻辑放在服务层
|
|
- 统一错误处理和响应格式
|
|
- 完善的参数验证和错误提示
|
|
- 适当的日志记录
|
|
|
|
### 中间件使用
|
|
- 按需应用中间件,避免过度使用
|
|
- 注意中间件的执行顺序
|
|
- 错误处理中间件应该最先应用
|
|
- 认证中间件应该在业务中间件之前
|
|
|
|
### 路由设计
|
|
- 遵循 RESTful 设计原则
|
|
- 合理的路由分组和版本管理
|
|
- 清晰的路由命名和结构
|
|
- 适当的权限控制
|
|
|
|
### 性能考虑
|
|
- 使用响应压缩减少传输大小
|
|
- 适当的缓存控制
|
|
- 限制请求大小和频率
|
|
- 异步处理耗时操作
|
|
|
|
本模块是整个 API 的入口和门面,确保接口设计合理、响应格式统一、错误处理完善是项目成功的关键。 |