Files
junhong_cmp_fiber/.specify/memory/constitution.md
huang fb83c9a706 feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (pkg/errors/codes.go)
- 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go)
- 新增错误上下文管理 (pkg/errors/context.go)
- 增强 Panic 恢复中间件 (internal/middleware/recover.go)
- 新增完整的单元测试和集成测试
- 新增功能文档 (docs/003-error-handling/)
- 新增功能规范 (specs/003-error-handling/)
- 更新 CLAUDE.md 和 README.md
2025-11-15 12:17:44 +08:00

59 KiB
Raw Blame History

Version Change: 2.2.0 → 2.3.0 Date: 2025-11-11

NEW PRINCIPLES ADDED:

  • VIII. Access Logging Standards (访问日志规范) - NEW principle for comprehensive request/response logging

MODIFIED SECTIONS:

  • Added new Principle VIII with mandatory access logging requirements
    • Rule: ALL requests MUST be logged to access.log without exception
    • Rule: Request parameters (query + body) MUST be logged (limited to 50KB)
    • Rule: Response parameters (body) MUST be logged (limited to 50KB)
    • Rule: Logging MUST happen via centralized Logger middleware
    • Rule: No middleware can bypass access logging (including auth failures)
    • Rule: Body truncation MUST indicate "... (truncated)" when over limit
    • Rationale for comprehensive logging: debugging, audit trails, compliance

TEMPLATES REQUIRING UPDATES: .specify/templates/plan-template.md - Added access logging check in Constitution Check .specify/templates/tasks-template.md - Added access logging verification in Quality Gates

FOLLOW-UP ACTIONS:

  • None required - logging implementation already completed

RATIONALE: MINOR version bump (2.3.0) - New principle added for access logging standards. This establishes a mandatory governance rule that ALL HTTP requests must be logged with complete request and response data, regardless of middleware short-circuiting (auth failures, rate limits, etc.). This ensures:

  1. Complete audit trail for all API interactions
  2. Debugging capability for all failure scenarios
  3. Compliance with logging requirements
  4. No special cases or exceptions in logging

This is a MINOR bump (not PATCH) because it adds a new mandatory principle that affects the development workflow and quality gates, requiring verification that all middleware respects the logging standard. -->

============================================ Version Change: 2.2.0 → 2.3.0 Date: 2025-11-11

NEW PRINCIPLES ADDED:

  • VIII. Access Logging Standards (访问日志规范) - NEW principle for comprehensive request/response logging

MODIFIED SECTIONS:

  • Added new Principle VIII with mandatory access logging requirements
    • Rule: ALL requests MUST be logged to access.log without exception
    • Rule: Request parameters (query + body) MUST be logged (limited to 50KB)
    • Rule: Response parameters (body) MUST be logged (limited to 50KB)
    • Rule: Logging MUST happen via centralized Logger middleware
    • Rule: No middleware can bypass access logging (including auth failures)
    • Rule: Body truncation MUST indicate "... (truncated)" when over limit
    • Rationale for comprehensive logging: debugging, audit trails, compliance

TEMPLATES REQUIRING UPDATES: .specify/templates/plan-template.md - Added access logging check in Constitution Check .specify/templates/tasks-template.md - Added access logging verification in Quality Gates

FOLLOW-UP ACTIONS:

  • None required - logging implementation already completed

RATIONALE: MINOR version bump (2.3.0) - New principle added for access logging standards. This establishes a mandatory governance rule that ALL HTTP requests must be logged with complete request and response data, regardless of middleware short-circuiting (auth failures, rate limits, etc.). This ensures:

  1. Complete audit trail for all API interactions
  2. Debugging capability for all failure scenarios
  3. Compliance with logging requirements
  4. No special cases or exceptions in logging

This is a MINOR bump (not PATCH) because it adds a new mandatory principle that affects the development workflow and quality gates, requiring verification that all middleware respects the logging standard. -->

君鸿卡管系统 Constitution

Core Principles

I. Tech Stack Adherence (技术栈遵守)

规则 (RULES):

  • 开发时 MUST 严格遵守项目定义的技术栈Fiber + GORM + Viper + Zap + Lumberjack.v2 + Validator + sonic JSON + Asynq + PostgreSQL
  • MUST NOT 使用原生调用或绕过框架的快捷方式(禁止 database/sql 直接调用、禁止 net/http 替代 Fiber、禁止 encoding/json 替代 sonic
  • 所有 HTTP 路由和中间件 MUST 使用 Fiber 框架
  • 所有数据库操作 MUST 通过 GORM 进行
  • 所有配置管理 MUST 使用 Viper
  • 所有日志记录 MUST 使用 Zap + Lumberjack.v2
  • 所有 JSON 序列化 SHOULD 优先使用 sonic仅在必须使用标准库的场景如某些第三方库要求才使用 encoding/json
  • 所有异步任务 MUST 使用 Asynq
  • MUST 使用 Go 官方工具链:go fmtgo vetgolangci-lint
  • MUST 使用 Go Modules 进行依赖管理

理由 (RATIONALE):

一致的技术栈使用确保代码可维护性、团队协作效率和长期技术债务可控。绕过框架的"快捷方式"会导致代码碎片化、难以调试、性能不一致和安全漏洞。框架选择已经过深思熟虑必须信任并充分利用其生态系统。Go 官方工具链确保代码风格一致性和质量。


II. Code Quality Standards (代码质量标准)

规则 (RULES):

  • 代码 MUST 遵循项目分层架构:Handler → Service → Store → Model
  • Handler 层 MUST ONLY 处理 HTTP 请求/响应,不得包含业务逻辑
  • Service 层 MUST 包含所有业务逻辑,支持跨模块调用
  • Store 层 MUST 统一管理所有数据访问,支持事务处理
  • Model 层 MUST 定义清晰的数据结构和 DTO
  • 所有依赖 MUST 通过结构体字段进行依赖注入(不使用构造函数模式)
  • 所有公共错误 MUSTpkg/errors/ 中定义,使用统一错误码
  • 所有 API 响应 MUST 使用 pkg/response/ 的统一格式
  • 所有常量 MUSTpkg/constants/ 中定义和管理
  • 所有 Redis key MUST 通过 pkg/constants/ 中的 Key 生成函数统一管理
  • MUST 为所有导出的函数、类型和常量编写 Go 风格的文档注释(// FunctionName does something...
  • MUST 避免 magic numbers 和 magic strings使用常量定义

Go 代码风格要求:

  • MUST 使用 gofmt 格式化所有代码
  • MUST 遵循 Effective GoGo Code Review Comments
  • 变量命名 MUST 使用 Go 风格:userID(不是 userId)、HTTPServer(不是 HttpServer
  • 缩写词 MUST 全部大写或全部小写:URLIDHTTP(导出)或 urlidhttp(未导出)
  • 包名 MUST 简短、小写、单数、无下划线:userorderpkg(不是 usersuserServiceuser_service
  • 接口命名 SHOULD 使用 -er 后缀:ReaderWriterLogger(不是 ILoggerLoggerInterface

常量管理规范 (Constants Management):

  • 业务常量(状态码、类型枚举等)MUST 定义在 pkg/constants/constants.go 或按模块分文件
  • Redis key MUST 使用函数生成,不允许硬编码字符串拼接
  • Redis key 生成函数 MUST 遵循命名规范:Redis{Module}{Purpose}Key(params...)
  • Redis key 格式 MUST 使用冒号分隔:{module}:{purpose}:{identifier}
  • 示例:
    // 正确:使用常量和生成函数
    constants.SIMStatusActive
    constants.RedisSIMStatusKey(iccid)  // 生成 "sim:status:{iccid}"
    
    // 错误:硬编码和拼接
    status := "active"
    key := "sim:status:" + iccid
    

Magic Numbers 和硬编码规则 (Magic Numbers and Hardcoding Rules):

  • MUST NOT 在代码中直接使用 magic numbers未定义含义的数字字面量
  • MUST NOT 在代码中硬编码字符串字面量URL、状态码、配置值、业务规则等
  • 当相同的字面量值在 3 个或以上位置使用时,MUST 提取为常量
  • 已定义的常量 MUST 被使用,MUST NOT 重复硬编码相同的值
  • 只允许在以下情况使用字面量:
    • 语言特性:niltruefalse
    • 数学常量:01-1(用于循环、索引、比较等明确的上下文)
    • 一次性使用的临时值(测试数据、日志消息等)

正确的常量使用:

// pkg/constants/constants.go
const (
    DefaultPageSize = 20
    MaxPageSize     = 100
    MinPageSize     = 1
    
    SIMStatusActive   = "active"
    SIMStatusInactive = "inactive"
    SIMStatusSuspended = "suspended"
    
    OrderTypeRecharge = "recharge"
    OrderTypeTransfer = "transfer"
)

// internal/handler/user.go
func (h *Handler) ListUsers(c *fiber.Ctx) error {
    page := c.QueryInt("page", 1)
    pageSize := c.QueryInt("page_size", constants.DefaultPageSize) // ✅ 使用常量
    
    if pageSize > constants.MaxPageSize {  // ✅ 使用常量
        pageSize = constants.MaxPageSize
    }
    
    users, err := h.service.List(page, pageSize)
    // ...
}

// internal/service/sim.go
func (s *Service) Activate(iccid string) error {
    return s.store.UpdateStatus(iccid, constants.SIMStatusActive) // ✅ 使用常量
}

错误的硬编码模式:

// ❌ 硬编码 magic number在多处使用
func ListUsers(c *fiber.Ctx) error {
    pageSize := c.QueryInt("page_size", 20)  // ❌ 硬编码 20
    if pageSize > 100 {                       // ❌ 硬编码 100
        pageSize = 100
    }
    // ...
}

func ListOrders(c *fiber.Ctx) error {
    pageSize := c.QueryInt("page_size", 20)  // ❌ 重复硬编码 20
    // ...
}

// ❌ 硬编码字符串(在多处使用)
func ActivateSIM(iccid string) error {
    return UpdateStatus(iccid, "active")      // ❌ 硬编码 "active"
}

func IsSIMActive(sim *SIM) bool {
    return sim.Status == "active"             // ❌ 重复硬编码 "active"
}

// ❌ 已定义常量但不使用
// 常量已定义在 pkg/response/codes.go
func Success(c *fiber.Ctx, data any) error {
    return c.JSON(Response{
        Code: 0,  // ❌ 应该使用 errors.CodeSuccess
        Data: data,
    })
}

可接受的字面量使用:

// ✅ 语言特性和明确上下文的数字
if user == nil {                              // ✅ nil
    return errors.New("user not found")
}

for i := 0; i < len(items); i++ {            // ✅ 0, 1 用于循环
    // ...
}

if count == 1 {                               // ✅ 1 用于比较
    // 特殊处理单个元素
}

// ✅ 测试数据和日志消息
func TestUserCreate(t *testing.T) {
    user := &User{
        Name:  "Test User",                   // ✅ 测试数据
        Email: "test@example.com",
    }
    // ...
}

logger.Info("processing request",            // ✅ 日志消息
    zap.String("path", c.Path()),
    zap.Int("status", 200))

中文注释和输出规范 (Chinese Comments and Output Standards):

本项目面向中文开发者,为提高代码可读性和维护效率,SHOULD 优先使用中文进行注释和输出。

  • 代码注释implementation commentsSHOULD 使用中文
  • 日志消息log messagesSHOULD 使用中文
  • 用户可见的错误消息 MUST 使用中文(通过 pkg/errors/ 的双语消息支持)
  • 内部错误消息和调试日志 SHOULD 使用中文
  • Go 文档注释doc comments for exported APIsMAY 使用英文以保持生态兼容性,但中文注释更佳
  • 变量名、函数名、类型名 MUST 使用英文(遵循 Go 命名规范)

正确的中文注释使用:

// GetUserByID 根据用户 ID 获取用户信息
// 如果用户不存在,返回 NotFoundError
func (s *Service) GetUserByID(ctx context.Context, id string) (*User, error) {
    // 参数验证
    if id == "" {
        return nil, errors.New(errors.CodeInvalidParam, "用户 ID 不能为空")
    }
    
    // 从存储层获取用户
    user, err := s.store.GetByID(ctx, id)
    if err != nil {
        s.logger.Error("获取用户失败",
            zap.String("user_id", id),
            zap.Error(err))
        return nil, fmt.Errorf("获取用户失败: %w", err)
    }
    
    // 检查用户状态
    if user.Status != constants.UserStatusActive {
        s.logger.Warn("用户状态异常",
            zap.String("user_id", id),
            zap.String("status", user.Status))
    }
    
    return user, nil
}

正确的中文日志使用:

// 信息日志
logger.Info("服务启动成功",
    zap.String("host", cfg.Server.Host),
    zap.Int("port", cfg.Server.Port))

// 警告日志
logger.Warn("Redis 连接延迟较高",
    zap.Duration("latency", latency),
    zap.String("threshold", "50ms"))

// 错误日志
logger.Error("数据库连接失败",
    zap.String("host", cfg.DB.Host),
    zap.Int("port", cfg.DB.Port),
    zap.Error(err))

函数复杂度和职责分离 (Function Complexity and Responsibility Separation):

  • 函数长度 MUST NOT 超过合理范围(通常 50-100 行,核心逻辑建议 ≤ 50 行)
  • 超过 100 行的函数 MUST 拆分为多个小函数,每个函数只负责一件事
  • main() 函数 MUST 只做编排orchestration不包含具体实现逻辑
  • main() 函数中的每个初始化步骤 SHOULD 提取为独立的辅助函数
  • 编排函数orchestratorMUST 清晰表达流程,避免嵌套的实现细节
  • MUST 遵循单一职责原则Single Responsibility Principle
  • 虽然 MUST NOT 过度封装,但 MUST 在职责边界清晰的地方进行适度分离

理由 (RATIONALE):

清晰的分层架构和代码组织使代码易于理解、测试和维护。统一的错误处理和响应格式提升 API 一致性和客户端集成体验。依赖注入模式便于单元测试和模块替换。集中管理常量和 Redis key 避免拼写错误、重复定义和命名不一致提升代码可维护性和重构安全性。Redis key 统一管理便于监控、调试和缓存策略调整。遵循 Go 官方代码风格确保代码一致性和可读性。

函数复杂度控制和职责分离的理由:

  1. 可读性: 小函数易于阅读和理解,特别是 main 函数清晰表达程序流程
  2. 可测试性: 小函数易于编写单元测试,提高测试覆盖率
  3. 可维护性: 职责单一的函数修改风险低,不易引入 bug
  4. 可复用性: 提取的辅助函数可以在其他地方复用
  5. 减少认知负担: 阅读者不需要同时理解过多细节
  6. 便于重构: 小函数更容易安全地重构和优化

避免硬编码和强制使用常量的规则能够:

  1. 提高可维护性:修改常量值只需改一处,不需要搜索所有硬编码位置
  2. 减少错误:避免手动输入错误(拼写错误、大小写错误)
  3. 增强可读性constants.MaxPageSize100 更能表达意图
  4. 便于重构IDE 可以追踪常量使用,重命名时不会遗漏
  5. 统一业务规则:确保所有地方使用相同的业务规则值
  6. "3 次规则"提供明确的阈值,避免过早优化,同时确保重复值被及时抽取

使用中文注释和日志的理由:

  1. 提高团队效率:中文开发团队阅读中文注释更快速准确
  2. 降低理解成本:减少翻译和理解偏差
  3. 改善调试体验:中文日志更易于排查问题
  4. 保持生态兼容:导出 API 的文档注释可保留英文

III. Testing Standards (测试标准)

规则 (RULES):

  • 所有核心业务逻辑Service 层)MUST 有单元测试覆盖
  • 所有 API 端点 MUST 有集成测试覆盖
  • 所有数据库操作 SHOULD 有事务回滚测试
  • 测试 MUST 使用 Go 标准测试框架(testing 包)
  • 测试文件 MUST 与源文件同目录,命名为 *_test.go
  • 测试函数 MUST 使用 Test 前缀:func TestUserCreate(t *testing.T)
  • 基准测试 MUST 使用 Benchmark 前缀:func BenchmarkUserCreate(b *testing.B)
  • 测试 MUST 可独立运行,不依赖外部服务(使用 mock 或 testcontainers
  • 单元测试 MUST 在 100ms 内完成
  • 集成测试 SHOULD 在 1s 内完成
  • 测试覆盖率 SHOULD 达到 70% 以上(核心业务代码必须 90% 以上)
  • 测试 MUST 使用 table-driven tests 处理多个测试用例
  • 测试 MUST 使用 t.Helper() 标记辅助函数

Table-Driven Test 示例:

func TestUserValidate(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {"valid user", User{Name: "John", Email: "john@example.com"}, false},
        {"empty name", User{Name: "", Email: "john@example.com"}, true},
        {"invalid email", User{Name: "John", Email: "invalid"}, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

理由 (RATIONALE):

高质量的测试是代码质量的基石。单元测试确保业务逻辑正确性,集成测试确保模块间协作正常。快速的测试执行时间保证开发效率。测试独立性避免环境依赖导致的 flaky tests。Table-driven tests 使测试更简洁、易于扩展和维护,这是 Go 社区的最佳实践。


IV. User Experience Consistency (用户体验一致性)

规则 (RULES):

  • 所有 API 响应 MUST 使用统一的 JSON 格式:
    {
      "code": 0,
      "message": "success",
      "data": {},
      "timestamp": "2025-11-10T15:30:00Z"
    }
    
  • 所有错误响应 MUST 包含明确的错误码和错误消息(中英文双语)
  • 所有 API 端点 MUST 遵循 RESTful 设计原则
  • 所有分页 API MUST 使用统一的分页参数:pagepage_sizetotal
  • 所有时间字段 MUST 使用 ISO 8601 格式RFC3339
  • 所有货币金额 MUST 使用整数表示(分为单位),避免浮点精度问题
  • 所有布尔字段 MUST 使用 true/false,不使用 0/1
  • API 版本 MUST 通过 URL 路径管理(如 /api/v1/...

理由 (RATIONALE):

一致的 API 设计降低客户端开发成本,减少集成错误。统一的数据格式和错误处理提升系统可预测性。清晰的时间和金额表示避免常见的数据处理错误。


V. Performance Requirements (性能要求)

规则 (RULES):

  • API 响应时间P95MUST < 200ms数据库查询 < 50ms
  • API 响应时间P99MUST < 500ms
  • 批量操作 MUST 使用批量查询/插入,避免 N+1 查询问题
  • 所有数据库查询 MUST 有适当的索引支持
  • 列表查询 MUST 实现分页,默认 page_size=20,最大 page_size=100
  • 异步任务 MUST 用于非实时操作(批量同步、分佣计算等)
  • 内存使用API 服务)SHOULD < 500MB正常负载
  • 内存使用Worker 服务)SHOULD < 1GB正常负载
  • 数据库连接池 MUST 配置合理(MaxOpenConns=25, MaxIdleConns=10, ConnMaxLifetime=5m
  • Redis 连接池 MUST 配置合理(PoolSize=10, MinIdleConns=5
  • 并发操作 SHOULD 使用 goroutines 和 channels不是线程池模式
  • MUST 使用 context.Context 进行超时和取消控制
  • MUST 使用 sync.Pool 复用频繁分配的对象(如缓冲区)

理由 (RATIONALE):

性能要求确保系统在生产环境下的稳定性和用户体验。批量操作和异步任务避免阻塞主流程。合理的连接池配置平衡性能和资源消耗。明确的性能指标便于监控和优化。使用 Go 的并发原语goroutines、channels而不是传统的线程池模式充分发挥 Go 的并发优势。


VI. Go Idiomatic Design Principles (Go 语言惯用设计原则)

核心理念:写 Go 味道的代码,不要写 Java 味道的代码

包组织 (Package Organization)

MUST 遵循的原则:

  • 包结构 MUST 扁平化,避免深层嵌套(最多 2-3 层)
  • MUST 按功能组织,不是按层次组织
  • 包名 MUST 描述功能,不是类型(http 不是 httputilshandlers

正确的 Go 风格:

internal/
├── user/          # user 功能的所有代码
│   ├── handler.go # HTTP handlers
│   ├── service.go # 业务逻辑
│   ├── store.go   # 数据访问
│   └── model.go   # 数据模型
├── order/
│   ├── handler.go
│   ├── service.go
│   └── store.go
└── sim/
    ├── handler.go
    ├── service.go
    └── store.go

错误的 Java 风格(禁止):

internal/
├── handlers/
│   ├── user/
│   │   └── UserHandler.go      # ❌ 类名式命名
│   └── order/
│       └── OrderHandler.go
├── services/
│   ├── user/
│   │   ├── IUserService.go     # ❌ 接口前缀 I
│   │   └── UserServiceImpl.go  # ❌ Impl 后缀
│   └── impls/                   # ❌ 过度抽象
├── repositories/                # ❌ Repository 模式过度使用
│   └── interfaces/
└── models/
    └── entities/
        └── dto/                 # ❌ 过度分层

接口设计 (Interface Design)

MUST 遵循的原则:

  • 接口 MUST 小而专注1-3 个方法),不是大而全
  • 接口 SHOULD 在使用方定义,不是实现方(依赖倒置)
  • 接口命名 SHOULD 使用 -er 后缀:ReaderWriterStorer
  • MUST NOT 使用 I 前缀或 Interface 后缀
  • MUST NOT 创建只有一个实现的接口(除非明确需要抽象)

正确的 Go 风格:

// 小接口,在使用方定义
type UserStorer interface {
    Create(ctx context.Context, user *User) error
    GetByID(ctx context.Context, id string) (*User, error)
}

// 具体实现
type PostgresStore struct {
    db *gorm.DB
}

func (s *PostgresStore) Create(ctx context.Context, user *User) error {
    return s.db.WithContext(ctx).Create(user).Error
}

错误的 Java 风格(禁止):

// ❌ 大接口,方法过多
type IUserRepository interface {
    Create(user *User) error
    Update(user *User) error
    Delete(id string) error
    FindByID(id string) (*User, error)
    FindByEmail(email string) (*User, error)
    FindAll() ([]*User, error)
    FindByStatus(status string) ([]*User, error)
    Count() (int, error)
    // ... 更多方法
}

// ❌ I 前缀
type IUserService interface {}

// ❌ 不必要的抽象层
type UserRepositoryImpl struct {}  // 只有一个实现

错误处理 (Error Handling)

MUST 遵循的原则:

  • 错误 MUST 显式返回和检查不使用异常panic/recover
  • 错误处理 MUST 紧跟错误产生的代码
  • MUST 使用 errors.Is()errors.As() 检查错误类型
  • MUST 使用 fmt.Errorf() 包装错误,保留错误链
  • 自定义错误 SHOULD 实现 error 接口
  • panic MUST ONLY 用于不可恢复的程序错误

正确的 Go 风格:

func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    user, err := s.store.GetByID(ctx, id)
    if err != nil {
        // 包装错误,添加上下文
        return nil, fmt.Errorf("get user by id %s: %w", id, err)
    }
    return user, nil
}

// 自定义错误类型
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}

错误的 Java 风格(禁止):

// ❌ 使用 panic 代替错误返回
func GetUser(id string) *User {
    user, err := db.Find(id)
    if err != nil {
        panic(err)  // ❌ 不要这样做
    }
    return user
}

// ❌ try-catch 风格
func DoSomething() {
    defer func() {
        if r := recover(); r != nil {  // ❌ 滥用 recover
            log.Println("recovered:", r)
        }
    }()
    // ...
}

// ❌ 异常类层次结构
type Exception interface {         // ❌ 不需要
    GetMessage() string
    GetStackTrace() []string
}
type BusinessException struct {}   // ❌ 过度设计
type ValidationException struct {} // ❌ 过度设计

结构体和方法 (Structs and Methods)

MUST 遵循的原则:

  • 结构体 MUST 简单直接不是类class的替代品
  • MUST NOT 为每个字段创建 getter/setter 方法
  • MUST 直接访问导出的字段(大写开头)
  • MUST 使用组合composition而不是继承inheritance
  • 构造函数 SHOULD 命名为 NewNewXxx,返回具体类型
  • MUST NOT 使用构造器模式Builder Pattern除非真正需要

正确的 Go 风格:

// 简单结构体,导出字段直接访问
type User struct {
    ID    string
    Name  string
    Email string
}

// 简单构造函数
func NewUser(name, email string) *User {
    return &User{
        ID:    generateID(),
        Name:  name,
        Email: email,
    }
}

// 使用组合
type Service struct {
    store  UserStorer
    logger *zap.Logger
}

func NewService(store UserStorer, logger *zap.Logger) *Service {
    return &Service{
        store:  store,
        logger: logger,
    }
}

错误的 Java 风格(禁止):

// ❌ 私有字段 + getter/setter
type User struct {
    id    string  // ❌ 不需要私有
    name  string
    email string
}

func (u *User) GetID() string { return u.id }       // ❌ 不需要
func (u *User) SetID(id string) { u.id = id }      // ❌ 不需要
func (u *User) GetName() string { return u.name }  // ❌ 不需要
func (u *User) SetName(name string) { u.name = name } // ❌ 不需要

// ❌ 构造器模式(不必要)
type UserBuilder struct {
    user *User
}

func NewUserBuilder() *UserBuilder {  // ❌ 过度设计
    return &UserBuilder{user: &User{}}
}

func (b *UserBuilder) WithName(name string) *UserBuilder {
    b.user.name = name
    return b
}

func (b *UserBuilder) Build() *User {
    return b.user
}

// ❌ 工厂模式(过度抽象)
type UserFactory interface {          // ❌ 不需要
    CreateUser(name string) *User
}

并发模式 (Concurrency Patterns)

MUST 遵循的原则:

  • MUST 使用 goroutines 和 channels不是线程和锁大多数情况
  • MUST 使用 context.Context 传递取消信号
  • MUST 遵循"通过通信共享内存,不要通过共享内存通信"
  • SHOULD 使用 sync.WaitGroup 等待 goroutines 完成
  • SHOULD 使用 sync.Once 确保只执行一次
  • MUST NOT 创建线程池类Go 运行时已处理)

正确的 Go 风格:

// 使用 goroutines 和 channels
func ProcessBatch(ctx context.Context, items []Item) error {
    results := make(chan Result, len(items))
    errors := make(chan error, len(items))

    for _, item := range items {
        go func(item Item) {
            result, err := process(ctx, item)
            if err != nil {
                errors <- err
                return
            }
            results <- result
        }(item)
    }

    // 收集结果
    for i := 0; i < len(items); i++ {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case err := <-errors:
            return err
        case <-results:
            // 处理结果
        }
    }
    return nil
}

// 使用 sync.WaitGroup
func ProcessConcurrently(items []Item) {
    var wg sync.WaitGroup
    for _, item := range items {
        wg.Add(1)
        go func(item Item) {
            defer wg.Done()
            process(item)
        }(item)
    }
    wg.Wait()
}

错误的 Java 风格(禁止):

// ❌ 线程池模式
type ThreadPool struct {              // ❌ 不需要
    workers   int
    taskQueue chan Task
    wg        sync.WaitGroup
}

func NewThreadPool(workers int) *ThreadPool { // ❌ Go 运行时已处理
    return &ThreadPool{
        workers:   workers,
        taskQueue: make(chan Task, 100),
    }
}

// ❌ Future/Promise 模式(不需要单独实现)
type Future struct {                  // ❌ 直接使用 channel
    result chan interface{}
    err    chan error
}

// ❌ 过度使用 mutex应该用 channel
type SafeMap struct {                 // ❌ 考虑使用 sync.Map 或 channel
    mu   sync.Mutex
    data map[string]interface{}
}

func (m *SafeMap) Get(key string) interface{} {
    m.mu.Lock()
    defer m.mu.Unlock()
    return m.data[key]
}

命名约定 (Naming Conventions)

MUST 遵循的原则:

  • 变量名 MUST 简短且符合上下文:
    • 短作用域用短名字:i, j, k 用于循环
    • 长作用域用描述性名字:userRepository, configManager
  • 缩写词 MUST 保持一致的大小写:URL, HTTP, ID(不是 Url, Http, Id
  • MUST NOT 使用匈牙利命名法或类型前缀:strName, arrUsers(禁止)
  • MUST NOT 使用下划线连接(除了测试和包名):user_service(禁止,应该是 userserviceUserService
  • 方法接收者名称 SHOULD 使用 1-2 个字母的缩写,全文件保持一致

正确的 Go 风格:

// 短作用域,短名字
for i, user := range users {
    fmt.Println(i, user.Name)
}

// 长作用域,描述性名字
func ProcessUserRegistration(ctx context.Context, req *RegistrationRequest) error {
    // ...
}

// 缩写词大小写一致
type UserAPI struct {
    BaseURL string
    APIKey  string
    UserID  int64
}

// 方法接收者简短一致
type UserService struct {}

func (s *UserService) Create(user *User) error { // s 用于 service
    return s.store.Create(user)
}

func (s *UserService) Update(user *User) error { // 保持一致使用 s
    return s.store.Update(user)
}

错误的 Java 风格(禁止):

// ❌ 过长的变量名
func ProcessRegistration() {
    userRegistrationRequest := &Request{}     // ❌ 太长
    userRegistrationValidator := NewValidator() // ❌ 太长
    userRegistrationService := NewService()   // ❌ 太长
}

// ❌ 匈牙利命名
strUserName := "John"    // ❌
intUserAge := 25         // ❌
arrUserList := []User{}  // ❌

// ❌ 下划线命名
type User_Service struct {}  // ❌ 应该是 UserService
func Get_User_By_Id() {}     // ❌ 应该是 GetUserByID

// ❌ 缩写词大小写不一致
type UserApi struct {    // ❌ 应该是 UserAPI
    BaseUrl string       // ❌ 应该是 BaseURL
    ApiKey  string       // ❌ 应该是 APIKey
    UserId  int64        // ❌ 应该是 UserID
}

// ❌ 方法接收者不一致或过长
func (userService *UserService) Create() {}  // ❌ 太长
func (us *UserService) Update() {}           // ❌ 与上面不一致
func (this *UserService) Delete() {}         // ❌ 不要使用 this/self

反模式清单 (Anti-Patterns to Avoid)

严格禁止的 Java 风格模式:

  1. 过度抽象:不需要的接口、工厂、构造器
  2. Getter/Setter:直接访问导出字段
  3. 继承层次:使用组合,不是嵌入
  4. 异常处理:使用错误返回,不是 panic/recover
  5. 单例模式:使用包级别变量或 sync.Once
  6. 线程池:直接使用 goroutines
  7. 深层包嵌套:保持扁平结构
  8. 类型前缀IService, AbstractBase, ServiceImpl
  9. Bean 风格:不需要 POJO/JavaBean 模式
  10. 过度 DI 框架:简单直接的依赖注入

理由 (RATIONALE):

Go 语言的设计哲学强调简单性、直接性和实用性。Java 风格的模式(深层继承、过度抽象、复杂的设计模式)违背了 Go 的核心理念。Go 提供了更简单、更高效的方式来解决相同的问题:接口的隐式实现、结构体组合、显式错误处理、轻量级并发。遵循 Go 惯用法不仅使代码更地道,还能充分发挥 Go 的性能优势和简洁性。


VII. Documentation Standards (文档规范)

规则 (RULES):

  • 每个功能完成后 MUSTdocs/ 目录创建总结文档
  • 总结文档路径 MUST 遵循规范:docs/{feature-id}/ 对应 specs/{feature-id}/
  • 总结文档文件名 MUST 使用中文命名(例如:功能总结.md使用指南.md架构说明.md
  • 总结文档内容 MUST 使用中文编写
  • 每次添加新功能总结文档时 MUST 同步更新 README.md
  • README.md 中的功能描述 MUST 简短精炼,让首次接触项目的开发者能快速了解
  • README.md 的功能描述 SHOULD 控制在 2-3 句话以内

文档结构示例:

specs/001-fiber-middleware-integration/  # 功能规划文档(设计阶段)
├── spec.md
├── plan.md
├── tasks.md
└── quickstart.md

docs/001-fiber-middleware-integration/   # 功能总结文档(完成阶段)
├── 功能总结.md                          # 功能概述和实现要点
├── 使用指南.md                          # 如何使用该功能
└── 架构说明.md                          # 架构设计和技术决策(可选)

README.md                                 # 项目主文档
## 核心功能
- **认证中间件**:基于 Redis 的 Token 认证
- **限流中间件**:基于 IP 的限流,支持可配置的限制和存储后端

正确的文档组织:

<!-- docs/002-user-management/功能总结.md -->
# 用户管理功能总结

## 功能概述

实现了完整的用户 CRUD 操作,包括用户创建、查询、更新、删除和列表分页。

## 核心实现

- Handler: `internal/handler/user.go`
- Service: `internal/service/user.go`
- Store: `internal/store/postgres/user.go`
- Model: `internal/model/user.go`

## API 端点

- `POST /api/v1/users` - 创建用户
- `GET /api/v1/users/:id` - 获取用户
- `PUT /api/v1/users/:id` - 更新用户
- `DELETE /api/v1/users/:id` - 删除用户
- `GET /api/v1/users` - 用户列表(分页)

## 技术要点

- 使用 GORM 进行数据访问
- 使用 Validator 进行参数验证
- 支持事务处理
- 统一错误处理和响应格式

详细使用说明请参阅 [使用指南.md](./使用指南.md)
<!-- README.md 更新示例 -->
## 核心功能

- **认证中间件**:基于 Redis 的 Token 认证
- **限流中间件**IP 限流,支持 Memory 和 Redis 存储
- **用户管理**:完整的用户 CRUD 和分页列表功能
- **日志系统**Zap 结构化日志,自动轮转和请求追踪

详细文档:
- [认证中间件](docs/001-fiber-middleware-integration/功能总结.md)
- [用户管理](docs/002-user-management/功能总结.md)

错误的文档组织(禁止):

❌ docs/feature-summary.md               # 所有功能混在一个文件
❌ docs/001-summary.md                   # 英文命名
❌ docs/001/summary.md                   # 英文内容
❌ specs/001.../implementation.md        # 总结文档放在 specs/ 下
❌ README.md 中没有更新新功能             # 遗漏 README 更新
❌ README.md 功能描述过长(超过 5 句话)  # 描述冗长

README.md 更新要求:

  • 简洁性:每个功能 2-3 句话描述核心价值
  • 可读性:使用中文,便于中文开发者快速理解
  • 链接性:提供到详细文档的链接
  • 分类性:按功能模块分组(如"核心功能"、"中间件"、"业务模块"等)

理由 (RATIONALE):

统一的文档结构使新成员快速了解项目功能和架构。中文文档降低中文团队的阅读门槛,提高协作效率。将总结文档与设计文档分离(docs/ vs specs/),清晰区分"计划做什么"和"做了什么"。强制更新 README.md 确保项目主文档始终反映最新功能。简短的 README 描述让首次接触项目的开发者能在 5 分钟内了解项目全貌。


VIII. Access Logging Standards (访问日志规范)

规则 (RULES):

  • 所有 HTTP 请求 MUST 被记录到 access.log,无例外
  • 访问日志 MUST 记录完整的请求参数query 参数 + request body
  • 访问日志 MUST 记录完整的响应参数response body
  • 请求/响应 body MUST 限制大小为 50KB超过部分截断并标注 ... (truncated)
  • 访问日志 MUST 通过统一的 Logger 中间件(pkg/logger/Middleware())记录
  • 任何中间件 的短路返回(认证失败、限流拒绝、参数验证失败等)MUST NOT 绕过访问日志
  • 访问日志 MUST 包含以下字段(最低要求):
    • method: HTTP 方法
    • path: 请求路径
    • query: Query 参数字符串
    • status: HTTP 状态码
    • duration_ms: 请求耗时(毫秒)
    • request_id: 请求唯一 ID
    • ip: 客户端 IP
    • user_agent: 用户代理
    • user_id: 用户 ID认证后有值否则为空
    • request_body: 请求体(限制 50KB
    • response_body: 响应体(限制 50KB
  • 访问日志 SHOULD 使用 JSON 格式,便于日志分析和监控
  • 访问日志文件 MUST 配置自动轮转(基于大小或时间)

正确的访问日志示例:

{
  "level": "info",
  "timestamp": "2025-11-11T17:45:03.186+0800",
  "message": "",
  "method": "POST",
  "path": "/api/v1/users",
  "query": "page=1&size=10",
  "status": 400,
  "duration_ms": 0.035,
  "request_id": "f1d8b767-dfb3-4588-9fa0-8a97e5337184",
  "ip": "127.0.0.1",
  "user_agent": "curl/8.7.1",
  "user_id": "",
  "request_body": "{\"username\":\"testuser\",\"email\":\"test@example.com\"}",
  "response_body": "{\"code\":1001,\"data\":null,\"msg\":\"缺失认证令牌\",\"timestamp\":\"2025-11-11T17:45:03+08:00\"}"
}

Logger 中间件实现要求:

// pkg/logger/middleware.go
func Middleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        startTime := time.Now()
        
        // 在 c.Next() 之前读取请求 body
        requestBody := truncateBody(c.Body(), MaxBodyLogSize)
        queryParams := string(c.Request().URI().QueryString())
        
        // 处理请求(可能被中间件短路返回)
        err := c.Next()
        
        // 在 c.Next() 之后读取响应 body无论是否短路
        responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize)
        
        // 记录完整的访问日志(包含请求和响应参数)
        accessLogger.Info("",
            zap.String("method", c.Method()),
            zap.String("path", c.Path()),
            zap.String("query", queryParams),
            zap.Int("status", c.Response().StatusCode()),
            zap.Float64("duration_ms", time.Since(startTime).Seconds()*1000),
            zap.String("request_id", getRequestID(c)),
            zap.String("ip", c.IP()),
            zap.String("user_agent", c.Get("User-Agent")),
            zap.String("user_id", getUserID(c)),
            zap.String("request_body", requestBody),
            zap.String("response_body", responseBody),
        )
        
        return err
    }
}

禁止的做法:

// ❌ 在中间件中跳过日志记录
func AuthMiddleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        if !isAuthenticated(c) {
            // ❌ 直接返回,没有记录到 access.log
            return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
        }
        return c.Next()
    }
}

// ❌ 只在部分路由记录日志
app.Use("/api/v1/users", logger.Middleware())  // ❌ 其他路由没有日志

// ❌ 不记录请求/响应 body
accessLogger.Info("",
    zap.String("method", c.Method()),
    zap.String("path", c.Path()),
    zap.Int("status", c.Response().StatusCode()),
    // ❌ 缺少 request_body 和 response_body
)

理由 (RATIONALE):

完整的访问日志是系统可观测性的基础,对于以下场景至关重要:

  1. 问题排查:当用户报告错误时,通过 request_id 可以追溯完整的请求/响应数据,快速定位问题
  2. 安全审计:记录所有请求(包括认证失败、参数验证失败等)可以追踪潜在的安全攻击
  3. 性能分析:通过 duration_ms 和请求参数可以分析慢查询和性能瓶颈
  4. 合规要求:某些行业(金融、医疗等)要求完整的操作审计日志
  5. 用户行为分析:通过 user_id 和请求参数可以分析用户行为模式
  6. 无例外原则:确保没有请求"逃脱"日志记录,避免日志盲点

通过强制在 Logger 中间件中统一记录,确保:

  • 中间件的短路返回auth 失败、rate limit 等)不会绕过日志
  • 所有请求的日志格式统一,便于日志分析工具处理
  • 50KB 限制平衡了日志完整性和存储成本

IX. Database Design Principles (数据库设计原则)

规则 (RULES):

  • 数据库表之间 MUST NOT 建立外键约束Foreign Key Constraints
  • GORM 模型之间 MUST NOT 使用 ORM 关联关系(foreignKeyreferenceshasManybelongsTo 等标签)
  • 表之间的关联 MUST 通过存储关联 ID 字段手动维护
  • 关联数据查询 MUST 在代码层面显式执行,不依赖 ORM 的自动加载Lazy Loading或预加载Eager Loading
  • 模型结构体 MUST ONLY 包含简单字段,不应包含其他模型的嵌套引用
  • 数据库迁移脚本 MUST NOT 包含外键约束定义
  • 数据库迁移脚本 MUST NOT 包含触发器用于维护关联数据
  • 时间字段(created_atupdated_at)的更新 MUST 由 GORM 自动处理,不使用数据库触发器

正确的关联设计:

// ✅ User 模型 - 完全独立
type User struct {
    BaseModel
    Username string `gorm:"uniqueIndex;not null;size:50"`
    Email    string `gorm:"uniqueIndex;not null;size:100"`
    Password string `gorm:"not null;size:255"`
    Status   string `gorm:"not null;size:20;default:'active'"`
}

// ✅ Order 模型 - 仅存储 UserID
type Order struct {
    BaseModel
    OrderID string `gorm:"uniqueIndex;not null;size:50"`
    UserID  uint   `gorm:"not null;index"` // 仅存储 ID无 ORM 关联
    Amount  int64  `gorm:"not null"`
    Status  string `gorm:"not null;size:20;default:'pending'"`
}

// ✅ 手动查询关联数据
func (s *OrderService) GetOrderWithUser(ctx context.Context, orderID uint) (*OrderDetail, error) {
    // 查询订单
    order, err := s.store.Order.GetByID(ctx, orderID)
    if err != nil {
        return nil, err
    }
    
    // 手动查询关联的用户
    user, err := s.store.User.GetByID(ctx, order.UserID)
    if err != nil {
        return nil, err
    }
    
    // 组装返回数据
    return &OrderDetail{
        Order: order,
        User:  user,
    }, nil
}

错误的关联设计(禁止):

// ❌ 使用 GORM 外键关联
type Order struct {
    BaseModel
    OrderID string
    UserID  uint
    User    *User  `gorm:"foreignKey:UserID"` // ❌ 禁止
    Amount  int64
}

// ❌ 使用 GORM hasMany 关联
type User struct {
    BaseModel
    Username string
    Orders   []Order `gorm:"foreignKey:UserID"` // ❌ 禁止
}

// ❌ 在迁移脚本中定义外键约束
CREATE TABLE tb_order (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    CONSTRAINT fk_order_user FOREIGN KEY (user_id) 
        REFERENCES tb_user(id) ON DELETE RESTRICT --  禁止
);

// ❌ 使用数据库触发器更新时间
CREATE TRIGGER update_order_updated_at 
    BEFORE UPDATE ON tb_order
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column(); --  禁止

// ❌ 依赖 GORM 预加载
orders, err := db.Preload("User").Find(&orders) // ❌ 禁止

GORM BaseModel 自动时间管理:

// ✅ GORM 自动处理时间字段
type BaseModel struct {
    ID        uint           `gorm:"primarykey"`
    CreatedAt time.Time      // GORM 自动填充创建时间
    UpdatedAt time.Time      // GORM 自动更新修改时间
    DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除支持
}

// 创建记录时GORM 自动设置 CreatedAt 和 UpdatedAt
db.Create(&user) 

// 更新记录时GORM 自动更新 UpdatedAt
db.Save(&user)

理由 (RATIONALE):

移除数据库外键约束和 ORM 关联关系的理由:

  1. 灵活性:业务逻辑完全在代码中控制,不受数据库约束限制。例如删除用户时可以根据业务需求决定是级联删除订单、保留订单还是转移订单,而不是被 ON DELETE CASCADE/RESTRICT 强制约束。

  2. 性能:无外键约束意味着无数据库层面的引用完整性检查开销。在高并发场景下,外键检查和锁竞争会成为性能瓶颈。

  3. 简单直接:显式的关联数据查询使数据流向清晰可见,代码行为明确。避免了 ORM 的"魔法"行为N+1 查询问题、意外的预加载、Lazy Loading 陷阱)。

  4. 可控性:开发者完全掌控何时查询关联数据、查询哪些关联数据。可以根据场景优化查询(批量查询、缓存等),而不是依赖 ORM 的自动行为。

  5. 可维护性:数据库 schema 更简单,迁移更容易。修改表结构不需要处理复杂的外键依赖关系。代码重构时不会被数据库约束限制。

  6. 分布式友好:在微服务和分布式数据库场景下,外键约束往往无法跨数据库工作。手动维护关联从设计上就支持未来的服务拆分。

  7. GORM 基础功能:保留 GORM 的核心价值(自动时间管理、软删除、查询构建、事务支持),去除复杂的关联功能,达到简单性和功能性的平衡。

这种设计哲学符合"明确优于隐式"的原则,代码的行为一目了然,没有隐藏的数据库操作和 ORM 魔法。


X. Error Handling Standards (错误处理规范)

规则 (RULES):

  • 所有 API 错误响应 MUST 使用统一的 JSON 格式(通过 pkg/errors/ 全局 ErrorHandler
  • 所有 Handler 层错误 MUST 通过返回 error 传递给全局 ErrorHandlerMUST NOT 手动构造错误响应
  • 所有 业务错误 MUST 使用 pkg/errors.New()pkg/errors.Wrap() 创建 AppError,并指定错误码
  • 所有 错误码 MUSTpkg/errors/codes.go 中统一定义和管理
  • 所有 Panic MUST 被 Recover 中间件自动捕获,转换为 500 错误响应
  • 所有 错误日志 MUST 包含完整的请求上下文Request ID、路径、方法、参数等
  • 5xx 服务端错误 MUST 自动脱敏,只返回通用错误消息,原始错误仅记录到日志
  • 4xx 客户端错误 MAY 返回具体业务错误消息(如"用户名已存在"
  • MUST NOT 在业务代码中主动 panic(除非遇到不可恢复的编程错误)
  • MUST NOT 在 Handler 中直接使用 c.Status().JSON() 返回错误响应

统一错误响应格式:

{
  "code": 1001,
  "data": null,
  "msg": "参数验证失败",
  "timestamp": "2025-11-15T10:00:00+08:00"
}

HTTP Header 包含:

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

错误码分类系统:

  • 0: 成功
  • 1000-1999: 客户端错误4xx HTTP 状态码,日志级别 Warn
    • 1001: 参数验证失败 → 400
    • 1002: 缺少认证令牌 → 401
    • 1003: 无效认证令牌 → 401
    • 1004: 认证凭证无效 → 401
    • 1005: 禁止访问 → 403
    • 1006: 资源未找到 → 404
    • 1007: 资源冲突 → 409
    • 1008: 请求过多 → 429
    • 1009: 请求体过大 → 413
  • 2000-2999: 服务端错误5xx HTTP 状态码,日志级别 Error
    • 2001: 内部服务器错误 → 500
    • 2002: 数据库错误 → 500
    • 2003: 缓存服务错误 → 500
    • 2004: 服务不可用 → 503
    • 2005: 请求超时 → 504
    • 2006: 任务队列错误 → 500

正确的错误处理模式:

// ✅ Handler 层 - 直接返回错误
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var req CreateUserRequest
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "请求参数格式错误")
    }
    
    // 业务验证
    if len(req.Username) < 3 {
        return errors.New(errors.CodeInvalidParam, "用户名长度必须在 3-20 个字符之间")
    }
    
    // 调用 Service 层
    user, err := h.service.Create(c.Context(), &req)
    if err != nil {
        // 包装底层错误,保留错误链
        return errors.Wrap(errors.CodeDatabaseError, "创建用户失败", err)
    }
    
    return response.Success(c, user)
}

// ✅ Service 层 - 返回标准 error
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
    // 检查用户名是否已存在
    exists, err := s.store.ExistsByUsername(ctx, req.Username)
    if err != nil {
        return nil, fmt.Errorf("检查用户名失败: %w", err)
    }
    
    if exists {
        return nil, errors.New(errors.CodeConflict, "用户名已被使用")
    }
    
    // 创建用户
    user := &User{Username: req.Username, Email: req.Email}
    if err := s.store.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("创建用户失败: %w", err)
    }
    
    return user, nil
}

错误的错误处理模式(禁止):

// ❌ 在 Handler 中手动构造错误响应
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var req CreateUserRequest
    if err := c.BodyParser(&req); err != nil {
        // ❌ 不要手动构造 JSON 响应
        return c.Status(400).JSON(fiber.Map{
            "error": "invalid request",
        })
    }
    // ...
}

// ❌ 不使用错误码系统
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    // ❌ 直接返回 Fiber 错误,没有业务错误码
    return fiber.NewError(400, "invalid request")
}

// ❌ 在业务代码中主动 panic
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
    if req.Username == "" {
        panic("username is empty") // ❌ 应该返回错误
    }
    // ...
}

// ❌ 丢失错误链
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
    user, err := s.store.Create(ctx, user)
    if err != nil {
        // ❌ 应该使用 errors.Wrap 保留错误链
        return nil, errors.New(errors.CodeDatabaseError, "创建用户失败")
    }
    // ...
}

Panic 恢复机制:

  • Recover 中间件 MUST 在中间件链的最前面注册(第一层防护)
  • Panic 被捕获后 MUST 记录完整的堆栈跟踪到日志
  • Panic MUST 转换为 AppError(Code2001) 并传递给 ErrorHandler
  • 单个请求的 panic MUST NOT 影响其他请求的处理
// ✅ Recover 中间件自动处理
app.Use(middleware.Recover(logger))  // 第一个注册
app.Use(requestid.New())
app.Use(logger.Middleware())
// ... 其他中间件

// 当业务代码发生 panic 时:
// 1. Recover 捕获 panic
// 2. 记录堆栈跟踪到日志
// 3. 返回 500 错误响应
// 4. 服务继续运行

错误上下文和追踪:

  • 每个错误日志 MUST 包含 request_id(由 RequestID 中间件生成)
  • 每个错误日志 MUST 包含请求路径、方法、IP、User-Agent
  • 每个错误日志 MUST 包含错误码和错误消息
  • 5xx 错误日志 MUST 包含完整的堆栈跟踪(如果是 panic
  • 错误响应 Header MUST 包含 X-Request-ID,便于客户端追踪

敏感信息保护:

  • 数据库错误 MUST NOT 暴露 SQL 语句或数据库结构
  • 文件系统错误 MUST NOT 暴露服务器路径
  • 外部 API 错误 MUST NOT 暴露 API 密钥或认证信息
  • 所有 5xx 错误 MUST 返回通用消息,原始错误仅记录到日志

错误码使用规范:

  • 新增错误码 MUSTpkg/errors/codes.go 中定义常量
  • 新增错误码 MUSTerrorMessages 中添加中文消息
  • 错误码 MUST 遵循分段规则1xxx=客户端2xxx=服务端)
  • 相同类型的错误 SHOULD 复用已有错误码,避免错误码膨胀

理由 (RATIONALE):

统一的错误处理系统确保:

  1. 一致性:所有 API 错误响应格式统一,客户端集成成本低
  2. 稳定性100% 捕获 panic防止服务崩溃保障系统稳定性
  3. 安全性5xx 错误自动脱敏,防止敏感信息泄露
  4. 可追踪性Request ID 贯穿整个请求生命周期,快速定位问题
  5. 可维护性:错误码集中管理,错误分类清晰,日志级别明确
  6. 开发效率Handler 层只需返回错误,全局 ErrorHandler 自动处理格式化和日志记录

通过全局 ErrorHandler + Recover 中间件的双层防护,确保系统在任何错误情况下都能优雅降级,不会崩溃,同时保留完整的调试信息。


Development Workflow (开发工作流程)

分支管理

  • MUSTmain 分支创建功能分支
  • 功能分支命名格式:feature/###-brief-descriptionfix/###-brief-description
  • MUST 在合并前保持分支与 main 同步rebase 或 merge
  • 合并到 main MUST 通过 Pull Request 并经过代码审查

提交规范

  • 提交信息 MUST 遵循 Conventional Commits 格式:
    • feat: 新功能描述
    • fix: 修复问题描述
    • refactor: 重构描述
    • test: 测试相关
    • docs: 文档更新
    • chore: 构建/工具相关
  • 提交信息 SHOULD 简洁明了(中文或英文)

代码审查

  • 所有 PR MUST 至少有一人审查通过
  • 审查者 MUST 验证:
    • 代码符合本宪章所有原则(特别是 Go 惯用法原则)
    • 无 Java 风格的反模式getter/setter、过度抽象等
    • 测试覆盖充分且通过
    • 无安全漏洞SQL 注入、XSS、命令注入等
    • 性能影响可接受
    • 代码通过 gofmtgo vetgolangci-lint 检查
    • 文档已按规范更新docs/ 和 README.md
    • 访问日志记录符合规范(所有请求都被记录,包含请求/响应 body

Quality Gates (质量关卡)

代码合并前检查

  • 所有测试通过(go test ./...
  • 代码格式化(gofmt -l . 无输出)
  • 代码静态检查通过(go vet ./...
  • 代码质量检查通过(golangci-lint run
  • 测试覆盖率符合标准(go test -cover ./...
  • 无 TODO/FIXME 遗留(或已记录 Issue
  • API 文档已更新(如有 API 变更)
  • 数据库迁移文件已创建(如有 schema 变更)
  • 常量和 Redis key 使用符合规范(无硬编码字符串)
  • 无重复硬编码值3 次以上相同字面量已提取为常量)
  • 已定义的常量被正确使用(无重复硬编码已有常量的值)
  • 代码注释优先使用中文(实现注释使用中文,提高团队可读性)
  • 日志消息使用中文Info/Warn/Error/Debug 日志使用中文描述)
  • 错误消息支持中文(用户可见错误有中文消息)
  • 无 Java 风格反模式(无 getter/setter、无不必要接口、无过度抽象
  • 遵循 Go 命名约定(缩写大小写一致、包名简短、无下划线)
  • 使用 Go 惯用并发模式goroutines/channels无线程池类
  • 功能总结文档已创建(在 docs/{feature-id}/ 目录下,中文命名和内容)
  • README.md 已更新添加功能简短描述2-3 句话)
  • 访问日志验证通过(所有请求被记录到 access.log包含完整请求/响应参数)
  • 访问日志格式正确包含所有必需字段method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body
  • 中间件不绕过日志(认证失败、限流等短路返回也被记录)

上线前检查

  • 所有功能在 staging 环境测试通过
  • 性能测试通过(响应时间、内存使用、并发能力)
  • 数据库迁移在 staging 环境验证通过
  • 监控和告警配置完成
  • 回滚方案已准备
  • 访问日志轮转配置正确(防止日志文件过大)

Governance

本宪章是所有开发实践的最高指导原则,优先级高于个人偏好或临时便利。

修订流程

  • 宪章修订 MUST 经过团队讨论并达成共识
  • 修订 MUST 包含明确的理由和影响分析
  • 修订 MUST 更新版本号(遵循语义化版本)
  • 修订 MUST 同步更新相关模板plan-template.md、spec-template.md、tasks-template.md

版本策略

  • MAJOR: 移除或重新定义核心原则(不兼容变更)
  • MINOR: 新增原则或显著扩展指导内容
  • PATCH: 澄清说明、措辞优化、错误修正

合规审查

  • 所有 PR 审查 MUST 验证是否符合本宪章
  • 违反宪章的代码 MUST 在合并前修正
  • 特别关注 Go 惯用法原则,拒绝 Java 风格的代码
  • 特别关注常量使用规范,拒绝不必要的硬编码
  • 特别关注文档规范,确保每个功能都有完整的中文文档
  • 特别关注访问日志规范,确保所有请求都被完整记录
  • 任何复杂性增加(新依赖、新架构层)MUST 在设计文档中明确说明必要性

运行时开发指导

开发时参考本宪章确保一致性。如有疑问,优先遵守原则,再讨论例外情况。记住:

  • 写 Go 代码,不是用 Go 语法写 Java
  • 定义常量是为了使用,不是为了装饰
  • 写文档是为了团队协作,不是为了应付检查
  • 记录日志是为了可观测性,不能有例外

Version: 2.4.0 | Ratified: 2025-11-10 | Last Amended: 2025-11-13