Files
photography/backend/pkg/CLAUDE.md
xujiang a2f2f66f88
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 1m37s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
refactor: 重构后端架构,采用 Go 风格四层设计模式
## 主要变更

### 🏗️ 架构重构
- 采用简洁的四层架构:API → Service → Repository → Model
- 遵循 Go 语言最佳实践和命名规范
- 实现依赖注入和接口导向设计
- 统一错误处理和响应格式

### 📁 目录结构优化
- 删除重复模块 (application/, domain/, infrastructure/ 等)
- 规范化命名 (使用 Go 风格的 snake_case)
- 清理无关文件 (package.json, node_modules/ 等)
- 新增规范化的测试目录结构

### 📚 文档系统
- 为每个模块创建详细的 CLAUDE.md 指导文件
- 包含开发规范、最佳实践和使用示例
- 支持模块化开发,缩短上下文长度

### 🔧 开发规范
- 统一接口命名规范 (UserServicer, PhotoRepositoryr)
- 标准化错误处理机制
- 完善的测试策略 (单元测试、集成测试、性能测试)
- 规范化的配置管理

### 🗂️ 新增文件
- cmd/server/ - 服务启动入口和配置
- internal/model/ - 数据模型层 (entity, dto, request)
- pkg/ - 共享工具包 (logger, response, validator)
- tests/ - 完整测试结构
- docs/ - API 文档和架构设计
- .gitignore - Git 忽略文件配置

### 🗑️ 清理内容
- 删除 Node.js 相关文件 (package.json, node_modules/)
- 移除重复的架构目录
- 清理临时文件和构建产物
- 删除重复的文档文件

## 影响
- 提高代码可维护性和可扩展性
- 统一开发规范,提升团队协作效率
- 优化项目结构,符合 Go 语言生态标准
- 完善文档体系,降低上手难度
2025-07-10 11:20:59 +08:00

21 KiB
Raw Blame History

Shared Package Layer - CLAUDE.md

本文件为 Claude Code 在共享包模块中工作时提供指导。

🎯 模块概览

pkg 包提供可复用的工具和组件,供整个应用使用,遵循 Go 语言的包设计哲学。

主要职责

  • 📦 提供通用工具和实用函数
  • 📝 提供日志记录组件
  • 🔄 提供统一响应格式
  • 提供数据验证工具
  • 🛠️ 提供辅助功能函数

📁 模块结构

pkg/
├── CLAUDE.md                    # 📋 当前文件 - 公共工具包指导
├── logger/                      # 📝 日志工具
│   ├── logger.go                # 日志接口和实现
│   ├── zap_logger.go            # Zap 日志实现
│   ├── config.go                # 日志配置
│   └── logger_test.go           # 日志测试
├── response/                    # 🔄 响应格式
│   ├── response.go              # 统一响应结构
│   ├── error.go                 # 错误响应
│   ├── success.go               # 成功响应
│   └── pagination.go            # 分页响应
├── validator/                   # ✅ 数据验证
│   ├── validator.go             # 验证器接口
│   ├── gin_validator.go         # Gin 验证器实现
│   ├── custom_validators.go     # 自定义验证规则
│   └── validator_test.go        # 验证器测试
├── utils/                       # 🛠️ 通用工具
│   ├── string.go                # 字符串工具
│   ├── time.go                  # 时间工具
│   ├── crypto.go                # 加密工具
│   ├── file.go                  # 文件工具
│   ├── uuid.go                  # UUID 生成
│   └── utils_test.go            # 工具测试
├── middleware/                  # 🔗 中间件
│   ├── cors.go                  # CORS 中间件
│   ├── rate_limit.go            # 限流中间件
│   ├── request_id.go            # 请求ID中间件
│   └── recovery.go              # 错误恢复中间件
├── database/                    # 🗄️ 数据库工具
│   ├── connection.go            # 数据库连接
│   ├── migrate.go               # 数据库迁移
│   └── health.go                # 健康检查
└── config/                      # ⚙️ 配置工具
    ├── config.go                # 配置管理
    ├── env.go                   # 环境变量处理
    └── validation.go            # 配置验证

📝 日志组件

日志接口设计

// logger/logger.go - 日志接口
package logger

import (
    "context"
)

// Logger 日志接口
type Logger interface {
    // 基础日志方法
    Debug(msg string, fields ...Field)
    Info(msg string, fields ...Field)
    Warn(msg string, fields ...Field)
    Error(msg string, fields ...Field)
    Fatal(msg string, fields ...Field)
    
    // 上下文日志方法
    DebugContext(ctx context.Context, msg string, fields ...Field)
    InfoContext(ctx context.Context, msg string, fields ...Field)
    WarnContext(ctx context.Context, msg string, fields ...Field)
    ErrorContext(ctx context.Context, msg string, fields ...Field)
    
    // 结构化字段方法
    With(fields ...Field) Logger
    WithError(err error) Logger
    WithContext(ctx context.Context) Logger
    
    // 日志级别控制
    SetLevel(level Level)
    GetLevel() Level
}

// Field 日志字段
type Field struct {
    Key   string
    Value interface{}
}

// Level 日志级别
type Level int

const (
    DebugLevel Level = iota
    InfoLevel
    WarnLevel
    ErrorLevel
    FatalLevel
)

// 便捷字段构造函数
func String(key, value string) Field {
    return Field{Key: key, Value: value}
}

func Int(key string, value int) Field {
    return Field{Key: key, Value: value}
}

func Uint(key string, value uint) Field {
    return Field{Key: key, Value: value}
}

func Error(err error) Field {
    return Field{Key: "error", Value: err}
}

func Any(key string, value interface{}) Field {
    return Field{Key: key, Value: value}
}

Zap 日志实现

// logger/zap_logger.go - Zap 日志实现
package logger

import (
    "context"
    "os"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

// ZapLogger Zap 日志实现
type ZapLogger struct {
    logger *zap.Logger
    config *Config
}

// Config 日志配置
type Config struct {
    Level      string `mapstructure:"level" json:"level"`
    Format     string `mapstructure:"format" json:"format"` // json, console
    OutputPath string `mapstructure:"output_path" json:"output_path"`
    ErrorPath  string `mapstructure:"error_path" json:"error_path"`
    MaxSize    int    `mapstructure:"max_size" json:"max_size"`
    MaxAge     int    `mapstructure:"max_age" json:"max_age"`
    MaxBackups int    `mapstructure:"max_backups" json:"max_backups"`
    Compress   bool   `mapstructure:"compress" json:"compress"`
}

// NewZapLogger 创建 Zap 日志实例
func NewZapLogger(config *Config) (Logger, error) {
    zapConfig := zap.NewProductionConfig()
    
    // 设置日志级别
    level, err := zapcore.ParseLevel(config.Level)
    if err != nil {
        level = zapcore.InfoLevel
    }
    zapConfig.Level.SetLevel(level)
    
    // 设置输出格式
    if config.Format == "console" {
        zapConfig.Encoding = "console"
        zapConfig.EncoderConfig = zap.NewDevelopmentEncoderConfig()
    } else {
        zapConfig.Encoding = "json"
        zapConfig.EncoderConfig = zap.NewProductionEncoderConfig()
    }
    
    // 设置输出路径
    if config.OutputPath != "" {
        zapConfig.OutputPaths = []string{config.OutputPath}
    }
    if config.ErrorPath != "" {
        zapConfig.ErrorOutputPaths = []string{config.ErrorPath}
    }
    
    logger, err := zapConfig.Build()
    if err != nil {
        return nil, err
    }
    
    return &ZapLogger{
        logger: logger,
        config: config,
    }, nil
}

// Debug 调试日志
func (l *ZapLogger) Debug(msg string, fields ...Field) {
    l.logger.Debug(msg, l.convertFields(fields...)...)
}

// Info 信息日志
func (l *ZapLogger) Info(msg string, fields ...Field) {
    l.logger.Info(msg, l.convertFields(fields...)...)
}

// Warn 警告日志
func (l *ZapLogger) Warn(msg string, fields ...Field) {
    l.logger.Warn(msg, l.convertFields(fields...)...)
}

// Error 错误日志
func (l *ZapLogger) Error(msg string, fields ...Field) {
    l.logger.Error(msg, l.convertFields(fields...)...)
}

// Fatal 致命错误日志
func (l *ZapLogger) Fatal(msg string, fields ...Field) {
    l.logger.Fatal(msg, l.convertFields(fields...)...)
}

// With 添加字段
func (l *ZapLogger) With(fields ...Field) Logger {
    return &ZapLogger{
        logger: l.logger.With(l.convertFields(fields...)...),
        config: l.config,
    }
}

// WithError 添加错误字段
func (l *ZapLogger) WithError(err error) Logger {
    return l.With(Error(err))
}

// convertFields 转换字段格式
func (l *ZapLogger) convertFields(fields ...Field) []zap.Field {
    zapFields := make([]zap.Field, len(fields))
    for i, field := range fields {
        zapFields[i] = zap.Any(field.Key, field.Value)
    }
    return zapFields
}

// NewNoop 创建空日志实例(用于测试)
func NewNoop() Logger {
    return &ZapLogger{
        logger: zap.NewNop(),
        config: &Config{},
    }
}

🔄 响应格式

统一响应结构

// response/response.go - 统一响应结构
package response

import (
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
)

// Response 基础响应结构
type Response struct {
    Success   bool        `json:"success"`
    Data      interface{} `json:"data,omitempty"`
    Message   string      `json:"message,omitempty"`
    Error     *Error      `json:"error,omitempty"`
    Timestamp int64       `json:"timestamp"`
    RequestID string      `json:"request_id,omitempty"`
}

// Error 错误信息结构
type Error struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

// PaginatedResponse 分页响应结构
type PaginatedResponse struct {
    Response
    Pagination *Pagination `json:"pagination,omitempty"`
}

// Pagination 分页信息
type Pagination struct {
    Page       int   `json:"page"`
    Limit      int   `json:"limit"`
    Total      int64 `json:"total"`
    TotalPages int   `json:"total_pages"`
    HasNext    bool  `json:"has_next"`
    HasPrev    bool  `json:"has_prev"`
}

// Success 成功响应
func Success(c *gin.Context, data interface{}, message ...string) {
    msg := "Success"
    if len(message) > 0 {
        msg = message[0]
    }
    
    response := Response{
        Success:   true,
        Data:      data,
        Message:   msg,
        Timestamp: time.Now().Unix(),
        RequestID: getRequestID(c),
    }
    
    c.JSON(http.StatusOK, response)
}

// Error 错误响应
func Error(c *gin.Context, code string, message string, details ...string) {
    detail := ""
    if len(details) > 0 {
        detail = details[0]
    }
    
    response := Response{
        Success: false,
        Error: &Error{
            Code:    code,
            Message: message,
            Details: detail,
        },
        Timestamp: time.Now().Unix(),
        RequestID: getRequestID(c),
    }
    
    c.JSON(getHTTPStatusFromCode(code), response)
}

// BadRequest 400 错误
func BadRequest(c *gin.Context, message string, details ...string) {
    Error(c, "BAD_REQUEST", message, details...)
}

// Unauthorized 401 错误
func Unauthorized(c *gin.Context, message string, details ...string) {
    Error(c, "UNAUTHORIZED", message, details...)
}

// Forbidden 403 错误
func Forbidden(c *gin.Context, message string, details ...string) {
    Error(c, "FORBIDDEN", message, details...)
}

// NotFound 404 错误
func NotFound(c *gin.Context, message string, details ...string) {
    Error(c, "NOT_FOUND", message, details...)
}

// InternalError 500 错误
func InternalError(c *gin.Context, message string, details ...string) {
    Error(c, "INTERNAL_ERROR", message, details...)
}

// Paginated 分页响应
func Paginated(c *gin.Context, data interface{}, page, limit int, total int64, message ...string) {
    msg := "Success"
    if len(message) > 0 {
        msg = message[0]
    }
    
    totalPages := int((total + int64(limit) - 1) / int64(limit))
    
    pagination := &Pagination{
        Page:       page,
        Limit:      limit,
        Total:      total,
        TotalPages: totalPages,
        HasNext:    page < totalPages,
        HasPrev:    page > 1,
    }
    
    response := PaginatedResponse{
        Response: Response{
            Success:   true,
            Data:      data,
            Message:   msg,
            Timestamp: time.Now().Unix(),
            RequestID: getRequestID(c),
        },
        Pagination: pagination,
    }
    
    c.JSON(http.StatusOK, response)
}

// getRequestID 获取请求ID
func getRequestID(c *gin.Context) string {
    if requestID, exists := c.Get("X-Request-ID"); exists {
        if id, ok := requestID.(string); ok {
            return id
        }
    }
    return ""
}

// getHTTPStatusFromCode 根据错误码获取HTTP状态码
func getHTTPStatusFromCode(code string) int {
    switch code {
    case "BAD_REQUEST", "INVALID_PARAMETER", "VALIDATION_ERROR":
        return http.StatusBadRequest
    case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED":
        return http.StatusUnauthorized
    case "FORBIDDEN", "PERMISSION_DENIED":
        return http.StatusForbidden
    case "NOT_FOUND", "USER_NOT_FOUND", "PHOTO_NOT_FOUND":
        return http.StatusNotFound
    case "CONFLICT", "USER_EXISTS", "DUPLICATE_KEY":
        return http.StatusConflict
    case "TOO_MANY_REQUESTS":
        return http.StatusTooManyRequests
    default:
        return http.StatusInternalServerError
    }
}

数据验证

自定义验证器

// validator/custom_validators.go - 自定义验证规则
package validator

import (
    "regexp"
    "strings"
    
    "github.com/go-playground/validator/v10"
)

// RegisterCustomValidators 注册自定义验证器
func RegisterCustomValidators(v *validator.Validate) {
    v.RegisterValidation("username", validateUsername)
    v.RegisterValidation("password", validatePassword)
    v.RegisterValidation("phone", validatePhone)
    v.RegisterValidation("slug", validateSlug)
    v.RegisterValidation("file_ext", validateFileExtension)
}

// validateUsername 验证用户名
func validateUsername(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    
    // 3-50个字符只允许字母、数字、下划线、短横线
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, username)
    return matched
}

// validatePassword 验证密码强度
func validatePassword(fl validator.FieldLevel) bool {
    password := fl.Field().String()
    
    // 至少8个字符包含字母、数字和特殊字符
    if len(password) < 8 {
        return false
    }
    
    hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
    hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
    hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]`).MatchString(password)
    
    return hasLetter && hasNumber && hasSpecial
}

// validatePhone 验证手机号
func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    
    // 支持多种手机号格式
    patterns := []string{
        `^1[3-9]\d{9}$`,           // 中国大陆
        `^\+1[2-9]\d{9}$`,         // 美国
        `^\+44[1-9]\d{8,9}$`,      // 英国
    }
    
    for _, pattern := range patterns {
        if matched, _ := regexp.MatchString(pattern, phone); matched {
            return true
        }
    }
    
    return false
}

// validateSlug 验证 URL 友好字符串
func validateSlug(fl validator.FieldLevel) bool {
    slug := fl.Field().String()
    matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug)
    return matched
}

// validateFileExtension 验证文件扩展名
func validateFileExtension(fl validator.FieldLevel) bool {
    filename := fl.Field().String()
    ext := strings.ToLower(getFileExtension(filename))
    
    // 允许的图片文件扩展名
    allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
    
    for _, allowedExt := range allowedExts {
        if ext == allowedExt {
            return true
        }
    }
    
    return false
}

// getFileExtension 获取文件扩展名
func getFileExtension(filename string) string {
    lastDot := strings.LastIndex(filename, ".")
    if lastDot == -1 {
        return ""
    }
    return filename[lastDot:]
}

// ValidationError 验证错误结构
type ValidationError struct {
    Field   string `json:"field"`
    Value   string `json:"value"`
    Tag     string `json:"tag"`
    Message string `json:"message"`
}

// FormatValidationErrors 格式化验证错误
func FormatValidationErrors(err error) []ValidationError {
    var errors []ValidationError
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            errors = append(errors, ValidationError{
                Field:   e.Field(),
                Value:   e.Value().(string),
                Tag:     e.Tag(),
                Message: getErrorMessage(e),
            })
        }
    }
    
    return errors
}

// getErrorMessage 获取错误消息
func getErrorMessage(e validator.FieldError) string {
    switch e.Tag() {
    case "required":
        return e.Field() + " is required"
    case "email":
        return e.Field() + " must be a valid email"
    case "min":
        return e.Field() + " must be at least " + e.Param() + " characters"
    case "max":
        return e.Field() + " must be at most " + e.Param() + " characters"
    case "username":
        return e.Field() + " must be 3-50 characters and contain only letters, numbers, underscores, and hyphens"
    case "password":
        return e.Field() + " must be at least 8 characters and contain letters, numbers, and special characters"
    case "phone":
        return e.Field() + " must be a valid phone number"
    case "slug":
        return e.Field() + " must be a valid URL slug"
    case "file_ext":
        return e.Field() + " must have a valid image file extension"
    default:
        return e.Field() + " is invalid"
    }
}

🛠️ 通用工具

字符串工具

// utils/string.go - 字符串工具
package utils

import (
    "crypto/rand"
    "math/big"
    "regexp"
    "strings"
    "unicode"
)

// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) (string, error) {
    const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    result := make([]byte, length)
    
    for i := range result {
        index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        if err != nil {
            return "", err
        }
        result[i] = charset[index.Int64()]
    }
    
    return string(result), nil
}

// ToSnakeCase 转换为蛇形命名
func ToSnakeCase(str string) string {
    var result []rune
    
    for i, r := range str {
        if unicode.IsUpper(r) {
            if i > 0 {
                result = append(result, '_')
            }
            result = append(result, unicode.ToLower(r))
        } else {
            result = append(result, r)
        }
    }
    
    return string(result)
}

// ToCamelCase 转换为驼峰命名
func ToCamelCase(str string) string {
    parts := strings.Split(str, "_")
    for i := 1; i < len(parts); i++ {
        if len(parts[i]) > 0 {
            parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
        }
    }
    return strings.Join(parts, "")
}

// Truncate 截断字符串
func Truncate(str string, length int) string {
    if len(str) <= length {
        return str
    }
    return str[:length] + "..."
}

// IsValidEmail 验证邮箱格式
func IsValidEmail(email string) bool {
    pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
    matched, _ := regexp.MatchString(pattern, email)
    return matched
}

// SanitizeFilename 清理文件名
func SanitizeFilename(filename string) string {
    // 移除或替换不安全的字符
    reg := regexp.MustCompile(`[<>:"/\\|?*]`)
    return reg.ReplaceAllString(filename, "_")
}

// Contains 检查字符串切片是否包含指定字符串
func Contains(slice []string, item string) bool {
    for _, s := range slice {
        if s == item {
            return true
        }
    }
    return false
}

加密工具

// utils/crypto.go - 加密工具
package utils

import (
    "crypto/md5"
    "crypto/sha256"
    "encoding/hex"
    "golang.org/x/crypto/bcrypt"
)

// HashPassword 加密密码
func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

// MD5Hash 计算MD5哈希
func MD5Hash(data string) string {
    hash := md5.Sum([]byte(data))
    return hex.EncodeToString(hash[:])
}

// SHA256Hash 计算SHA256哈希
func SHA256Hash(data string) string {
    hash := sha256.Sum256([]byte(data))
    return hex.EncodeToString(hash[:])
}

🔗 中间件

CORS 中间件

// middleware/cors.go - CORS 中间件
package middleware

import (
    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
)

// CORS 创建CORS中间件
func CORS() gin.HandlerFunc {
    config := cors.DefaultConfig()
    config.AllowOrigins = []string{"*"}
    config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
    config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"}
    config.ExposeHeaders = []string{"X-Request-ID"}
    config.AllowCredentials = true
    
    return cors.New(config)
}

请求ID中间件

// middleware/request_id.go - 请求ID中间件
package middleware

import (
    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

// RequestID 请求ID中间件
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := c.GetHeader("X-Request-ID")
        if requestID == "" {
            requestID = uuid.New().String()
        }
        
        c.Set("X-Request-ID", requestID)
        c.Header("X-Request-ID", requestID)
        c.Next()
    }
}

💡 最佳实践

包设计原则

  1. 单一职责: 每个包只负责一个明确的功能
  2. 接口导向: 优先定义接口,便于测试和替换
  3. 零依赖: 包应尽量减少外部依赖
  4. 文档完善: 提供清晰的使用文档和示例
  5. 向后兼容: 保持API的向后兼容性

代码质量

  1. 测试覆盖: 为所有公共函数编写测试
  2. 错误处理: 合理处理和传播错误
  3. 性能考虑: 关注内存分配和性能影响
  4. 线程安全: 确保并发使用的安全性
  5. 资源管理: 及时释放资源

使用建议

  1. 导入路径: 使用清晰的导入路径
  2. 命名规范: 遵循 Go 语言命名约定
  3. 版本管理: 使用语义化版本管理
  4. 依赖管理: 合理管理第三方依赖
  5. 配置化: 支持配置化使用

本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。