- 新增统一错误码定义和管理 (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
1608 lines
59 KiB
Markdown
1608 lines
59 KiB
Markdown
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
|