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 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 # 用户管理功能总结 ## 功能概述 实现了完整的用户 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 ## 核心功能 - **认证中间件**:基于 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 魔法。 --- ## 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