# Shared Package Layer - CLAUDE.md 本文件为 Claude Code 在共享包模块中工作时提供指导。 ## 🎯 模块概览 pkg 包提供可复用的工具和组件,供整个应用使用,遵循 Go 语言的包设计哲学。 ### 主要职责 - 📦 提供通用工具和实用函数 - 📝 提供日志记录组件 - 🔄 提供统一响应格式 - ✅ 提供数据验证工具 - 🛠️ 提供辅助功能函数 ## 📁 模块结构 ``` pkg/ ├── CLAUDE.md # 📋 当前文件 - 公共工具包指导 ├── logger/ # 📝 日志工具 │ ├── logger.go # 日志接口和实现 │ ├── zap_logger.go # Zap 日志实现 │ ├── config.go # 日志配置 │ └── logger_test.go # 日志测试 ├── response/ # 🔄 响应格式 │ ├── response.go # 统一响应结构 │ ├── error.go # 错误响应 │ ├── success.go # 成功响应 │ └── pagination.go # 分页响应 ├── validator/ # ✅ 数据验证 │ ├── validator.go # 验证器接口 │ ├── gin_validator.go # Gin 验证器实现 │ ├── custom_validators.go # 自定义验证规则 │ └── validator_test.go # 验证器测试 ├── utils/ # 🛠️ 通用工具 │ ├── string.go # 字符串工具 │ ├── time.go # 时间工具 │ ├── crypto.go # 加密工具 │ ├── file.go # 文件工具 │ ├── uuid.go # UUID 生成 │ └── utils_test.go # 工具测试 ├── middleware/ # 🔗 中间件 │ ├── cors.go # CORS 中间件 │ ├── rate_limit.go # 限流中间件 │ ├── request_id.go # 请求ID中间件 │ └── recovery.go # 错误恢复中间件 ├── database/ # 🗄️ 数据库工具 │ ├── connection.go # 数据库连接 │ ├── migrate.go # 数据库迁移 │ └── health.go # 健康检查 └── config/ # ⚙️ 配置工具 ├── config.go # 配置管理 ├── env.go # 环境变量处理 └── validation.go # 配置验证 ``` ## 📝 日志组件 ### 日志接口设计 ```go // logger/logger.go - 日志接口 package logger import ( "context" ) // Logger 日志接口 type Logger interface { // 基础日志方法 Debug(msg string, fields ...Field) Info(msg string, fields ...Field) Warn(msg string, fields ...Field) Error(msg string, fields ...Field) Fatal(msg string, fields ...Field) // 上下文日志方法 DebugContext(ctx context.Context, msg string, fields ...Field) InfoContext(ctx context.Context, msg string, fields ...Field) WarnContext(ctx context.Context, msg string, fields ...Field) ErrorContext(ctx context.Context, msg string, fields ...Field) // 结构化字段方法 With(fields ...Field) Logger WithError(err error) Logger WithContext(ctx context.Context) Logger // 日志级别控制 SetLevel(level Level) GetLevel() Level } // Field 日志字段 type Field struct { Key string Value interface{} } // Level 日志级别 type Level int const ( DebugLevel Level = iota InfoLevel WarnLevel ErrorLevel FatalLevel ) // 便捷字段构造函数 func String(key, value string) Field { return Field{Key: key, Value: value} } func Int(key string, value int) Field { return Field{Key: key, Value: value} } func Uint(key string, value uint) Field { return Field{Key: key, Value: value} } func Error(err error) Field { return Field{Key: "error", Value: err} } func Any(key string, value interface{}) Field { return Field{Key: key, Value: value} } ``` ### Zap 日志实现 ```go // logger/zap_logger.go - Zap 日志实现 package logger import ( "context" "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // ZapLogger Zap 日志实现 type ZapLogger struct { logger *zap.Logger config *Config } // Config 日志配置 type Config struct { Level string `mapstructure:"level" json:"level"` Format string `mapstructure:"format" json:"format"` // json, console OutputPath string `mapstructure:"output_path" json:"output_path"` ErrorPath string `mapstructure:"error_path" json:"error_path"` MaxSize int `mapstructure:"max_size" json:"max_size"` MaxAge int `mapstructure:"max_age" json:"max_age"` MaxBackups int `mapstructure:"max_backups" json:"max_backups"` Compress bool `mapstructure:"compress" json:"compress"` } // NewZapLogger 创建 Zap 日志实例 func NewZapLogger(config *Config) (Logger, error) { zapConfig := zap.NewProductionConfig() // 设置日志级别 level, err := zapcore.ParseLevel(config.Level) if err != nil { level = zapcore.InfoLevel } zapConfig.Level.SetLevel(level) // 设置输出格式 if config.Format == "console" { zapConfig.Encoding = "console" zapConfig.EncoderConfig = zap.NewDevelopmentEncoderConfig() } else { zapConfig.Encoding = "json" zapConfig.EncoderConfig = zap.NewProductionEncoderConfig() } // 设置输出路径 if config.OutputPath != "" { zapConfig.OutputPaths = []string{config.OutputPath} } if config.ErrorPath != "" { zapConfig.ErrorOutputPaths = []string{config.ErrorPath} } logger, err := zapConfig.Build() if err != nil { return nil, err } return &ZapLogger{ logger: logger, config: config, }, nil } // Debug 调试日志 func (l *ZapLogger) Debug(msg string, fields ...Field) { l.logger.Debug(msg, l.convertFields(fields...)...) } // Info 信息日志 func (l *ZapLogger) Info(msg string, fields ...Field) { l.logger.Info(msg, l.convertFields(fields...)...) } // Warn 警告日志 func (l *ZapLogger) Warn(msg string, fields ...Field) { l.logger.Warn(msg, l.convertFields(fields...)...) } // Error 错误日志 func (l *ZapLogger) Error(msg string, fields ...Field) { l.logger.Error(msg, l.convertFields(fields...)...) } // Fatal 致命错误日志 func (l *ZapLogger) Fatal(msg string, fields ...Field) { l.logger.Fatal(msg, l.convertFields(fields...)...) } // With 添加字段 func (l *ZapLogger) With(fields ...Field) Logger { return &ZapLogger{ logger: l.logger.With(l.convertFields(fields...)...), config: l.config, } } // WithError 添加错误字段 func (l *ZapLogger) WithError(err error) Logger { return l.With(Error(err)) } // convertFields 转换字段格式 func (l *ZapLogger) convertFields(fields ...Field) []zap.Field { zapFields := make([]zap.Field, len(fields)) for i, field := range fields { zapFields[i] = zap.Any(field.Key, field.Value) } return zapFields } // NewNoop 创建空日志实例(用于测试) func NewNoop() Logger { return &ZapLogger{ logger: zap.NewNop(), config: &Config{}, } } ``` ## 🔄 响应格式 ### 统一响应结构 ```go // response/response.go - 统一响应结构 package response import ( "net/http" "time" "github.com/gin-gonic/gin" ) // Response 基础响应结构 type Response struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Message string `json:"message,omitempty"` Error *Error `json:"error,omitempty"` Timestamp int64 `json:"timestamp"` RequestID string `json:"request_id,omitempty"` } // Error 错误信息结构 type Error struct { Code string `json:"code"` Message string `json:"message"` Details string `json:"details,omitempty"` } // PaginatedResponse 分页响应结构 type PaginatedResponse struct { Response Pagination *Pagination `json:"pagination,omitempty"` } // Pagination 分页信息 type Pagination struct { Page int `json:"page"` Limit int `json:"limit"` Total int64 `json:"total"` TotalPages int `json:"total_pages"` HasNext bool `json:"has_next"` HasPrev bool `json:"has_prev"` } // Success 成功响应 func Success(c *gin.Context, data interface{}, message ...string) { msg := "Success" if len(message) > 0 { msg = message[0] } response := Response{ Success: true, Data: data, Message: msg, Timestamp: time.Now().Unix(), RequestID: getRequestID(c), } c.JSON(http.StatusOK, response) } // Error 错误响应 func Error(c *gin.Context, code string, message string, details ...string) { detail := "" if len(details) > 0 { detail = details[0] } response := Response{ Success: false, Error: &Error{ Code: code, Message: message, Details: detail, }, Timestamp: time.Now().Unix(), RequestID: getRequestID(c), } c.JSON(getHTTPStatusFromCode(code), response) } // BadRequest 400 错误 func BadRequest(c *gin.Context, message string, details ...string) { Error(c, "BAD_REQUEST", message, details...) } // Unauthorized 401 错误 func Unauthorized(c *gin.Context, message string, details ...string) { Error(c, "UNAUTHORIZED", message, details...) } // Forbidden 403 错误 func Forbidden(c *gin.Context, message string, details ...string) { Error(c, "FORBIDDEN", message, details...) } // NotFound 404 错误 func NotFound(c *gin.Context, message string, details ...string) { Error(c, "NOT_FOUND", message, details...) } // InternalError 500 错误 func InternalError(c *gin.Context, message string, details ...string) { Error(c, "INTERNAL_ERROR", message, details...) } // Paginated 分页响应 func Paginated(c *gin.Context, data interface{}, page, limit int, total int64, message ...string) { msg := "Success" if len(message) > 0 { msg = message[0] } totalPages := int((total + int64(limit) - 1) / int64(limit)) pagination := &Pagination{ Page: page, Limit: limit, Total: total, TotalPages: totalPages, HasNext: page < totalPages, HasPrev: page > 1, } response := PaginatedResponse{ Response: Response{ Success: true, Data: data, Message: msg, Timestamp: time.Now().Unix(), RequestID: getRequestID(c), }, Pagination: pagination, } c.JSON(http.StatusOK, response) } // getRequestID 获取请求ID func getRequestID(c *gin.Context) string { if requestID, exists := c.Get("X-Request-ID"); exists { if id, ok := requestID.(string); ok { return id } } return "" } // getHTTPStatusFromCode 根据错误码获取HTTP状态码 func getHTTPStatusFromCode(code string) int { switch code { case "BAD_REQUEST", "INVALID_PARAMETER", "VALIDATION_ERROR": return http.StatusBadRequest case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED": return http.StatusUnauthorized case "FORBIDDEN", "PERMISSION_DENIED": return http.StatusForbidden case "NOT_FOUND", "USER_NOT_FOUND", "PHOTO_NOT_FOUND": return http.StatusNotFound case "CONFLICT", "USER_EXISTS", "DUPLICATE_KEY": return http.StatusConflict case "TOO_MANY_REQUESTS": return http.StatusTooManyRequests default: return http.StatusInternalServerError } } ``` ## ✅ 数据验证 ### 自定义验证器 ```go // validator/custom_validators.go - 自定义验证规则 package validator import ( "regexp" "strings" "github.com/go-playground/validator/v10" ) // RegisterCustomValidators 注册自定义验证器 func RegisterCustomValidators(v *validator.Validate) { v.RegisterValidation("username", validateUsername) v.RegisterValidation("password", validatePassword) v.RegisterValidation("phone", validatePhone) v.RegisterValidation("slug", validateSlug) v.RegisterValidation("file_ext", validateFileExtension) } // validateUsername 验证用户名 func validateUsername(fl validator.FieldLevel) bool { username := fl.Field().String() // 3-50个字符,只允许字母、数字、下划线、短横线 matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, username) return matched } // validatePassword 验证密码强度 func validatePassword(fl validator.FieldLevel) bool { password := fl.Field().String() // 至少8个字符,包含字母、数字和特殊字符 if len(password) < 8 { return false } hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password) hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password) hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]`).MatchString(password) return hasLetter && hasNumber && hasSpecial } // validatePhone 验证手机号 func validatePhone(fl validator.FieldLevel) bool { phone := fl.Field().String() // 支持多种手机号格式 patterns := []string{ `^1[3-9]\d{9}$`, // 中国大陆 `^\+1[2-9]\d{9}$`, // 美国 `^\+44[1-9]\d{8,9}$`, // 英国 } for _, pattern := range patterns { if matched, _ := regexp.MatchString(pattern, phone); matched { return true } } return false } // validateSlug 验证 URL 友好字符串 func validateSlug(fl validator.FieldLevel) bool { slug := fl.Field().String() matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug) return matched } // validateFileExtension 验证文件扩展名 func validateFileExtension(fl validator.FieldLevel) bool { filename := fl.Field().String() ext := strings.ToLower(getFileExtension(filename)) // 允许的图片文件扩展名 allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"} for _, allowedExt := range allowedExts { if ext == allowedExt { return true } } return false } // getFileExtension 获取文件扩展名 func getFileExtension(filename string) string { lastDot := strings.LastIndex(filename, ".") if lastDot == -1 { return "" } return filename[lastDot:] } // ValidationError 验证错误结构 type ValidationError struct { Field string `json:"field"` Value string `json:"value"` Tag string `json:"tag"` Message string `json:"message"` } // FormatValidationErrors 格式化验证错误 func FormatValidationErrors(err error) []ValidationError { var errors []ValidationError if validationErrors, ok := err.(validator.ValidationErrors); ok { for _, e := range validationErrors { errors = append(errors, ValidationError{ Field: e.Field(), Value: e.Value().(string), Tag: e.Tag(), Message: getErrorMessage(e), }) } } return errors } // getErrorMessage 获取错误消息 func getErrorMessage(e validator.FieldError) string { switch e.Tag() { case "required": return e.Field() + " is required" case "email": return e.Field() + " must be a valid email" case "min": return e.Field() + " must be at least " + e.Param() + " characters" case "max": return e.Field() + " must be at most " + e.Param() + " characters" case "username": return e.Field() + " must be 3-50 characters and contain only letters, numbers, underscores, and hyphens" case "password": return e.Field() + " must be at least 8 characters and contain letters, numbers, and special characters" case "phone": return e.Field() + " must be a valid phone number" case "slug": return e.Field() + " must be a valid URL slug" case "file_ext": return e.Field() + " must have a valid image file extension" default: return e.Field() + " is invalid" } } ``` ## 🛠️ 通用工具 ### 字符串工具 ```go // utils/string.go - 字符串工具 package utils import ( "crypto/rand" "math/big" "regexp" "strings" "unicode" ) // GenerateRandomString 生成随机字符串 func GenerateRandomString(length int) (string, error) { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" result := make([]byte, length) for i := range result { index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) if err != nil { return "", err } result[i] = charset[index.Int64()] } return string(result), nil } // ToSnakeCase 转换为蛇形命名 func ToSnakeCase(str string) string { var result []rune for i, r := range str { if unicode.IsUpper(r) { if i > 0 { result = append(result, '_') } result = append(result, unicode.ToLower(r)) } else { result = append(result, r) } } return string(result) } // ToCamelCase 转换为驼峰命名 func ToCamelCase(str string) string { parts := strings.Split(str, "_") for i := 1; i < len(parts); i++ { if len(parts[i]) > 0 { parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:] } } return strings.Join(parts, "") } // Truncate 截断字符串 func Truncate(str string, length int) string { if len(str) <= length { return str } return str[:length] + "..." } // IsValidEmail 验证邮箱格式 func IsValidEmail(email string) bool { pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` matched, _ := regexp.MatchString(pattern, email) return matched } // SanitizeFilename 清理文件名 func SanitizeFilename(filename string) string { // 移除或替换不安全的字符 reg := regexp.MustCompile(`[<>:"/\\|?*]`) return reg.ReplaceAllString(filename, "_") } // Contains 检查字符串切片是否包含指定字符串 func Contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } ``` ### 加密工具 ```go // utils/crypto.go - 加密工具 package utils import ( "crypto/md5" "crypto/sha256" "encoding/hex" "golang.org/x/crypto/bcrypt" ) // HashPassword 加密密码 func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(bytes), nil } // CheckPassword 验证密码 func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // MD5Hash 计算MD5哈希 func MD5Hash(data string) string { hash := md5.Sum([]byte(data)) return hex.EncodeToString(hash[:]) } // SHA256Hash 计算SHA256哈希 func SHA256Hash(data string) string { hash := sha256.Sum256([]byte(data)) return hex.EncodeToString(hash[:]) } ``` ## 🔗 中间件 ### CORS 中间件 ```go // middleware/cors.go - CORS 中间件 package middleware import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) // CORS 创建CORS中间件 func CORS() gin.HandlerFunc { config := cors.DefaultConfig() config.AllowOrigins = []string{"*"} config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"} config.ExposeHeaders = []string{"X-Request-ID"} config.AllowCredentials = true return cors.New(config) } ``` ### 请求ID中间件 ```go // middleware/request_id.go - 请求ID中间件 package middleware import ( "github.com/gin-gonic/gin" "github.com/google/uuid" ) // RequestID 请求ID中间件 func RequestID() gin.HandlerFunc { return func(c *gin.Context) { requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.New().String() } c.Set("X-Request-ID", requestID) c.Header("X-Request-ID", requestID) c.Next() } } ``` ## 💡 最佳实践 ### 包设计原则 1. **单一职责**: 每个包只负责一个明确的功能 2. **接口导向**: 优先定义接口,便于测试和替换 3. **零依赖**: 包应尽量减少外部依赖 4. **文档完善**: 提供清晰的使用文档和示例 5. **向后兼容**: 保持API的向后兼容性 ### 代码质量 1. **测试覆盖**: 为所有公共函数编写测试 2. **错误处理**: 合理处理和传播错误 3. **性能考虑**: 关注内存分配和性能影响 4. **线程安全**: 确保并发使用的安全性 5. **资源管理**: 及时释放资源 ### 使用建议 1. **导入路径**: 使用清晰的导入路径 2. **命名规范**: 遵循 Go 语言命名约定 3. **版本管理**: 使用语义化版本管理 4. **依赖管理**: 合理管理第三方依赖 5. **配置化**: 支持配置化使用 本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。