在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。 主要变更: - 新增原则IX:数据库设计原则(Database Design Principles) - 强制要求:数据库表不得使用外键约束 - 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等) - 强制要求:表关系必须通过ID字段手动维护 - 强制要求:关联数据查询必须显式编写,避免ORM魔法 - 强制要求:时间字段由GORM处理,不使用数据库触发器 设计理念: - 提升业务逻辑灵活性(无数据库约束限制) - 优化高并发性能(无外键检查开销) - 增强代码可读性(显式查询,无隐式预加载) - 简化数据库架构和迁移流程 - 支持分布式和微服务场景 版本升级:2.3.0 → 2.4.0(MINOR)
52 KiB
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:
- Complete audit trail for all API interactions
- Debugging capability for all failure scenarios
- Compliance with logging requirements
- 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:
- Complete audit trail for all API interactions
- Debugging capability for all failure scenarios
- Compliance with logging requirements
- 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 和 Go Code Review Comments
- 变量命名 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} - 示例:
// 正确:使用常量和生成函数 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(用于循环、索引、比较等明确的上下文) - 一次性使用的临时值(测试数据、日志消息等)
- 语言特性:
正确的常量使用:
// pkg/constants/constants.go
const (
DefaultPageSize = 20
MaxPageSize = 100
MinPageSize = 1
SIMStatusActive = "active"
SIMStatusInactive = "inactive"
SIMStatusSuspended = "suspended"
OrderTypeRecharge = "recharge"
OrderTypeTransfer = "transfer"
)
// internal/handler/user.go
func (h *Handler) ListUsers(c *fiber.Ctx) error {
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", constants.DefaultPageSize) // ✅ 使用常量
if pageSize > constants.MaxPageSize { // ✅ 使用常量
pageSize = constants.MaxPageSize
}
users, err := h.service.List(page, pageSize)
// ...
}
// internal/service/sim.go
func (s *Service) Activate(iccid string) error {
return s.store.UpdateStatus(iccid, constants.SIMStatusActive) // ✅ 使用常量
}
错误的硬编码模式:
// ❌ 硬编码 magic number(在多处使用)
func ListUsers(c *fiber.Ctx) error {
pageSize := c.QueryInt("page_size", 20) // ❌ 硬编码 20
if pageSize > 100 { // ❌ 硬编码 100
pageSize = 100
}
// ...
}
func ListOrders(c *fiber.Ctx) error {
pageSize := c.QueryInt("page_size", 20) // ❌ 重复硬编码 20
// ...
}
// ❌ 硬编码字符串(在多处使用)
func ActivateSIM(iccid string) error {
return UpdateStatus(iccid, "active") // ❌ 硬编码 "active"
}
func IsSIMActive(sim *SIM) bool {
return sim.Status == "active" // ❌ 重复硬编码 "active"
}
// ❌ 已定义常量但不使用
// 常量已定义在 pkg/response/codes.go
func Success(c *fiber.Ctx, data any) error {
return c.JSON(Response{
Code: 0, // ❌ 应该使用 errors.CodeSuccess
Data: data,
})
}
可接受的字面量使用:
// ✅ 语言特性和明确上下文的数字
if user == nil { // ✅ nil
return errors.New("user not found")
}
for i := 0; i < len(items); i++ { // ✅ 0, 1 用于循环
// ...
}
if count == 1 { // ✅ 1 用于比较
// 特殊处理单个元素
}
// ✅ 测试数据和日志消息
func TestUserCreate(t *testing.T) {
user := &User{
Name: "Test User", // ✅ 测试数据
Email: "test@example.com",
}
// ...
}
logger.Info("processing request", // ✅ 日志消息
zap.String("path", c.Path()),
zap.Int("status", 200))
中文注释和输出规范 (Chinese Comments and Output Standards):
本项目面向中文开发者,为提高代码可读性和维护效率,SHOULD 优先使用中文进行注释和输出。
- 代码注释(implementation comments)SHOULD 使用中文
- 日志消息(log messages)SHOULD 使用中文
- 用户可见的错误消息 MUST 使用中文(通过
pkg/errors/的双语消息支持) - 内部错误消息和调试日志 SHOULD 使用中文
- Go 文档注释(doc comments for exported APIs)MAY 使用英文以保持生态兼容性,但中文注释更佳
- 变量名、函数名、类型名 MUST 使用英文(遵循 Go 命名规范)
正确的中文注释使用:
// GetUserByID 根据用户 ID 获取用户信息
// 如果用户不存在,返回 NotFoundError
func (s *Service) GetUserByID(ctx context.Context, id string) (*User, error) {
// 参数验证
if id == "" {
return nil, errors.New(errors.CodeInvalidParam, "用户 ID 不能为空")
}
// 从存储层获取用户
user, err := s.store.GetByID(ctx, id)
if err != nil {
s.logger.Error("获取用户失败",
zap.String("user_id", id),
zap.Error(err))
return nil, fmt.Errorf("获取用户失败: %w", err)
}
// 检查用户状态
if user.Status != constants.UserStatusActive {
s.logger.Warn("用户状态异常",
zap.String("user_id", id),
zap.String("status", user.Status))
}
return user, nil
}
正确的中文日志使用:
// 信息日志
logger.Info("服务启动成功",
zap.String("host", cfg.Server.Host),
zap.Int("port", cfg.Server.Port))
// 警告日志
logger.Warn("Redis 连接延迟较高",
zap.Duration("latency", latency),
zap.String("threshold", "50ms"))
// 错误日志
logger.Error("数据库连接失败",
zap.String("host", cfg.DB.Host),
zap.Int("port", cfg.DB.Port),
zap.Error(err))
函数复杂度和职责分离 (Function Complexity and Responsibility Separation):
- 函数长度 MUST NOT 超过合理范围(通常 50-100 行,核心逻辑建议 ≤ 50 行)
- 超过 100 行的函数 MUST 拆分为多个小函数,每个函数只负责一件事
main()函数 MUST 只做编排(orchestration),不包含具体实现逻辑main()函数中的每个初始化步骤 SHOULD 提取为独立的辅助函数- 编排函数(orchestrator)MUST 清晰表达流程,避免嵌套的实现细节
- MUST 遵循单一职责原则(Single Responsibility Principle)
- 虽然 MUST NOT 过度封装,但 MUST 在职责边界清晰的地方进行适度分离
理由 (RATIONALE):
清晰的分层架构和代码组织使代码易于理解、测试和维护。统一的错误处理和响应格式提升 API 一致性和客户端集成体验。依赖注入模式便于单元测试和模块替换。集中管理常量和 Redis key 避免拼写错误、重复定义和命名不一致,提升代码可维护性和重构安全性。Redis key 统一管理便于监控、调试和缓存策略调整。遵循 Go 官方代码风格确保代码一致性和可读性。
函数复杂度控制和职责分离的理由:
- 可读性: 小函数易于阅读和理解,特别是 main 函数清晰表达程序流程
- 可测试性: 小函数易于编写单元测试,提高测试覆盖率
- 可维护性: 职责单一的函数修改风险低,不易引入 bug
- 可复用性: 提取的辅助函数可以在其他地方复用
- 减少认知负担: 阅读者不需要同时理解过多细节
- 便于重构: 小函数更容易安全地重构和优化
避免硬编码和强制使用常量的规则能够:
- 提高可维护性:修改常量值只需改一处,不需要搜索所有硬编码位置
- 减少错误:避免手动输入错误(拼写错误、大小写错误)
- 增强可读性:
constants.MaxPageSize比100更能表达意图 - 便于重构:IDE 可以追踪常量使用,重命名时不会遗漏
- 统一业务规则:确保所有地方使用相同的业务规则值
- "3 次规则"提供明确的阈值,避免过早优化,同时确保重复值被及时抽取
使用中文注释和日志的理由:
- 提高团队效率:中文开发团队阅读中文注释更快速准确
- 降低理解成本:减少翻译和理解偏差
- 改善调试体验:中文日志更易于排查问题
- 保持生态兼容:导出 API 的文档注释可保留英文
III. Testing Standards (测试标准)
规则 (RULES):
- 所有核心业务逻辑(Service 层)MUST 有单元测试覆盖
- 所有 API 端点 MUST 有集成测试覆盖
- 所有数据库操作 SHOULD 有事务回滚测试
- 测试 MUST 使用 Go 标准测试框架(
testing包) - 测试文件 MUST 与源文件同目录,命名为
*_test.go - 测试函数 MUST 使用
Test前缀:func TestUserCreate(t *testing.T) - 基准测试 MUST 使用
Benchmark前缀:func BenchmarkUserCreate(b *testing.B) - 测试 MUST 可独立运行,不依赖外部服务(使用 mock 或 testcontainers)
- 单元测试 MUST 在 100ms 内完成
- 集成测试 SHOULD 在 1s 内完成
- 测试覆盖率 SHOULD 达到 70% 以上(核心业务代码必须 90% 以上)
- 测试 MUST 使用 table-driven tests 处理多个测试用例
- 测试 MUST 使用
t.Helper()标记辅助函数
Table-Driven Test 示例:
func TestUserValidate(t *testing.T) {
tests := []struct {
name string
user User
wantErr bool
}{
{"valid user", User{Name: "John", Email: "john@example.com"}, false},
{"empty name", User{Name: "", Email: "john@example.com"}, true},
{"invalid email", User{Name: "John", Email: "invalid"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.user.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
理由 (RATIONALE):
高质量的测试是代码质量的基石。单元测试确保业务逻辑正确性,集成测试确保模块间协作正常。快速的测试执行时间保证开发效率。测试独立性避免环境依赖导致的 flaky tests。Table-driven tests 使测试更简洁、易于扩展和维护,这是 Go 社区的最佳实践。
IV. User Experience Consistency (用户体验一致性)
规则 (RULES):
- 所有 API 响应 MUST 使用统一的 JSON 格式:
{ "code": 0, "message": "success", "data": {}, "timestamp": "2025-11-10T15:30:00Z" } - 所有错误响应 MUST 包含明确的错误码和错误消息(中英文双语)
- 所有 API 端点 MUST 遵循 RESTful 设计原则
- 所有分页 API MUST 使用统一的分页参数:
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 风格:
// 小接口,在使用方定义
type UserStorer interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id string) (*User, error)
}
// 具体实现
type PostgresStore struct {
db *gorm.DB
}
func (s *PostgresStore) Create(ctx context.Context, user *User) error {
return s.db.WithContext(ctx).Create(user).Error
}
错误的 Java 风格(禁止):
// ❌ 大接口,方法过多
type IUserRepository interface {
Create(user *User) error
Update(user *User) error
Delete(id string) error
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
FindAll() ([]*User, error)
FindByStatus(status string) ([]*User, error)
Count() (int, error)
// ... 更多方法
}
// ❌ I 前缀
type IUserService interface {}
// ❌ 不必要的抽象层
type UserRepositoryImpl struct {} // 只有一个实现
错误处理 (Error Handling)
MUST 遵循的原则:
- 错误 MUST 显式返回和检查,不使用异常(panic/recover)
- 错误处理 MUST 紧跟错误产生的代码
- MUST 使用
errors.Is()和errors.As()检查错误类型 - MUST 使用
fmt.Errorf()包装错误,保留错误链 - 自定义错误 SHOULD 实现
error接口 - panic MUST ONLY 用于不可恢复的程序错误
正确的 Go 风格:
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.store.GetByID(ctx, id)
if err != nil {
// 包装错误,添加上下文
return nil, fmt.Errorf("get user by id %s: %w", id, err)
}
return user, nil
}
// 自定义错误类型
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}
错误的 Java 风格(禁止):
// ❌ 使用 panic 代替错误返回
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // ❌ 不要这样做
}
return user
}
// ❌ try-catch 风格
func DoSomething() {
defer func() {
if r := recover(); r != nil { // ❌ 滥用 recover
log.Println("recovered:", r)
}
}()
// ...
}
// ❌ 异常类层次结构
type Exception interface { // ❌ 不需要
GetMessage() string
GetStackTrace() []string
}
type BusinessException struct {} // ❌ 过度设计
type ValidationException struct {} // ❌ 过度设计
结构体和方法 (Structs and Methods)
MUST 遵循的原则:
- 结构体 MUST 简单直接,不是类(class)的替代品
- MUST NOT 为每个字段创建 getter/setter 方法
- MUST 直接访问导出的字段(大写开头)
- MUST 使用组合(composition)而不是继承(inheritance)
- 构造函数 SHOULD 命名为
New或NewXxx,返回具体类型 - MUST NOT 使用构造器模式(Builder Pattern)除非真正需要
正确的 Go 风格:
// 简单结构体,导出字段直接访问
type User struct {
ID string
Name string
Email string
}
// 简单构造函数
func NewUser(name, email string) *User {
return &User{
ID: generateID(),
Name: name,
Email: email,
}
}
// 使用组合
type Service struct {
store UserStorer
logger *zap.Logger
}
func NewService(store UserStorer, logger *zap.Logger) *Service {
return &Service{
store: store,
logger: logger,
}
}
错误的 Java 风格(禁止):
// ❌ 私有字段 + getter/setter
type User struct {
id string // ❌ 不需要私有
name string
email string
}
func (u *User) GetID() string { return u.id } // ❌ 不需要
func (u *User) SetID(id string) { u.id = id } // ❌ 不需要
func (u *User) GetName() string { return u.name } // ❌ 不需要
func (u *User) SetName(name string) { u.name = name } // ❌ 不需要
// ❌ 构造器模式(不必要)
type UserBuilder struct {
user *User
}
func NewUserBuilder() *UserBuilder { // ❌ 过度设计
return &UserBuilder{user: &User{}}
}
func (b *UserBuilder) WithName(name string) *UserBuilder {
b.user.name = name
return b
}
func (b *UserBuilder) Build() *User {
return b.user
}
// ❌ 工厂模式(过度抽象)
type UserFactory interface { // ❌ 不需要
CreateUser(name string) *User
}
并发模式 (Concurrency Patterns)
MUST 遵循的原则:
- MUST 使用 goroutines 和 channels,不是线程和锁(大多数情况)
- MUST 使用
context.Context传递取消信号 - MUST 遵循"通过通信共享内存,不要通过共享内存通信"
- SHOULD 使用
sync.WaitGroup等待 goroutines 完成 - SHOULD 使用
sync.Once确保只执行一次 - MUST NOT 创建线程池类(Go 运行时已处理)
正确的 Go 风格:
// 使用 goroutines 和 channels
func ProcessBatch(ctx context.Context, items []Item) error {
results := make(chan Result, len(items))
errors := make(chan error, len(items))
for _, item := range items {
go func(item Item) {
result, err := process(ctx, item)
if err != nil {
errors <- err
return
}
results <- result
}(item)
}
// 收集结果
for i := 0; i < len(items); i++ {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errors:
return err
case <-results:
// 处理结果
}
}
return nil
}
// 使用 sync.WaitGroup
func ProcessConcurrently(items []Item) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
}
错误的 Java 风格(禁止):
// ❌ 线程池模式
type ThreadPool struct { // ❌ 不需要
workers int
taskQueue chan Task
wg sync.WaitGroup
}
func NewThreadPool(workers int) *ThreadPool { // ❌ Go 运行时已处理
return &ThreadPool{
workers: workers,
taskQueue: make(chan Task, 100),
}
}
// ❌ Future/Promise 模式(不需要单独实现)
type Future struct { // ❌ 直接使用 channel
result chan interface{}
err chan error
}
// ❌ 过度使用 mutex(应该用 channel)
type SafeMap struct { // ❌ 考虑使用 sync.Map 或 channel
mu sync.Mutex
data map[string]interface{}
}
func (m *SafeMap) Get(key string) interface{} {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[key]
}
命名约定 (Naming Conventions)
MUST 遵循的原则:
- 变量名 MUST 简短且符合上下文:
- 短作用域用短名字:
i,j,k用于循环 - 长作用域用描述性名字:
userRepository,configManager
- 短作用域用短名字:
- 缩写词 MUST 保持一致的大小写:
URL,HTTP,ID(不是Url,Http,Id) - MUST NOT 使用匈牙利命名法或类型前缀:
strName,arrUsers(禁止) - MUST NOT 使用下划线连接(除了测试和包名):
user_service(禁止,应该是userservice或UserService) - 方法接收者名称 SHOULD 使用 1-2 个字母的缩写,全文件保持一致
正确的 Go 风格:
// 短作用域,短名字
for i, user := range users {
fmt.Println(i, user.Name)
}
// 长作用域,描述性名字
func ProcessUserRegistration(ctx context.Context, req *RegistrationRequest) error {
// ...
}
// 缩写词大小写一致
type UserAPI struct {
BaseURL string
APIKey string
UserID int64
}
// 方法接收者简短一致
type UserService struct {}
func (s *UserService) Create(user *User) error { // s 用于 service
return s.store.Create(user)
}
func (s *UserService) Update(user *User) error { // 保持一致使用 s
return s.store.Update(user)
}
错误的 Java 风格(禁止):
// ❌ 过长的变量名
func ProcessRegistration() {
userRegistrationRequest := &Request{} // ❌ 太长
userRegistrationValidator := NewValidator() // ❌ 太长
userRegistrationService := NewService() // ❌ 太长
}
// ❌ 匈牙利命名
strUserName := "John" // ❌
intUserAge := 25 // ❌
arrUserList := []User{} // ❌
// ❌ 下划线命名
type User_Service struct {} // ❌ 应该是 UserService
func Get_User_By_Id() {} // ❌ 应该是 GetUserByID
// ❌ 缩写词大小写不一致
type UserApi struct { // ❌ 应该是 UserAPI
BaseUrl string // ❌ 应该是 BaseURL
ApiKey string // ❌ 应该是 APIKey
UserId int64 // ❌ 应该是 UserID
}
// ❌ 方法接收者不一致或过长
func (userService *UserService) Create() {} // ❌ 太长
func (us *UserService) Update() {} // ❌ 与上面不一致
func (this *UserService) Delete() {} // ❌ 不要使用 this/self
反模式清单 (Anti-Patterns to Avoid)
严格禁止的 Java 风格模式:
- ❌ 过度抽象:不需要的接口、工厂、构造器
- ❌ Getter/Setter:直接访问导出字段
- ❌ 继承层次:使用组合,不是嵌入
- ❌ 异常处理:使用错误返回,不是 panic/recover
- ❌ 单例模式:使用包级别变量或
sync.Once - ❌ 线程池:直接使用 goroutines
- ❌ 深层包嵌套:保持扁平结构
- ❌ 类型前缀:
IService,AbstractBase,ServiceImpl - ❌ Bean 风格:不需要 POJO/JavaBean 模式
- ❌ 过度 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 的限流,支持可配置的限制和存储后端
正确的文档组织:
<!-- docs/002-user-management/功能总结.md -->
# 用户管理功能总结
## 功能概述
实现了完整的用户 CRUD 操作,包括用户创建、查询、更新、删除和列表分页。
## 核心实现
- Handler: `internal/handler/user.go`
- Service: `internal/service/user.go`
- Store: `internal/store/postgres/user.go`
- Model: `internal/model/user.go`
## API 端点
- `POST /api/v1/users` - 创建用户
- `GET /api/v1/users/:id` - 获取用户
- `PUT /api/v1/users/:id` - 更新用户
- `DELETE /api/v1/users/:id` - 删除用户
- `GET /api/v1/users` - 用户列表(分页)
## 技术要点
- 使用 GORM 进行数据访问
- 使用 Validator 进行参数验证
- 支持事务处理
- 统一错误处理和响应格式
详细使用说明请参阅 [使用指南.md](./使用指南.md)
<!-- README.md 更新示例 -->
## 核心功能
- **认证中间件**:基于 Redis 的 Token 认证
- **限流中间件**:IP 限流,支持 Memory 和 Redis 存储
- **用户管理**:完整的用户 CRUD 和分页列表功能
- **日志系统**:Zap 结构化日志,自动轮转和请求追踪
详细文档:
- [认证中间件](docs/001-fiber-middleware-integration/功能总结.md)
- [用户管理](docs/002-user-management/功能总结.md)
错误的文档组织(禁止):
❌ docs/feature-summary.md # 所有功能混在一个文件
❌ docs/001-summary.md # 英文命名
❌ docs/001/summary.md # 英文内容
❌ specs/001.../implementation.md # 总结文档放在 specs/ 下
❌ README.md 中没有更新新功能 # 遗漏 README 更新
❌ README.md 功能描述过长(超过 5 句话) # 描述冗长
README.md 更新要求:
- 简洁性:每个功能 2-3 句话描述核心价值
- 可读性:使用中文,便于中文开发者快速理解
- 链接性:提供到详细文档的链接
- 分类性:按功能模块分组(如"核心功能"、"中间件"、"业务模块"等)
理由 (RATIONALE):
统一的文档结构使新成员快速了解项目功能和架构。中文文档降低中文团队的阅读门槛,提高协作效率。将总结文档与设计文档分离(docs/ vs specs/),清晰区分"计划做什么"和"做了什么"。强制更新 README.md 确保项目主文档始终反映最新功能。简短的 README 描述让首次接触项目的开发者能在 5 分钟内了解项目全貌。
VIII. Access Logging Standards (访问日志规范)
规则 (RULES):
- 所有 HTTP 请求 MUST 被记录到
access.log,无例外 - 访问日志 MUST 记录完整的请求参数(query 参数 + request body)
- 访问日志 MUST 记录完整的响应参数(response body)
- 请求/响应 body MUST 限制大小为 50KB,超过部分截断并标注
... (truncated) - 访问日志 MUST 通过统一的 Logger 中间件(
pkg/logger/Middleware())记录 - 任何中间件 的短路返回(认证失败、限流拒绝、参数验证失败等)MUST NOT 绕过访问日志
- 访问日志 MUST 包含以下字段(最低要求):
method: HTTP 方法path: 请求路径query: Query 参数字符串status: HTTP 状态码duration_ms: 请求耗时(毫秒)request_id: 请求唯一 IDip: 客户端 IPuser_agent: 用户代理user_id: 用户 ID(认证后有值,否则为空)request_body: 请求体(限制 50KB)response_body: 响应体(限制 50KB)
- 访问日志 SHOULD 使用 JSON 格式,便于日志分析和监控
- 访问日志文件 MUST 配置自动轮转(基于大小或时间)
正确的访问日志示例:
{
"level": "info",
"timestamp": "2025-11-11T17:45:03.186+0800",
"message": "",
"method": "POST",
"path": "/api/v1/users",
"query": "page=1&size=10",
"status": 400,
"duration_ms": 0.035,
"request_id": "f1d8b767-dfb3-4588-9fa0-8a97e5337184",
"ip": "127.0.0.1",
"user_agent": "curl/8.7.1",
"user_id": "",
"request_body": "{\"username\":\"testuser\",\"email\":\"test@example.com\"}",
"response_body": "{\"code\":1001,\"data\":null,\"msg\":\"缺失认证令牌\",\"timestamp\":\"2025-11-11T17:45:03+08:00\"}"
}
Logger 中间件实现要求:
// pkg/logger/middleware.go
func Middleware() fiber.Handler {
return func(c *fiber.Ctx) error {
startTime := time.Now()
// 在 c.Next() 之前读取请求 body
requestBody := truncateBody(c.Body(), MaxBodyLogSize)
queryParams := string(c.Request().URI().QueryString())
// 处理请求(可能被中间件短路返回)
err := c.Next()
// 在 c.Next() 之后读取响应 body(无论是否短路)
responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize)
// 记录完整的访问日志(包含请求和响应参数)
accessLogger.Info("",
zap.String("method", c.Method()),
zap.String("path", c.Path()),
zap.String("query", queryParams),
zap.Int("status", c.Response().StatusCode()),
zap.Float64("duration_ms", time.Since(startTime).Seconds()*1000),
zap.String("request_id", getRequestID(c)),
zap.String("ip", c.IP()),
zap.String("user_agent", c.Get("User-Agent")),
zap.String("user_id", getUserID(c)),
zap.String("request_body", requestBody),
zap.String("response_body", responseBody),
)
return err
}
}
禁止的做法:
// ❌ 在中间件中跳过日志记录
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
if !isAuthenticated(c) {
// ❌ 直接返回,没有记录到 access.log
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
}
return c.Next()
}
}
// ❌ 只在部分路由记录日志
app.Use("/api/v1/users", logger.Middleware()) // ❌ 其他路由没有日志
// ❌ 不记录请求/响应 body
accessLogger.Info("",
zap.String("method", c.Method()),
zap.String("path", c.Path()),
zap.Int("status", c.Response().StatusCode()),
// ❌ 缺少 request_body 和 response_body
)
理由 (RATIONALE):
完整的访问日志是系统可观测性的基础,对于以下场景至关重要:
- 问题排查:当用户报告错误时,通过 request_id 可以追溯完整的请求/响应数据,快速定位问题
- 安全审计:记录所有请求(包括认证失败、参数验证失败等)可以追踪潜在的安全攻击
- 性能分析:通过 duration_ms 和请求参数可以分析慢查询和性能瓶颈
- 合规要求:某些行业(金融、医疗等)要求完整的操作审计日志
- 用户行为分析:通过 user_id 和请求参数可以分析用户行为模式
- 无例外原则:确保没有请求"逃脱"日志记录,避免日志盲点
通过强制在 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 自动处理,不使用数据库触发器
正确的关联设计:
// ✅ User 模型 - 完全独立
type User struct {
BaseModel
Username string `gorm:"uniqueIndex;not null;size:50"`
Email string `gorm:"uniqueIndex;not null;size:100"`
Password string `gorm:"not null;size:255"`
Status string `gorm:"not null;size:20;default:'active'"`
}
// ✅ Order 模型 - 仅存储 UserID
type Order struct {
BaseModel
OrderID string `gorm:"uniqueIndex;not null;size:50"`
UserID uint `gorm:"not null;index"` // 仅存储 ID,无 ORM 关联
Amount int64 `gorm:"not null"`
Status string `gorm:"not null;size:20;default:'pending'"`
}
// ✅ 手动查询关联数据
func (s *OrderService) GetOrderWithUser(ctx context.Context, orderID uint) (*OrderDetail, error) {
// 查询订单
order, err := s.store.Order.GetByID(ctx, orderID)
if err != nil {
return nil, err
}
// 手动查询关联的用户
user, err := s.store.User.GetByID(ctx, order.UserID)
if err != nil {
return nil, err
}
// 组装返回数据
return &OrderDetail{
Order: order,
User: user,
}, nil
}
错误的关联设计(禁止):
// ❌ 使用 GORM 外键关联
type Order struct {
BaseModel
OrderID string
UserID uint
User *User `gorm:"foreignKey:UserID"` // ❌ 禁止
Amount int64
}
// ❌ 使用 GORM hasMany 关联
type User struct {
BaseModel
Username string
Orders []Order `gorm:"foreignKey:UserID"` // ❌ 禁止
}
// ❌ 在迁移脚本中定义外键约束
CREATE TABLE tb_order (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
CONSTRAINT fk_order_user FOREIGN KEY (user_id)
REFERENCES tb_user(id) ON DELETE RESTRICT -- ❌ 禁止
);
// ❌ 使用数据库触发器更新时间
CREATE TRIGGER update_order_updated_at
BEFORE UPDATE ON tb_order
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column(); -- ❌ 禁止
// ❌ 依赖 GORM 预加载
orders, err := db.Preload("User").Find(&orders) // ❌ 禁止
GORM BaseModel 自动时间管理:
// ✅ GORM 自动处理时间字段
type BaseModel struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time // GORM 自动填充创建时间
UpdatedAt time.Time // GORM 自动更新修改时间
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除支持
}
// 创建记录时,GORM 自动设置 CreatedAt 和 UpdatedAt
db.Create(&user)
// 更新记录时,GORM 自动更新 UpdatedAt
db.Save(&user)
理由 (RATIONALE):
移除数据库外键约束和 ORM 关联关系的理由:
-
灵活性:业务逻辑完全在代码中控制,不受数据库约束限制。例如删除用户时可以根据业务需求决定是级联删除订单、保留订单还是转移订单,而不是被
ON DELETE CASCADE/RESTRICT强制约束。 -
性能:无外键约束意味着无数据库层面的引用完整性检查开销。在高并发场景下,外键检查和锁竞争会成为性能瓶颈。
-
简单直接:显式的关联数据查询使数据流向清晰可见,代码行为明确。避免了 ORM 的"魔法"行为(N+1 查询问题、意外的预加载、Lazy Loading 陷阱)。
-
可控性:开发者完全掌控何时查询关联数据、查询哪些关联数据。可以根据场景优化查询(批量查询、缓存等),而不是依赖 ORM 的自动行为。
-
可维护性:数据库 schema 更简单,迁移更容易。修改表结构不需要处理复杂的外键依赖关系。代码重构时不会被数据库约束限制。
-
分布式友好:在微服务和分布式数据库场景下,外键约束往往无法跨数据库工作。手动维护关联从设计上就支持未来的服务拆分。
-
GORM 基础功能:保留 GORM 的核心价值(自动时间管理、软删除、查询构建、事务支持),去除复杂的关联功能,达到简单性和功能性的平衡。
这种设计哲学符合"明确优于隐式"的原则,代码的行为一目了然,没有隐藏的数据库操作和 ORM 魔法。
Development Workflow (开发工作流程)
分支管理
- MUST 从
main分支创建功能分支 - 功能分支命名格式:
feature/###-brief-description或fix/###-brief-description - MUST 在合并前保持分支与
main同步(rebase 或 merge) - 合并到
mainMUST 通过 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