Files
photography/backend-old/pkg/CLAUDE.md
xujiang 010fe2a8c7
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 5m8s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
fix
2025-07-10 18:09:11 +08:00

793 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. **配置化**: 支持配置化使用
本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。