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

1608 lines
59 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.
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.
-->
<!--
SYNC IMPACT REPORT - Constitution Amendment
Version Change: 2.3.0 → 2.4.0
Date: 2025-11-13
NEW PRINCIPLES ADDED:
- IX. Database Design Principles (数据库设计原则) - NEW principle for database schema and ORM relationship management
MODIFIED SECTIONS:
- Added new Principle IX with mandatory database design requirements
- Rule: Database tables MUST NOT have foreign key constraints
- Rule: GORM models MUST NOT use ORM association tags (foreignKey, hasMany, belongsTo, etc.)
- Rule: Table relationships MUST be maintained manually via ID fields
- Rule: Associated data queries MUST be explicit in code, not ORM magic
- Rule: Model structs MUST ONLY contain simple fields
- Rule: Migration scripts MUST NOT include foreign key constraints
- Rule: Migration scripts MUST NOT include triggers for relationship maintenance
- Rule: Time fields (created_at, updated_at) MUST be handled by GORM, not database triggers
- Rationale: Flexibility, performance, simplicity, maintainability, distributed-friendly
TEMPLATES REQUIRING UPDATES:
⚠️ .specify/templates/plan-template.md - Should add database design principle check
⚠️ .specify/templates/tasks-template.md - Should add migration script validation
FOLLOW-UP ACTIONS:
- Migration scripts updated (removed foreign keys and triggers)
- User and Order models updated (removed GORM associations)
- OrderService.ListOrdersByUserID added for manual relationship queries
RATIONALE:
MINOR version bump (2.4.0) - New principle added for database design standards.
This establishes a mandatory governance rule that database relationships must NOT
be enforced at the database or ORM level, but rather managed explicitly in code.
This ensures:
1. Business logic flexibility (no database constraints limiting deletion, updates)
2. Performance (no foreign key check overhead in high-concurrency scenarios)
3. Code clarity (explicit queries, no ORM magic like N+1 queries or unexpected preloading)
4. Developer control (decide when and how to query associated data)
5. Maintainability (simpler schema, easier migrations, no complex FK dependencies)
6. Distributed-friendly (manual relationships work across databases/microservices)
7. GORM value retention (keep core features: auto timestamps, soft delete, query builder)
This is a MINOR bump (not PATCH) because it adds a new mandatory principle that
fundamentally changes how we design database schemas and model relationships,
affecting all future database-related development.
PREVIOUS AMENDMENTS:
- 2.3.0 (2025-11-11): Added Principle VIII - Access Logging Standards
- 2.2.0 (Previous): Added comprehensive code quality and Go idiomatic principles
-->
============================================
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 fmt``go vet``golangci-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** 通过结构体字段进行依赖注入(不使用构造函数模式)
- 所有公共错误 **MUST**`pkg/errors/` 中定义,使用统一错误码
- 所有 API 响应 **MUST** 使用 `pkg/response/` 的统一格式
- 所有常量 **MUST**`pkg/constants/` 中定义和管理
- 所有 Redis key **MUST** 通过 `pkg/constants/` 中的 Key 生成函数统一管理
- **MUST** 为所有导出的函数、类型和常量编写 Go 风格的文档注释(`// FunctionName does something...`
- **MUST** 避免 magic numbers 和 magic strings使用常量定义
**Go 代码风格要求:**
- **MUST** 使用 `gofmt` 格式化所有代码
- **MUST** 遵循 [Effective Go](https://go.dev/doc/effective_go) 和 [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
- 变量命名 **MUST** 使用 Go 风格:`userID`(不是 `userId`)、`HTTPServer`(不是 `HttpServer`
- 缩写词 **MUST** 全部大写或全部小写:`URL``ID``HTTP`(导出)或 `url``id``http`(未导出)
- 包名 **MUST** 简短、小写、单数、无下划线:`user``order``pkg`(不是 `users``userService``user_service`
- 接口命名 **SHOULD** 使用 `-er` 后缀:`Reader``Writer``Logger`(不是 `ILogger``LoggerInterface`
**常量管理规范 (Constants Management):**
- 业务常量(状态码、类型枚举等)**MUST** 定义在 `pkg/constants/constants.go` 或按模块分文件
- Redis key **MUST** 使用函数生成,不允许硬编码字符串拼接
- Redis key 生成函数 **MUST** 遵循命名规范:`Redis{Module}{Purpose}Key(params...)`
- Redis key 格式 **MUST** 使用冒号分隔:`{module}:{purpose}:{identifier}`
- 示例:
```go
// 正确:使用常量和生成函数
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** 重复硬编码相同的值
- 只允许在以下情况使用字面量:
- 语言特性:`nil`、`true`、`false`
- 数学常量:`0`、`1`、`-1`(用于循环、索引、比较等明确的上下文)
- 一次性使用的临时值(测试数据、日志消息等)
**正确的常量使用:**
```go
// 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) // ✅ 使用常量
}
```
**错误的硬编码模式:**
```go
// ❌ 硬编码 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,
})
}
```
**可接受的字面量使用:**
```go
// ✅ 语言特性和明确上下文的数字
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 comments**SHOULD** 使用中文
- 日志消息log messages**SHOULD** 使用中文
- 用户可见的错误消息 **MUST** 使用中文(通过 `pkg/errors/` 的双语消息支持)
- 内部错误消息和调试日志 **SHOULD** 使用中文
- Go 文档注释doc comments for exported APIs**MAY** 使用英文以保持生态兼容性,但中文注释更佳
- 变量名、函数名、类型名 **MUST** 使用英文(遵循 Go 命名规范)
**正确的中文注释使用:**
```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
}
```
**正确的中文日志使用:**
```go
// 信息日志
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** 提取为独立的辅助函数
- 编排函数orchestrator**MUST** 清晰表达流程,避免嵌套的实现细节
- **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.MaxPageSize` 比 `100` 更能表达意图
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 示例:**
```go
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 格式:
```json
{
"code": 0,
"message": "success",
"data": {},
"timestamp": "2025-11-10T15:30:00Z"
}
```
- 所有错误响应 **MUST** 包含明确的错误码和错误消息(中英文双语)
- 所有 API 端点 **MUST** 遵循 RESTful 设计原则
- 所有分页 API **MUST** 使用统一的分页参数:`page`、`page_size`、`total`
- 所有时间字段 **MUST** 使用 ISO 8601 格式RFC3339
- 所有货币金额 **MUST** 使用整数表示(分为单位),避免浮点精度问题
- 所有布尔字段 **MUST** 使用 `true`/`false`,不使用 `0`/`1`
- API 版本 **MUST** 通过 URL 路径管理(如 `/api/v1/...`
**理由 (RATIONALE):**
一致的 API 设计降低客户端开发成本,减少集成错误。统一的数据格式和错误处理提升系统可预测性。清晰的时间和金额表示避免常见的数据处理错误。
---
### V. Performance Requirements (性能要求)
**规则 (RULES):**
- API 响应时间P95**MUST** < 200ms数据库查询 < 50ms
- API 响应时间P99**MUST** < 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` 不是 `httputils`、`handlers`
**正确的 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` 后缀:`Reader`、`Writer`、`Storer`
- **MUST NOT** 使用 `I` 前缀或 `Interface` 后缀
- **MUST NOT** 创建只有一个实现的接口(除非明确需要抽象)
**正确的 Go 风格:**
```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 风格(禁止):**
```go
// ❌ 大接口,方法过多
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 风格:**
```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 风格(禁止):**
```go
// ❌ 使用 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** 命名为 `New` 或 `NewXxx`,返回具体类型
- **MUST NOT** 使用构造器模式Builder Pattern除非真正需要
**正确的 Go 风格:**
```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 风格(禁止):**
```go
// ❌ 私有字段 + 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 风格:**
```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 风格(禁止):**
```go
// ❌ 线程池模式
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`(禁止,应该是 `userservice` 或 `UserService`
- 方法接收者名称 **SHOULD** 使用 1-2 个字母的缩写,全文件保持一致
**正确的 Go 风格:**
```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 风格(禁止):**
```go
// ❌ 过长的变量名
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):**
- 每个功能完成后 **MUST** 在 `docs/` 目录创建总结文档
- 总结文档路径 **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 的限流,支持可配置的限制和存储后端
```
**正确的文档组织:**
```markdown
<!-- 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)
```
```markdown
<!-- 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** 配置自动轮转(基于大小或时间)
**正确的访问日志示例:**
```json
{
"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 中间件实现要求:**
```go
// 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
}
}
```
**禁止的做法:**
```go
// ❌ 在中间件中跳过日志记录
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 关联关系(`foreignKey`、`references`、`hasMany`、`belongsTo` 等标签)
- 表之间的关联 **MUST** 通过存储关联 ID 字段手动维护
- 关联数据查询 **MUST** 在代码层面显式执行,不依赖 ORM 的自动加载Lazy Loading或预加载Eager Loading
- 模型结构体 **MUST ONLY** 包含简单字段,不应包含其他模型的嵌套引用
- 数据库迁移脚本 **MUST NOT** 包含外键约束定义
- 数据库迁移脚本 **MUST NOT** 包含触发器用于维护关联数据
- 时间字段(`created_at`、`updated_at`)的更新 **MUST** 由 GORM 自动处理,不使用数据库触发器
**正确的关联设计:**
```go
// ✅ 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
}
```
**错误的关联设计(禁止):**
```go
// ❌ 使用 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 自动时间管理:**
```go
// ✅ 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` 传递给全局 ErrorHandler**MUST NOT** 手动构造错误响应
- **所有** 业务错误 **MUST** 使用 `pkg/errors.New()` 或 `pkg/errors.Wrap()` 创建 `AppError`,并指定错误码
- **所有** 错误码 **MUST** 在 `pkg/errors/codes.go` 中统一定义和管理
- **所有** Panic **MUST** 被 Recover 中间件自动捕获,转换为 500 错误响应
- **所有** 错误日志 **MUST** 包含完整的请求上下文Request ID、路径、方法、参数等
- 5xx 服务端错误 **MUST** 自动脱敏,只返回通用错误消息,原始错误仅记录到日志
- 4xx 客户端错误 **MAY** 返回具体业务错误消息(如"用户名已存在"
- **MUST NOT** 在业务代码中主动 `panic`(除非遇到不可恢复的编程错误)
- **MUST NOT** 在 Handler 中直接使用 `c.Status().JSON()` 返回错误响应
**统一错误响应格式:**
```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
**正确的错误处理模式:**
```go
// ✅ 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
}
```
**错误的错误处理模式(禁止):**
```go
// ❌ 在 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** 影响其他请求的处理
```go
// ✅ 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** 返回通用消息,原始错误仅记录到日志
**错误码使用规范:**
- 新增错误码 **MUST** 在 `pkg/errors/codes.go` 中定义常量
- 新增错误码 **MUST** 在 `errorMessages` 中添加中文消息
- 错误码 **MUST** 遵循分段规则1xxx=客户端2xxx=服务端)
- 相同类型的错误 **SHOULD** 复用已有错误码,避免错误码膨胀
**理由 (RATIONALE):**
统一的错误处理系统确保:
1. **一致性**:所有 API 错误响应格式统一,客户端集成成本低
2. **稳定性**100% 捕获 panic防止服务崩溃保障系统稳定性
3. **安全性**5xx 错误自动脱敏,防止敏感信息泄露
4. **可追踪性**Request ID 贯穿整个请求生命周期,快速定位问题
5. **可维护性**:错误码集中管理,错误分类清晰,日志级别明确
6. **开发效率**Handler 层只需返回错误,全局 ErrorHandler 自动处理格式化和日志记录
通过全局 ErrorHandler + Recover 中间件的双层防护,确保系统在任何错误情况下都能优雅降级,不会崩溃,同时保留完整的调试信息。
---
## Development Workflow (开发工作流程)
### 分支管理
- **MUST** 从 `main` 分支创建功能分支
- 功能分支命名格式:`feature/###-brief-description` 或 `fix/###-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、命令注入等
- 性能影响可接受
- 代码通过 `gofmt`、`go vet`、`golangci-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