做一次小小的备份,等会又删掉了
This commit is contained in:
@@ -1,31 +1,42 @@
|
|||||||
<!--
|
<!--
|
||||||
SYNC IMPACT REPORT - Constitution Amendment
|
SYNC IMPACT REPORT - Constitution Amendment
|
||||||
============================================
|
============================================
|
||||||
Version Change: INITIAL → 1.0.0
|
Version Change: 1.1.0 → 2.0.0
|
||||||
Date: 2025-11-10
|
Date: 2025-11-10
|
||||||
|
|
||||||
NEW PRINCIPLES ESTABLISHED:
|
MODIFIED PRINCIPLES:
|
||||||
- I. Tech Stack Adherence (Fiber 生态系统)
|
- I. Tech Stack Adherence → Added explicit Go tooling requirements
|
||||||
- II. Code Quality Standards (代码质量标准)
|
- II. Code Quality Standards → Completely rewritten to emphasize Go idioms and eliminate Java patterns
|
||||||
- III. Testing Standards (测试标准)
|
- NEW: Go Idiomatic Design Principles (Principle VI)
|
||||||
- IV. User Experience Consistency (用户体验一致性)
|
|
||||||
- V. Performance Requirements (性能要求)
|
|
||||||
|
|
||||||
SECTIONS ADDED:
|
ADDED SECTIONS:
|
||||||
- Development Workflow (开发工作流程)
|
- Principle VI: Go Idiomatic Design Principles (Go 味道 vs Java 味道)
|
||||||
- Quality Gates (质量关卡)
|
- Package organization (flat, not deep hierarchies)
|
||||||
|
- Interface design (small, focused interfaces)
|
||||||
|
- Error handling (explicit, not exceptions)
|
||||||
|
- Concurrency patterns (goroutines and channels)
|
||||||
|
- Naming conventions (Go-style, not Java camelCase)
|
||||||
|
- Anti-patterns to avoid (getters/setters, builder patterns, etc.)
|
||||||
|
|
||||||
|
SECTIONS EXPANDED:
|
||||||
|
- Code Quality Standards now explicitly forbid Java-style patterns
|
||||||
|
- Added Go project structure requirements
|
||||||
|
- Added explicit guidance against OOP over-engineering
|
||||||
|
|
||||||
TEMPLATES REQUIRING UPDATES:
|
TEMPLATES REQUIRING UPDATES:
|
||||||
✅ .specify/templates/plan-template.md - Constitution Check section aligned
|
✅ .specify/templates/plan-template.md - Added Go idiomatic checks
|
||||||
✅ .specify/templates/spec-template.md - Requirements section aligned with principles
|
✅ .specify/templates/spec-template.md - Added Go design requirements
|
||||||
✅ .specify/templates/tasks-template.md - Task organization reflects quality gates
|
✅ .specify/templates/tasks-template.md - Added Go code review tasks
|
||||||
|
|
||||||
FOLLOW-UP ACTIONS:
|
FOLLOW-UP ACTIONS:
|
||||||
- None - all placeholders resolved
|
- None - all placeholders resolved
|
||||||
|
|
||||||
RATIONALE:
|
RATIONALE:
|
||||||
Initial constitution establishment for 君鸿卡管系统 project. Major version 1.0.0
|
MAJOR version bump (2.0.0) - Fundamental shift in design philosophy from Java-style to
|
||||||
as this is the first formal governance document defining core development principles.
|
Go-idiomatic patterns. This is a breaking change in coding standards that affects all
|
||||||
|
existing and future code. Added new principle (VI) defining mandatory Go idioms and
|
||||||
|
explicitly forbidding Java patterns like deep class hierarchies, getter/setter methods,
|
||||||
|
excessive abstraction, and OOP-heavy designs.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# 君鸿卡管系统 Constitution
|
# 君鸿卡管系统 Constitution
|
||||||
@@ -44,10 +55,12 @@ as this is the first formal governance document defining core development princi
|
|||||||
- 所有日志记录 **MUST** 使用 Zap + Lumberjack.v2
|
- 所有日志记录 **MUST** 使用 Zap + Lumberjack.v2
|
||||||
- 所有 JSON 序列化 **MUST** 使用 sonic
|
- 所有 JSON 序列化 **MUST** 使用 sonic
|
||||||
- 所有异步任务 **MUST** 使用 Asynq
|
- 所有异步任务 **MUST** 使用 Asynq
|
||||||
|
- **MUST** 使用 Go 官方工具链:`go fmt`、`go vet`、`golangci-lint`
|
||||||
|
- **MUST** 使用 Go Modules 进行依赖管理
|
||||||
|
|
||||||
**理由 (RATIONALE):**
|
**理由 (RATIONALE):**
|
||||||
|
|
||||||
一致的技术栈使用确保代码可维护性、团队协作效率和长期技术债务可控。绕过框架的"快捷方式"会导致代码碎片化、难以调试、性能不一致和安全漏洞。框架选择已经过深思熟虑,必须信任并充分利用其生态系统。
|
一致的技术栈使用确保代码可维护性、团队协作效率和长期技术债务可控。绕过框架的"快捷方式"会导致代码碎片化、难以调试、性能不一致和安全漏洞。框架选择已经过深思熟虑,必须信任并充分利用其生态系统。Go 官方工具链确保代码风格一致性和质量。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,15 +73,43 @@ as this is the first formal governance document defining core development princi
|
|||||||
- Service 层 **MUST** 包含所有业务逻辑,支持跨模块调用
|
- Service 层 **MUST** 包含所有业务逻辑,支持跨模块调用
|
||||||
- Store 层 **MUST** 统一管理所有数据访问,支持事务处理
|
- Store 层 **MUST** 统一管理所有数据访问,支持事务处理
|
||||||
- Model 层 **MUST** 定义清晰的数据结构和 DTO
|
- Model 层 **MUST** 定义清晰的数据结构和 DTO
|
||||||
- 所有依赖 **MUST** 通过 `Service` 和 `Store` 结构体进行依赖注入
|
- 所有依赖 **MUST** 通过结构体字段进行依赖注入(不使用构造函数模式)
|
||||||
- 所有公共错误 **MUST** 在 `pkg/errors/` 中定义,使用统一错误码
|
- 所有公共错误 **MUST** 在 `pkg/errors/` 中定义,使用统一错误码
|
||||||
- 所有 API 响应 **MUST** 使用 `pkg/response/` 的统一格式
|
- 所有 API 响应 **MUST** 使用 `pkg/response/` 的统一格式
|
||||||
- **MUST** 为所有公共函数编写清晰的注释(英文或中文)
|
- 所有常量 **MUST** 在 `pkg/constants/` 中定义和管理
|
||||||
|
- 所有 Redis key **MUST** 通过 `pkg/constants/` 中的 Key 生成函数统一管理
|
||||||
|
- **MUST** 为所有导出的函数、类型和常量编写 Go 风格的文档注释(`// FunctionName does something...`)
|
||||||
- **MUST** 避免 magic numbers 和 magic strings,使用常量定义
|
- **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
|
||||||
|
```
|
||||||
|
|
||||||
**理由 (RATIONALE):**
|
**理由 (RATIONALE):**
|
||||||
|
|
||||||
清晰的分层架构和代码组织使代码易于理解、测试和维护。统一的错误处理和响应格式提升 API 一致性和客户端集成体验。依赖注入模式便于单元测试和模块替换。
|
清晰的分层架构和代码组织使代码易于理解、测试和维护。统一的错误处理和响应格式提升 API 一致性和客户端集成体验。依赖注入模式便于单元测试和模块替换。集中管理常量和 Redis key 避免拼写错误、重复定义和命名不一致,提升代码可维护性和重构安全性。Redis key 统一管理便于监控、调试和缓存策略调整。遵循 Go 官方代码风格确保代码一致性和可读性。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,14 +122,43 @@ as this is the first formal governance document defining core development princi
|
|||||||
- 所有数据库操作 **SHOULD** 有事务回滚测试
|
- 所有数据库操作 **SHOULD** 有事务回滚测试
|
||||||
- 测试 **MUST** 使用 Go 标准测试框架(`testing` 包)
|
- 测试 **MUST** 使用 Go 标准测试框架(`testing` 包)
|
||||||
- 测试文件 **MUST** 与源文件同目录,命名为 `*_test.go`
|
- 测试文件 **MUST** 与源文件同目录,命名为 `*_test.go`
|
||||||
|
- 测试函数 **MUST** 使用 `Test` 前缀:`func TestUserCreate(t *testing.T)`
|
||||||
|
- 基准测试 **MUST** 使用 `Benchmark` 前缀:`func BenchmarkUserCreate(b *testing.B)`
|
||||||
- 测试 **MUST** 可独立运行,不依赖外部服务(使用 mock 或 testcontainers)
|
- 测试 **MUST** 可独立运行,不依赖外部服务(使用 mock 或 testcontainers)
|
||||||
- 单元测试 **MUST** 在 100ms 内完成
|
- 单元测试 **MUST** 在 100ms 内完成
|
||||||
- 集成测试 **SHOULD** 在 1s 内完成
|
- 集成测试 **SHOULD** 在 1s 内完成
|
||||||
- 测试覆盖率 **SHOULD** 达到 70% 以上(核心业务代码必须 90% 以上)
|
- 测试覆盖率 **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):**
|
**理由 (RATIONALE):**
|
||||||
|
|
||||||
高质量的测试是代码质量的基石。单元测试确保业务逻辑正确性,集成测试确保模块间协作正常。快速的测试执行时间保证开发效率。测试独立性避免环境依赖导致的 flaky tests。
|
高质量的测试是代码质量的基石。单元测试确保业务逻辑正确性,集成测试确保模块间协作正常。快速的测试执行时间保证开发效率。测试独立性避免环境依赖导致的 flaky tests。Table-driven tests 使测试更简洁、易于扩展和维护,这是 Go 社区的最佳实践。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -133,10 +203,444 @@ as this is the first formal governance document defining core development princi
|
|||||||
- 内存使用(Worker 服务)**SHOULD** < 1GB(正常负载)
|
- 内存使用(Worker 服务)**SHOULD** < 1GB(正常负载)
|
||||||
- 数据库连接池 **MUST** 配置合理(`MaxOpenConns=25`, `MaxIdleConns=10`, `ConnMaxLifetime=5m`)
|
- 数据库连接池 **MUST** 配置合理(`MaxOpenConns=25`, `MaxIdleConns=10`, `ConnMaxLifetime=5m`)
|
||||||
- Redis 连接池 **MUST** 配置合理(`PoolSize=10`, `MinIdleConns=5`)
|
- Redis 连接池 **MUST** 配置合理(`PoolSize=10`, `MinIdleConns=5`)
|
||||||
|
- 并发操作 **SHOULD** 使用 goroutines 和 channels(不是线程池模式)
|
||||||
|
- **MUST** 使用 `context.Context` 进行超时和取消控制
|
||||||
|
- **MUST** 使用 `sync.Pool` 复用频繁分配的对象(如缓冲区)
|
||||||
|
|
||||||
**理由 (RATIONALE):**
|
**理由 (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 的性能优势和简洁性。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -164,10 +668,12 @@ as this is the first formal governance document defining core development princi
|
|||||||
|
|
||||||
- 所有 PR **MUST** 至少有一人审查通过
|
- 所有 PR **MUST** 至少有一人审查通过
|
||||||
- 审查者 **MUST** 验证:
|
- 审查者 **MUST** 验证:
|
||||||
- 代码符合本宪章所有原则
|
- 代码符合本宪章所有原则(特别是 Go 惯用法原则)
|
||||||
|
- 无 Java 风格的反模式(getter/setter、过度抽象等)
|
||||||
- 测试覆盖充分且通过
|
- 测试覆盖充分且通过
|
||||||
- 无安全漏洞(SQL 注入、XSS、命令注入等)
|
- 无安全漏洞(SQL 注入、XSS、命令注入等)
|
||||||
- 性能影响可接受
|
- 性能影响可接受
|
||||||
|
- 代码通过 `gofmt`、`go vet`、`golangci-lint` 检查
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,12 +682,17 @@ as this is the first formal governance document defining core development princi
|
|||||||
### 代码合并前检查
|
### 代码合并前检查
|
||||||
|
|
||||||
- [ ] 所有测试通过(`go test ./...`)
|
- [ ] 所有测试通过(`go test ./...`)
|
||||||
- [ ] 代码格式化(`gofmt` 或 `goimports`)
|
- [ ] 代码格式化(`gofmt -l .` 无输出)
|
||||||
- [ ] 代码静态检查通过(`go vet` 和 `golangci-lint`)
|
- [ ] 代码静态检查通过(`go vet ./...`)
|
||||||
- [ ] 测试覆盖率符合标准
|
- [ ] 代码质量检查通过(`golangci-lint run`)
|
||||||
|
- [ ] 测试覆盖率符合标准(`go test -cover ./...`)
|
||||||
- [ ] 无 TODO/FIXME 遗留(或已记录 Issue)
|
- [ ] 无 TODO/FIXME 遗留(或已记录 Issue)
|
||||||
- [ ] API 文档已更新(如有 API 变更)
|
- [ ] API 文档已更新(如有 API 变更)
|
||||||
- [ ] 数据库迁移文件已创建(如有 schema 变更)
|
- [ ] 数据库迁移文件已创建(如有 schema 变更)
|
||||||
|
- [ ] 常量和 Redis key 使用符合规范(无硬编码字符串)
|
||||||
|
- [ ] **无 Java 风格反模式**(无 getter/setter、无不必要接口、无过度抽象)
|
||||||
|
- [ ] **遵循 Go 命名约定**(缩写大小写一致、包名简短、无下划线)
|
||||||
|
- [ ] **使用 Go 惯用并发模式**(goroutines/channels,无线程池类)
|
||||||
|
|
||||||
### 上线前检查
|
### 上线前检查
|
||||||
|
|
||||||
@@ -214,12 +725,13 @@ as this is the first formal governance document defining core development princi
|
|||||||
|
|
||||||
- 所有 PR 审查 **MUST** 验证是否符合本宪章
|
- 所有 PR 审查 **MUST** 验证是否符合本宪章
|
||||||
- 违反宪章的代码 **MUST** 在合并前修正
|
- 违反宪章的代码 **MUST** 在合并前修正
|
||||||
|
- 特别关注 Go 惯用法原则,拒绝 Java 风格的代码
|
||||||
- 任何复杂性增加(新依赖、新架构层)**MUST** 在设计文档中明确说明必要性
|
- 任何复杂性增加(新依赖、新架构层)**MUST** 在设计文档中明确说明必要性
|
||||||
|
|
||||||
### 运行时开发指导
|
### 运行时开发指导
|
||||||
|
|
||||||
开发时参考本宪章确保一致性。如有疑问,优先遵守原则,再讨论例外情况。
|
开发时参考本宪章确保一致性。如有疑问,优先遵守原则,再讨论例外情况。记住:**写 Go 代码,不是用 Go 语法写 Java**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version**: 1.0.0 | **Ratified**: 2025-11-10 | **Last Amended**: 2025-11-10
|
**Version**: 2.0.0 | **Ratified**: 2025-11-10 | **Last Amended**: 2025-11-10
|
||||||
|
|||||||
@@ -37,20 +37,43 @@
|
|||||||
- [ ] All HTTP operations use Fiber framework
|
- [ ] All HTTP operations use Fiber framework
|
||||||
- [ ] All database operations use GORM
|
- [ ] All database operations use GORM
|
||||||
- [ ] All async tasks use Asynq
|
- [ ] All async tasks use Asynq
|
||||||
|
- [ ] Uses Go official toolchain: `go fmt`, `go vet`, `golangci-lint`
|
||||||
|
- [ ] Uses Go Modules for dependency management
|
||||||
|
|
||||||
**Code Quality Standards**:
|
**Code Quality Standards**:
|
||||||
- [ ] Follows Handler → Service → Store → Model architecture
|
- [ ] Follows Handler → Service → Store → Model architecture
|
||||||
- [ ] Handler layer only handles HTTP, no business logic
|
- [ ] Handler layer only handles HTTP, no business logic
|
||||||
- [ ] Service layer contains business logic with cross-module support
|
- [ ] Service layer contains business logic with cross-module support
|
||||||
- [ ] Store layer manages all data access with transaction support
|
- [ ] Store layer manages all data access with transaction support
|
||||||
- [ ] Uses dependency injection via Service/Store structs
|
- [ ] Uses dependency injection via struct fields (not constructor patterns)
|
||||||
- [ ] Unified error codes in `pkg/errors/`
|
- [ ] Unified error codes in `pkg/errors/`
|
||||||
- [ ] Unified API responses via `pkg/response/`
|
- [ ] Unified API responses via `pkg/response/`
|
||||||
|
- [ ] All constants defined in `pkg/constants/`
|
||||||
|
- [ ] All Redis keys managed via key generation functions (no hardcoded strings)
|
||||||
|
- [ ] All exported functions/types have Go-style doc comments
|
||||||
|
- [ ] Code formatted with `gofmt`
|
||||||
|
- [ ] Follows Effective Go and Go Code Review Comments
|
||||||
|
|
||||||
|
**Go Idiomatic Design**:
|
||||||
|
- [ ] Package structure is flat (max 2-3 levels), organized by feature
|
||||||
|
- [ ] Interfaces are small (1-3 methods), defined at use site
|
||||||
|
- [ ] No Java-style patterns: no I-prefix, no Impl-suffix, no getters/setters
|
||||||
|
- [ ] Error handling is explicit (return errors, no panic/recover abuse)
|
||||||
|
- [ ] Uses composition over inheritance
|
||||||
|
- [ ] Uses goroutines and channels (not thread pools)
|
||||||
|
- [ ] Uses `context.Context` for cancellation and timeouts
|
||||||
|
- [ ] Naming follows Go conventions: short receivers, consistent abbreviations (URL, ID, HTTP)
|
||||||
|
- [ ] No Hungarian notation or type prefixes
|
||||||
|
- [ ] Simple constructors (New/NewXxx), no Builder pattern unless necessary
|
||||||
|
|
||||||
**Testing Standards**:
|
**Testing Standards**:
|
||||||
- [ ] Unit tests for all core business logic (Service layer)
|
- [ ] Unit tests for all core business logic (Service layer)
|
||||||
- [ ] Integration tests for all API endpoints
|
- [ ] Integration tests for all API endpoints
|
||||||
- [ ] Tests use Go standard testing framework
|
- [ ] Tests use Go standard testing framework
|
||||||
|
- [ ] Test files named `*_test.go` in same directory
|
||||||
|
- [ ] Test functions use `Test` prefix, benchmarks use `Benchmark` prefix
|
||||||
|
- [ ] Table-driven tests for multiple test cases
|
||||||
|
- [ ] Test helpers marked with `t.Helper()`
|
||||||
- [ ] Tests are independent (no external service dependencies)
|
- [ ] Tests are independent (no external service dependencies)
|
||||||
- [ ] Target coverage: 70%+ overall, 90%+ for core business
|
- [ ] Target coverage: 70%+ overall, 90%+ for core business
|
||||||
|
|
||||||
@@ -69,6 +92,9 @@
|
|||||||
- [ ] List queries implement pagination (default 20, max 100)
|
- [ ] List queries implement pagination (default 20, max 100)
|
||||||
- [ ] Non-realtime operations use async tasks
|
- [ ] Non-realtime operations use async tasks
|
||||||
- [ ] Database and Redis connection pools properly configured
|
- [ ] Database and Redis connection pools properly configured
|
||||||
|
- [ ] Uses goroutines/channels for concurrency (not thread pools)
|
||||||
|
- [ ] Uses `context.Context` for timeout control
|
||||||
|
- [ ] Uses `sync.Pool` for frequently allocated objects
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|||||||
@@ -104,12 +104,26 @@
|
|||||||
- [ ] All async tasks use Asynq
|
- [ ] All async tasks use Asynq
|
||||||
- [ ] All logging uses Zap + Lumberjack.v2
|
- [ ] All logging uses Zap + Lumberjack.v2
|
||||||
- [ ] All configuration uses Viper
|
- [ ] All configuration uses Viper
|
||||||
|
- [ ] Uses Go official toolchain: `go fmt`, `go vet`, `golangci-lint`
|
||||||
|
|
||||||
**Architecture Requirements**:
|
**Architecture Requirements**:
|
||||||
- [ ] Implementation follows Handler → Service → Store → Model layers
|
- [ ] Implementation follows Handler → Service → Store → Model layers
|
||||||
- [ ] Dependencies injected via Service/Store structs
|
- [ ] Dependencies injected via struct fields (not constructor patterns)
|
||||||
- [ ] Unified error codes defined in `pkg/errors/`
|
- [ ] Unified error codes defined in `pkg/errors/`
|
||||||
- [ ] Unified API responses via `pkg/response/`
|
- [ ] Unified API responses via `pkg/response/`
|
||||||
|
- [ ] All constants defined in `pkg/constants/` (no magic numbers/strings)
|
||||||
|
- [ ] All Redis keys managed via `pkg/constants/` key generation functions
|
||||||
|
- [ ] Package structure is flat, organized by feature (not by layer)
|
||||||
|
|
||||||
|
**Go Idiomatic Design Requirements**:
|
||||||
|
- [ ] No Java-style patterns: no getter/setter methods, no I-prefix interfaces, no Impl-suffix
|
||||||
|
- [ ] Interfaces are small (1-3 methods), defined where used
|
||||||
|
- [ ] Error handling is explicit (return errors, not panic)
|
||||||
|
- [ ] Uses composition (struct embedding) not inheritance
|
||||||
|
- [ ] Uses goroutines and channels for concurrency
|
||||||
|
- [ ] Naming follows Go conventions: `UserID` not `userId`, `HTTPServer` not `HttpServer`
|
||||||
|
- [ ] No Hungarian notation or type prefixes
|
||||||
|
- [ ] Simple and direct code structure
|
||||||
|
|
||||||
**API Design Requirements**:
|
**API Design Requirements**:
|
||||||
- [ ] All APIs follow RESTful principles
|
- [ ] All APIs follow RESTful principles
|
||||||
@@ -125,10 +139,13 @@
|
|||||||
- [ ] Batch operations use bulk queries
|
- [ ] Batch operations use bulk queries
|
||||||
- [ ] List queries implement pagination (default 20, max 100)
|
- [ ] List queries implement pagination (default 20, max 100)
|
||||||
- [ ] Non-realtime operations delegated to async tasks
|
- [ ] Non-realtime operations delegated to async tasks
|
||||||
|
- [ ] Uses `context.Context` for timeouts and cancellation
|
||||||
|
|
||||||
**Testing Requirements**:
|
**Testing Requirements**:
|
||||||
- [ ] Unit tests for all Service layer business logic
|
- [ ] Unit tests for all Service layer business logic
|
||||||
- [ ] Integration tests for all API endpoints
|
- [ ] Integration tests for all API endpoints
|
||||||
|
- [ ] Tests use Go standard testing framework with `*_test.go` files
|
||||||
|
- [ ] Table-driven tests for multiple test cases
|
||||||
- [ ] Tests are independent and use mocks/testcontainers
|
- [ ] Tests are independent and use mocks/testcontainers
|
||||||
- [ ] Target coverage: 70%+ overall, 90%+ for core business logic
|
- [ ] Target coverage: 70%+ overall, 90%+ for core business logic
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ description: "Task list template for feature implementation"
|
|||||||
- [ ] T003 [P] Configure linting (golangci-lint) and formatting tools (gofmt/goimports)
|
- [ ] T003 [P] Configure linting (golangci-lint) and formatting tools (gofmt/goimports)
|
||||||
- [ ] T004 [P] Setup unified error codes in pkg/errors/
|
- [ ] T004 [P] Setup unified error codes in pkg/errors/
|
||||||
- [ ] T005 [P] Setup unified API response in pkg/response/
|
- [ ] T005 [P] Setup unified API response in pkg/response/
|
||||||
|
- [ ] T006 [P] Setup constants management in pkg/constants/ (business constants and Redis key functions)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,18 +65,18 @@ description: "Task list template for feature implementation"
|
|||||||
|
|
||||||
Foundational tasks for 君鸿卡管系统 tech stack:
|
Foundational tasks for 君鸿卡管系统 tech stack:
|
||||||
|
|
||||||
- [ ] T006 Setup PostgreSQL database connection via GORM with connection pool (MaxOpenConns=25, MaxIdleConns=10)
|
- [ ] T007 Setup PostgreSQL database connection via GORM with connection pool (MaxOpenConns=25, MaxIdleConns=10)
|
||||||
- [ ] T007 Setup Redis connection with connection pool (PoolSize=10, MinIdleConns=5)
|
- [ ] T008 Setup Redis connection with connection pool (PoolSize=10, MinIdleConns=5)
|
||||||
- [ ] T008 [P] Setup database migrations framework (golang-migrate or GORM AutoMigrate)
|
- [ ] T009 [P] Setup database migrations framework (golang-migrate or GORM AutoMigrate)
|
||||||
- [ ] T009 [P] Implement Fiber routing structure in internal/router/
|
- [ ] T010 [P] Implement Fiber routing structure in internal/router/
|
||||||
- [ ] T010 [P] Implement Fiber middleware (authentication, logging, recovery, validation) in internal/handler/middleware/
|
- [ ] T011 [P] Implement Fiber middleware (authentication, logging, recovery, validation) in internal/handler/middleware/
|
||||||
- [ ] T011 [P] Setup Zap logger with Lumberjack rotation in pkg/logger/
|
- [ ] T012 [P] Setup Zap logger with Lumberjack rotation in pkg/logger/
|
||||||
- [ ] T012 [P] Setup Viper configuration management in pkg/config/
|
- [ ] T013 [P] Setup Viper configuration management in pkg/config/
|
||||||
- [ ] T013 [P] Setup Asynq task queue client and server in pkg/queue/
|
- [ ] T014 [P] Setup Asynq task queue client and server in pkg/queue/
|
||||||
- [ ] T014 [P] Setup Validator integration in pkg/validator/
|
- [ ] T015 [P] Setup Validator integration in pkg/validator/
|
||||||
- [ ] T015 Create base Store structure with transaction support in internal/store/
|
- [ ] T016 Create base Store structure with transaction support in internal/store/
|
||||||
- [ ] T016 Create base Service structure with dependency injection in internal/service/
|
- [ ] T017 Create base Service structure with dependency injection in internal/service/
|
||||||
- [ ] T017 Setup sonic JSON as default serializer for Fiber
|
- [ ] T018 Setup sonic JSON as default serializer for Fiber
|
||||||
|
|
||||||
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
||||||
|
|
||||||
@@ -179,6 +180,11 @@ Foundational tasks for 君鸿卡管系统 tech stack:
|
|||||||
- [ ] TXXX Quality Gate: Check no TODO/FIXME remains (or documented in issues)
|
- [ ] TXXX Quality Gate: Check no TODO/FIXME remains (or documented in issues)
|
||||||
- [ ] TXXX Quality Gate: Verify database migrations work correctly
|
- [ ] TXXX Quality Gate: Verify database migrations work correctly
|
||||||
- [ ] TXXX Quality Gate: Verify API documentation updated (if API changes)
|
- [ ] TXXX Quality Gate: Verify API documentation updated (if API changes)
|
||||||
|
- [ ] TXXX Quality Gate: Verify no hardcoded constants or Redis keys (all use pkg/constants/)
|
||||||
|
- [ ] TXXX Quality Gate: Verify no Java-style anti-patterns (no getter/setter, no I-prefix, no Impl-suffix)
|
||||||
|
- [ ] TXXX Quality Gate: Verify Go naming conventions (UserID not userId, HTTPServer not HttpServer)
|
||||||
|
- [ ] TXXX Quality Gate: Verify error handling is explicit (no panic/recover abuse)
|
||||||
|
- [ ] TXXX Quality Gate: Verify uses goroutines/channels (not thread pool patterns)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
30
CLAUDE.md
Normal file
30
CLAUDE.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# junhong_cmp_fiber Development Guidelines
|
||||||
|
|
||||||
|
Auto-generated from all feature plans. Last updated: 2025-11-10
|
||||||
|
|
||||||
|
## Active Technologies
|
||||||
|
|
||||||
|
- Go 1.25.1 (001-fiber-middleware-integration)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/
|
||||||
|
frontend/
|
||||||
|
tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
# Add commands for Go 1.25.1
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
Go 1.25.1: Follow standard conventions
|
||||||
|
|
||||||
|
## Recent Changes
|
||||||
|
|
||||||
|
- 001-fiber-middleware-integration: Added Go 1.25.1
|
||||||
|
|
||||||
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
<!-- MANUAL ADDITIONS END -->
|
||||||
43
READEME.md
43
READEME.md
@@ -41,6 +41,7 @@ junhong_cmp_fiber/
|
|||||||
│ ├── queue/ # 队列封装(Asynq)
|
│ ├── queue/ # 队列封装(Asynq)
|
||||||
│ ├── response/ # 统一响应格式
|
│ ├── response/ # 统一响应格式
|
||||||
│ ├── errors/ # 错误码定义
|
│ ├── errors/ # 错误码定义
|
||||||
|
│ ├── constants/ # 常量定义(业务常量、Redis Key 管理)
|
||||||
│ └── validator/ # 验证器封装
|
│ └── validator/ # 验证器封装
|
||||||
│
|
│
|
||||||
├── config/ # 配置文件(yaml)
|
├── config/ # 配置文件(yaml)
|
||||||
@@ -94,9 +95,26 @@ store.Transaction(ctx, func(tx *store.Store) error {
|
|||||||
|
|
||||||
### 异步任务
|
### 异步任务
|
||||||
- 高频任务:批量状态同步、流量同步、实名检查
|
- 高频任务:批量状态同步、流量同步、实名检查
|
||||||
- 业务任务:分佣计算、生命周期变更通知
|
- 业务任务:分佣计算、生命周期变更通知
|
||||||
- 任务优先级:critical > default > low
|
- 任务优先级:critical > default > low
|
||||||
|
|
||||||
|
### 常量和 Redis Key 管理
|
||||||
|
所有常量统一在 `pkg/constants/` 目录管理:
|
||||||
|
```go
|
||||||
|
// 业务常量
|
||||||
|
constants.SIMStatusActive
|
||||||
|
constants.SIMStatusInactive
|
||||||
|
|
||||||
|
// Redis Key 管理(统一使用 Key 生成函数)
|
||||||
|
constants.RedisSIMStatusKey(iccid) // sim:status:{iccid}
|
||||||
|
constants.RedisAgentCommissionKey(agentID) // agent:commission:{agentID}
|
||||||
|
constants.RedisTaskLockKey(taskName) // task:lock:{taskName}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
key := constants.RedisSIMStatusKey("898600...")
|
||||||
|
rdb.Set(ctx, key, status, time.Hour)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
@@ -324,19 +342,16 @@ go run cmd/worker/main.go
|
|||||||
# 3. 生成实现计划(包含技术调研和设计)
|
# 3. 生成实现计划(包含技术调研和设计)
|
||||||
/speckit.plan
|
/speckit.plan
|
||||||
|
|
||||||
# 4. 分析一致性(可选,验证规划质量)
|
# 4. 生成任务列表
|
||||||
/speckit.analyze
|
|
||||||
|
|
||||||
# 5. 生成任务列表
|
|
||||||
/speckit.tasks
|
/speckit.tasks
|
||||||
|
|
||||||
# 6. 再次分析一致性(推荐)
|
# 5. 再次分析一致性(推荐)
|
||||||
/speckit.analyze
|
/speckit.analyze
|
||||||
|
|
||||||
# 7. 执行实现
|
# 6. 执行实现
|
||||||
/speckit.implement
|
/speckit.implement
|
||||||
|
|
||||||
# 8. 代码审查和合并
|
# 7. 代码审查和合并
|
||||||
# - 验证宪章符合性
|
# - 验证宪章符合性
|
||||||
# - 确保所有测试通过
|
# - 确保所有测试通过
|
||||||
# - 检查代码覆盖率
|
# - 检查代码覆盖率
|
||||||
@@ -373,7 +388,7 @@ go run cmd/worker/main.go
|
|||||||
- ✅ 确保数据库索引支持查询
|
- ✅ 确保数据库索引支持查询
|
||||||
|
|
||||||
#### API 设计
|
#### API 设计
|
||||||
- ✅ 使用统一 JSON 响应格式(code/message/data/timestamp)
|
- ✅ 使用统一 JSON 响应格式(code/msg/data/timestamp)
|
||||||
- ✅ 错误消息中英文双语
|
- ✅ 错误消息中英文双语
|
||||||
- ✅ 时间字段使用 ISO 8601 格式
|
- ✅ 时间字段使用 ISO 8601 格式
|
||||||
- ✅ 金额字段使用整数(分)
|
- ✅ 金额字段使用整数(分)
|
||||||
@@ -407,17 +422,17 @@ specs/
|
|||||||
|
|
||||||
### 常见问题
|
### 常见问题
|
||||||
|
|
||||||
**Q: 为什么要使用 Speckit?**
|
**Q: 为什么要使用 Speckit?**
|
||||||
A: Speckit 确保团队遵循统一的开发流程,减少沟通成本,提高代码质量和可维护性。
|
A: Speckit 确保团队遵循统一的开发流程,减少沟通成本,提高代码质量和可维护性。
|
||||||
|
|
||||||
**Q: 可以跳过某些步骤吗?**
|
**Q: 可以跳过某些步骤吗?**
|
||||||
A: 不推荐。每个步骤都有明确目的,跳过可能导致需求不清、架构不合理或测试不足。
|
A: 不推荐。每个步骤都有明确目的,跳过可能导致需求不清、架构不合理或测试不足。
|
||||||
|
|
||||||
**Q: 如何处理紧急修复?**
|
**Q: 如何处理紧急修复?**
|
||||||
A: 小型修复可以简化流程,但仍需遵循宪章原则(技术栈、测试、代码质量)。
|
A: 小型修复可以简化流程,但仍需遵循宪章原则(技术栈、测试、代码质量)。
|
||||||
|
|
||||||
**Q: 宪章可以修改吗?**
|
**Q: 宪章可以修改吗?**
|
||||||
A: 可以,使用 `/speckit.constitution` 修改。修改需要团队共识,并会自动同步所有模板。
|
A: 可以,使用 `/speckit.constitution` 修改。修改需要团队共识,并会自动同步所有模板。
|
||||||
|
|
||||||
**Q: 测试覆盖率达不到 70% 怎么办?**
|
**Q: 测试覆盖率达不到 70% 怎么办?**
|
||||||
A: 核心业务逻辑必须达到 90%+。工具函数、简单的 getter/setter 可以适当放宽,但总体必须 > 70%。
|
A: 核心业务逻辑必须达到 90%+。工具函数、简单的 getter/setter 可以适当放宽,但总体必须 > 70%。
|
||||||
|
|||||||
@@ -1 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
AppName: "君鸿卡管系统 v0.0.1",
|
||||||
|
StrictRouting: true,
|
||||||
|
CaseSensitive: true,
|
||||||
|
JSONEncoder: sonic.Marshal,
|
||||||
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
// Prefork: true, // 该字段是用来开启监听端口是否可以被多个应用监听的,非必要不要开了
|
||||||
|
})
|
||||||
|
//压缩中间件-根据Accept-Encoding使用gzip 、 deflate 和 brotli 压缩来压缩响应
|
||||||
|
app.Use(compress.New(compress.Config{
|
||||||
|
// Next: func(c *fiber.Ctx) bool { return c.Path() == "/test"}, //返回值为true时会跳过压缩中间件
|
||||||
|
Level: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.Listen(":3000")
|
||||||
|
}
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -2,18 +2,27 @@ module junhong_cmp_fiber
|
|||||||
|
|
||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
require github.com/gofiber/fiber/v2 v2.52.9
|
require (
|
||||||
|
github.com/bytedance/sonic v1.14.2
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.9
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
52
go.sum
52
go.sum
@@ -1,27 +1,79 @@
|
|||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||||
|
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||||
|
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Specification Quality Checklist: Fiber Middleware Integration with Configuration Management
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2025-11-10
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Validation Results
|
||||||
|
|
||||||
|
### Content Quality Assessment
|
||||||
|
|
||||||
|
✅ **PASS**: The specification focuses on user value and business outcomes without implementation details. While technical components (Viper, Zap, Redis) are mentioned in the functional requirements, they describe **what capabilities** the system must provide (configuration management, structured logging, token validation) rather than **how to implement** them. The Technical Requirements section appropriately references the project constitution's technology choices.
|
||||||
|
|
||||||
|
✅ **PASS**: The specification is written from a business/operations perspective with user stories framed around administrator needs, API consumer expectations, and system reliability outcomes.
|
||||||
|
|
||||||
|
✅ **PASS**: All mandatory sections (User Scenarios, Requirements, Success Criteria) are completed with detailed content.
|
||||||
|
|
||||||
|
### Requirement Completeness Assessment
|
||||||
|
|
||||||
|
✅ **PASS**: No [NEEDS CLARIFICATION] markers present. All requirements are concrete and specific.
|
||||||
|
|
||||||
|
✅ **PASS**: All functional requirements are testable with clear expected behaviors (e.g., "detect changes within 5 seconds", "return HTTP 401 with specific error format").
|
||||||
|
|
||||||
|
✅ **PASS**: Success criteria include specific measurable metrics:
|
||||||
|
- Time-based: "within 5 seconds", "within 50ms", "within 100ms"
|
||||||
|
- Percentage-based: "100% consistency", "100% of HTTP requests"
|
||||||
|
- Behavior-based: "automatically rotate when reaching configured size limits"
|
||||||
|
|
||||||
|
✅ **PASS**: Success criteria are technology-agnostic, focusing on user/system outcomes (e.g., "System administrators can modify configuration and see it applied" rather than "Viper watches config files").
|
||||||
|
|
||||||
|
✅ **PASS**: All user stories include detailed acceptance scenarios in Given-When-Then format.
|
||||||
|
|
||||||
|
✅ **PASS**: Edge cases section covers 7 different failure/boundary scenarios with expected behaviors.
|
||||||
|
|
||||||
|
✅ **PASS**: Scope is clearly bounded through prioritized user stories (P1, P2, P3) and explicitly states rate limiting is "implemented but disabled by default".
|
||||||
|
|
||||||
|
✅ **PASS**: Dependencies identified through Technical Requirements section referencing constitution standards, and implicit dependencies on Redis for token validation.
|
||||||
|
|
||||||
|
### Feature Readiness Assessment
|
||||||
|
|
||||||
|
✅ **PASS**: Each of the 22 functional requirements maps to acceptance scenarios in user stories and success criteria.
|
||||||
|
|
||||||
|
✅ **PASS**: Seven prioritized user stories cover the complete feature set from foundational (configuration, logging) to security (authentication) to optional (rate limiting).
|
||||||
|
|
||||||
|
✅ **PASS**: Ten success criteria provide measurable validation for all major functional areas.
|
||||||
|
|
||||||
|
✅ **PASS**: Technical Requirements section appropriately references existing project standards rather than introducing new implementation details.
|
||||||
|
|
||||||
|
## Overall Assessment
|
||||||
|
|
||||||
|
**STATUS**: ✅ **READY FOR PLANNING**
|
||||||
|
|
||||||
|
All checklist items pass validation. The specification is complete, testable, and ready for the `/speckit.plan` phase.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The specification successfully balances business requirements with project constitution compliance
|
||||||
|
- User stories are well-prioritized with clear independent test criteria
|
||||||
|
- Edge cases demonstrate thorough consideration of failure scenarios
|
||||||
|
- Success criteria provide clear acceptance thresholds without prescribing implementation approaches
|
||||||
432
specs/001-fiber-middleware-integration/contracts/api.yaml
Normal file
432
specs/001-fiber-middleware-integration/contracts/api.yaml
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: 君鸿卡管系统 API
|
||||||
|
description: |
|
||||||
|
Card Management System API with unified response format and middleware integration.
|
||||||
|
|
||||||
|
## Unified Response Format
|
||||||
|
|
||||||
|
All API endpoints return responses in the following unified format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {},
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
| Code | HTTP Status | Description (EN) | Description (ZH) |
|
||||||
|
|------|-------------|------------------|------------------|
|
||||||
|
| 0 | 200 | Success | 成功 |
|
||||||
|
| 1000 | 500 | Internal server error | 内部服务器错误 |
|
||||||
|
| 1001 | 401 | Missing authentication token | 缺失认证令牌 |
|
||||||
|
| 1002 | 401 | Invalid or expired token | 令牌无效或已过期 |
|
||||||
|
| 1003 | 429 | Too many requests | 请求过于频繁 |
|
||||||
|
| 1004 | 503 | Authentication service unavailable | 认证服务不可用 |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Protected endpoints require a valid authentication token in the request header:
|
||||||
|
|
||||||
|
```
|
||||||
|
token: your-auth-token-here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request Tracing
|
||||||
|
|
||||||
|
Every response includes an `X-Request-ID` header containing a unique UUID v4 identifier for request tracing and correlation.
|
||||||
|
|
||||||
|
version: 0.0.1
|
||||||
|
contact:
|
||||||
|
name: API Support
|
||||||
|
email: support@example.com
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:3000
|
||||||
|
description: Development server
|
||||||
|
- url: https://api-staging.example.com
|
||||||
|
description: Staging server
|
||||||
|
- url: https://api.example.com
|
||||||
|
description: Production server
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: health
|
||||||
|
description: Health check and system status
|
||||||
|
- name: users
|
||||||
|
description: User management (example endpoints)
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- health
|
||||||
|
summary: Health check endpoint
|
||||||
|
description: Returns system health status. No authentication required.
|
||||||
|
operationId: getHealth
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: System is healthy
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessResponse'
|
||||||
|
example:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
status: "healthy"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
/api/v1/users:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: List users (example endpoint)
|
||||||
|
description: |
|
||||||
|
Returns a list of users. Demonstrates middleware integration:
|
||||||
|
- Request ID generation
|
||||||
|
- Authentication (keyauth)
|
||||||
|
- Access logging
|
||||||
|
- Rate limiting (if enabled)
|
||||||
|
operationId: listUsers
|
||||||
|
security:
|
||||||
|
- TokenAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
description: Page number (1-indexed)
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
minimum: 1
|
||||||
|
- name: page_size
|
||||||
|
in: query
|
||||||
|
description: Number of items per page
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successfully retrieved user list
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessResponse'
|
||||||
|
example:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
- id: "user-123"
|
||||||
|
name: "John Doe"
|
||||||
|
email: "john@example.com"
|
||||||
|
- id: "user-456"
|
||||||
|
name: "Jane Smith"
|
||||||
|
email: "jane@example.com"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/ServiceUnavailable'
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: Create user (example endpoint)
|
||||||
|
description: Creates a new user. Requires authentication.
|
||||||
|
operationId: createUser
|
||||||
|
security:
|
||||||
|
- TokenAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- email
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: "John Doe"
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
example: "john@example.com"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User created successfully
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessResponse'
|
||||||
|
example:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
id: "user-789"
|
||||||
|
name: "John Doe"
|
||||||
|
email: "john@example.com"
|
||||||
|
created_at: "2025-11-10T15:30:45Z"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
|
||||||
|
/api/v1/users/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: Get user by ID (example endpoint)
|
||||||
|
description: Returns a single user by ID. Requires authentication.
|
||||||
|
operationId: getUserByID
|
||||||
|
security:
|
||||||
|
- TokenAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: User ID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: "user-123"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successfully retrieved user
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessResponse'
|
||||||
|
example:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
id: "user-123"
|
||||||
|
name: "John Doe"
|
||||||
|
email: "john@example.com"
|
||||||
|
created_at: "2025-11-10T15:00:00Z"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
code: 2001
|
||||||
|
data: null
|
||||||
|
msg: "User not found"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
TokenAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: token
|
||||||
|
description: Authentication token stored in Redis (token → user ID mapping)
|
||||||
|
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
description: Unique request identifier (UUID v4) for tracing and correlation
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
SuccessResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- data
|
||||||
|
- msg
|
||||||
|
- timestamp
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: integer
|
||||||
|
description: Application status code (0 = success)
|
||||||
|
example: 0
|
||||||
|
data:
|
||||||
|
description: Response data (object, array, or null)
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
- type: array
|
||||||
|
- type: 'null'
|
||||||
|
msg:
|
||||||
|
type: string
|
||||||
|
description: Human-readable message
|
||||||
|
example: "success"
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Response timestamp in ISO 8601 format (RFC3339)
|
||||||
|
example: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- data
|
||||||
|
- msg
|
||||||
|
- timestamp
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: integer
|
||||||
|
description: Application error code (non-zero)
|
||||||
|
example: 1002
|
||||||
|
data:
|
||||||
|
type: 'null'
|
||||||
|
description: Always null for error responses
|
||||||
|
example: null
|
||||||
|
msg:
|
||||||
|
type: string
|
||||||
|
description: Error message (bilingual support)
|
||||||
|
example: "Invalid or expired token"
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Response timestamp in ISO 8601 format (RFC3339)
|
||||||
|
example: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
responses:
|
||||||
|
Unauthorized:
|
||||||
|
description: Authentication failed
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
examples:
|
||||||
|
missing_token:
|
||||||
|
summary: Missing token
|
||||||
|
value:
|
||||||
|
code: 1001
|
||||||
|
data: null
|
||||||
|
msg: "Missing authentication token"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
invalid_token:
|
||||||
|
summary: Invalid or expired token
|
||||||
|
value:
|
||||||
|
code: 1002
|
||||||
|
data: null
|
||||||
|
msg: "Invalid or expired token"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
TooManyRequests:
|
||||||
|
description: Rate limit exceeded
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
Retry-After:
|
||||||
|
description: Number of seconds to wait before retrying
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
example: 60
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
code: 1003
|
||||||
|
data: null
|
||||||
|
msg: "Too many requests"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
InternalServerError:
|
||||||
|
description: Internal server error
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
code: 1000
|
||||||
|
data: null
|
||||||
|
msg: "Internal server error"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
ServiceUnavailable:
|
||||||
|
description: Service unavailable (e.g., Redis down)
|
||||||
|
headers:
|
||||||
|
X-Request-ID:
|
||||||
|
$ref: '#/components/headers/X-Request-ID'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
code: 1004
|
||||||
|
data: null
|
||||||
|
msg: "Authentication service unavailable"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
examples:
|
||||||
|
SuccessWithObject:
|
||||||
|
summary: Success response with object data
|
||||||
|
value:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
id: "user-123"
|
||||||
|
name: "John Doe"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
SuccessWithArray:
|
||||||
|
summary: Success response with array data
|
||||||
|
value:
|
||||||
|
code: 0
|
||||||
|
data:
|
||||||
|
- id: "1"
|
||||||
|
name: "Item 1"
|
||||||
|
- id: "2"
|
||||||
|
name: "Item 2"
|
||||||
|
msg: "success"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
|
|
||||||
|
SuccessWithNull:
|
||||||
|
summary: Success response with null data
|
||||||
|
value:
|
||||||
|
code: 0
|
||||||
|
data: null
|
||||||
|
msg: "Operation completed"
|
||||||
|
timestamp: "2025-11-10T15:30:45Z"
|
||||||
678
specs/001-fiber-middleware-integration/data-model.md
Normal file
678
specs/001-fiber-middleware-integration/data-model.md
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
# Data Model: Fiber Middleware Integration
|
||||||
|
|
||||||
|
**Feature**: 001-fiber-middleware-integration
|
||||||
|
**Date**: 2025-11-10
|
||||||
|
**Phase**: 1 - Design & Contracts
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the data structures, entities, and models used in the middleware integration feature. All structures follow Go idiomatic design principles with simple, flat structures and no Java-style patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Configuration Model
|
||||||
|
|
||||||
|
### Config Structure
|
||||||
|
|
||||||
|
**File**: `pkg/config/config.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `mapstructure:"server"`
|
||||||
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
|
Middleware MiddlewareConfig `mapstructure:"middleware"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig contains HTTP server settings
|
||||||
|
type ServerConfig struct {
|
||||||
|
Address string `mapstructure:"address"` // e.g., ":3000"
|
||||||
|
ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "10s"
|
||||||
|
WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "10s"
|
||||||
|
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // e.g., "30s"
|
||||||
|
Prefork bool `mapstructure:"prefork"` // Multi-process mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisConfig contains Redis connection settings
|
||||||
|
type RedisConfig struct {
|
||||||
|
Address string `mapstructure:"address"` // e.g., "localhost:6379"
|
||||||
|
Password string `mapstructure:"password"` // Leave empty if no auth
|
||||||
|
DB int `mapstructure:"db"` // Database number (0-15)
|
||||||
|
PoolSize int `mapstructure:"pool_size"` // Max connections
|
||||||
|
MinIdleConns int `mapstructure:"min_idle_conns"` // Keep-alive connections
|
||||||
|
DialTimeout time.Duration `mapstructure:"dial_timeout"` // e.g., "5s"
|
||||||
|
ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "3s"
|
||||||
|
WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "3s"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggingConfig contains logging settings
|
||||||
|
type LoggingConfig struct {
|
||||||
|
Level string `mapstructure:"level"` // debug, info, warn, error
|
||||||
|
Development bool `mapstructure:"development"` // Enable dev mode (pretty print)
|
||||||
|
AppLog LogRotationConfig `mapstructure:"app_log"` // Application log settings
|
||||||
|
AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP access log settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogRotationConfig contains log rotation settings for Lumberjack
|
||||||
|
type LogRotationConfig struct {
|
||||||
|
Filename string `mapstructure:"filename"` // Log file path
|
||||||
|
MaxSize int `mapstructure:"max_size"` // Max size in MB before rotation
|
||||||
|
MaxBackups int `mapstructure:"max_backups"` // Max number of old files to keep
|
||||||
|
MaxAge int `mapstructure:"max_age"` // Max days to retain old files
|
||||||
|
Compress bool `mapstructure:"compress"` // Compress rotated files
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareConfig contains middleware settings
|
||||||
|
type MiddlewareConfig struct {
|
||||||
|
EnableAuth bool `mapstructure:"enable_auth"` // Enable keyauth middleware
|
||||||
|
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // Enable limiter (default: false)
|
||||||
|
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // Rate limiter settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimiterConfig contains rate limiter settings
|
||||||
|
type RateLimiterConfig struct {
|
||||||
|
Max int `mapstructure:"max"` // Max requests per window
|
||||||
|
Expiration time.Duration `mapstructure:"expiration"` // Time window (e.g., "1m")
|
||||||
|
Storage string `mapstructure:"storage"` // "memory" or "redis"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates configuration values
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if c.Server.Address == "" {
|
||||||
|
return errors.New("server address cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Server.ReadTimeout <= 0 {
|
||||||
|
return errors.New("server read timeout must be positive")
|
||||||
|
}
|
||||||
|
if c.Redis.Address == "" {
|
||||||
|
return errors.New("redis address cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Redis.PoolSize <= 0 {
|
||||||
|
return errors.New("redis pool size must be positive")
|
||||||
|
}
|
||||||
|
if c.Logging.AppLog.Filename == "" {
|
||||||
|
return errors.New("app log filename cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Logging.AccessLog.Filename == "" {
|
||||||
|
return errors.New("access log filename cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Middleware.RateLimiter.Max <= 0 {
|
||||||
|
c.Middleware.RateLimiter.Max = 100 // Default
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example YAML Configuration** (`configs/config.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":3000"
|
||||||
|
read_timeout: "10s"
|
||||||
|
write_timeout: "10s"
|
||||||
|
shutdown_timeout: "30s"
|
||||||
|
prefork: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
address: "localhost:6379"
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
pool_size: 10
|
||||||
|
min_idle_conns: 5
|
||||||
|
dial_timeout: "5s"
|
||||||
|
read_timeout: "3s"
|
||||||
|
write_timeout: "3s"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
development: false
|
||||||
|
app_log:
|
||||||
|
filename: "logs/app.log"
|
||||||
|
max_size: 100 # MB
|
||||||
|
max_backups: 30
|
||||||
|
max_age: 30 # days
|
||||||
|
compress: true
|
||||||
|
access_log:
|
||||||
|
filename: "logs/access.log"
|
||||||
|
max_size: 500 # MB
|
||||||
|
max_backups: 90
|
||||||
|
max_age: 90 # days
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
middleware:
|
||||||
|
enable_auth: true
|
||||||
|
enable_rate_limiter: false # Disabled by default
|
||||||
|
rate_limiter:
|
||||||
|
max: 100 # requests
|
||||||
|
expiration: "1m" # per minute
|
||||||
|
storage: "memory" # or "redis"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
- All required fields must be non-empty
|
||||||
|
- Timeouts must be positive durations
|
||||||
|
- Pool sizes must be positive integers
|
||||||
|
- Log filenames must be valid paths
|
||||||
|
- Rate limiter max must be positive (defaults to 100)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Authentication Model
|
||||||
|
|
||||||
|
### AuthToken Entity
|
||||||
|
|
||||||
|
**Storage**: Redis key-value pair
|
||||||
|
**Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`)
|
||||||
|
**Value Format**: Plain string containing user ID
|
||||||
|
**TTL**: Managed by Redis (set when token is created)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Token validation doesn't use a struct - just Redis key-value
|
||||||
|
// Key: "auth:token:abc123def456"
|
||||||
|
// Value: "user-789"
|
||||||
|
// TTL: 3600 seconds (1 hour)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Redis Operations**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Store token (example - not part of this feature)
|
||||||
|
rdb.Set(ctx, constants.RedisAuthTokenKey(token), userID, 1*time.Hour)
|
||||||
|
|
||||||
|
// Validate token (this feature)
|
||||||
|
userID, err := rdb.Get(ctx, constants.RedisAuthTokenKey(token)).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Token not found or expired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete token (example - logout feature)
|
||||||
|
rdb.Del(ctx, constants.RedisAuthTokenKey(token))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Generation Function** (`pkg/constants/redis.go`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RedisAuthTokenKey generates Redis key for authentication tokens
|
||||||
|
func RedisAuthTokenKey(token string) string {
|
||||||
|
return fmt.Sprintf("auth:token:%s", token)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
- Token must exist as Redis key
|
||||||
|
- Redis must be available (fail closed if not)
|
||||||
|
- Token value must be non-empty user ID
|
||||||
|
- TTL is checked automatically by Redis (expired keys return `redis.Nil`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Request Context Model
|
||||||
|
|
||||||
|
### RequestContext Structure
|
||||||
|
|
||||||
|
**File**: `pkg/middleware/context.go` (or stored in Fiber's `c.Locals()`)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Request context is stored in Fiber's Locals, not a struct
|
||||||
|
// Access via: c.Locals("key")
|
||||||
|
|
||||||
|
// Request ID (set by requestid middleware)
|
||||||
|
requestID := c.Locals("requestid").(string) // UUID v4 string
|
||||||
|
|
||||||
|
// User ID (set by keyauth middleware after validation)
|
||||||
|
userID, ok := c.Locals("user_id").(string)
|
||||||
|
if !ok {
|
||||||
|
// Not authenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start time (for duration calculation)
|
||||||
|
startTime := c.Locals("start_time").(time.Time)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Context Keys** (constants in `pkg/constants/constants.go`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
// Context keys for Fiber Locals
|
||||||
|
ContextKeyRequestID = "requestid"
|
||||||
|
ContextKeyUserID = "user_id"
|
||||||
|
ContextKeyStartTime = "start_time"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lifecycle**:
|
||||||
|
1. **requestid middleware**: Sets `requestid` in Locals (UUID v4)
|
||||||
|
2. **logger middleware**: Sets `start_time` in Locals
|
||||||
|
3. **keyauth middleware**: Sets `user_id` in Locals (after validation)
|
||||||
|
4. **handler**: Accesses context values from Locals
|
||||||
|
5. **logger middleware** (after handler): Calculates duration and logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Log Entry Models
|
||||||
|
|
||||||
|
### Application Log Entry
|
||||||
|
|
||||||
|
**Format**: JSON
|
||||||
|
**Output**: `logs/app.log`
|
||||||
|
**Logger**: Zap (appLogger instance)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-10T15:30:45.123Z",
|
||||||
|
"level": "info",
|
||||||
|
"logger": "service.user",
|
||||||
|
"caller": "user/service.go:42",
|
||||||
|
"message": "User created successfully",
|
||||||
|
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "user-789",
|
||||||
|
"username": "john_doe",
|
||||||
|
"ip": "192.168.1.100"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `timestamp`: ISO 8601 format (RFC3339)
|
||||||
|
- `level`: debug, info, warn, error
|
||||||
|
- `logger`: Logger name (optional, for structured logging)
|
||||||
|
- `caller`: Source file and line number
|
||||||
|
- `message`: Log message
|
||||||
|
- `request_id`: Request correlation ID (if available)
|
||||||
|
- `user_id`: Authenticated user ID (if available)
|
||||||
|
- Custom fields: Any additional context-specific fields
|
||||||
|
|
||||||
|
**Zap Usage**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
appLogger.Info("User created successfully",
|
||||||
|
zap.String("request_id", requestID),
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("username", username),
|
||||||
|
zap.String("ip", ip),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Log Entry
|
||||||
|
|
||||||
|
**Format**: JSON
|
||||||
|
**Output**: `logs/access.log`
|
||||||
|
**Logger**: Zap (accessLogger instance)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-10T15:30:45.123Z",
|
||||||
|
"level": "info",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/v1/users",
|
||||||
|
"status": 200,
|
||||||
|
"duration_ms": 45.234,
|
||||||
|
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"user_agent": "Mozilla/5.0...",
|
||||||
|
"user_id": "user-789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `timestamp`: ISO 8601 format (RFC3339)
|
||||||
|
- `level`: Always "info" for access logs
|
||||||
|
- `method`: HTTP method (GET, POST, PUT, DELETE)
|
||||||
|
- `path`: Request path
|
||||||
|
- `status`: HTTP status code
|
||||||
|
- `duration_ms`: Request duration in milliseconds
|
||||||
|
- `request_id`: Request correlation ID
|
||||||
|
- `ip`: Client IP address
|
||||||
|
- `user_agent`: User-Agent header
|
||||||
|
- `user_id`: Authenticated user ID (if available, empty if not)
|
||||||
|
|
||||||
|
**Zap Usage**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
accessLogger.Info("",
|
||||||
|
zap.String("method", c.Method()),
|
||||||
|
zap.String("path", c.Path()),
|
||||||
|
zap.Int("status", c.Response().StatusCode()),
|
||||||
|
zap.Float64("duration_ms", duration.Seconds()*1000),
|
||||||
|
zap.String("request_id", requestID),
|
||||||
|
zap.String("ip", c.IP()),
|
||||||
|
zap.String("user_agent", c.Get("User-Agent")),
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API Response Model
|
||||||
|
|
||||||
|
### Unified Response Structure
|
||||||
|
|
||||||
|
**File**: `pkg/response/response.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Response is the unified API response structure
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"` // Application error code (0 = success)
|
||||||
|
Data interface{} `json:"data"` // Response data (object, array, or null)
|
||||||
|
Message string `json:"msg"` // Human-readable message
|
||||||
|
Timestamp string `json:"timestamp"` // ISO 8601 timestamp (optional, can be added)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success returns a successful response
|
||||||
|
func Success(c *fiber.Ctx, data interface{}) error {
|
||||||
|
return c.JSON(Response{
|
||||||
|
Code: 0,
|
||||||
|
Data: data,
|
||||||
|
Message: "success",
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns an error response
|
||||||
|
func Error(c *fiber.Ctx, httpStatus int, code int, message string) error {
|
||||||
|
return c.Status(httpStatus).JSON(Response{
|
||||||
|
Code: code,
|
||||||
|
Data: nil,
|
||||||
|
Message: message,
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuccessWithMessage returns a successful response with custom message
|
||||||
|
func SuccessWithMessage(c *fiber.Ctx, data interface{}, message string) error {
|
||||||
|
return c.JSON(Response{
|
||||||
|
Code: 0,
|
||||||
|
Data: data,
|
||||||
|
Message: message,
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response Example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"id": "user-123",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com"
|
||||||
|
},
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response Example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1002,
|
||||||
|
"data": null,
|
||||||
|
"msg": "Invalid or expired token",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**List Response Example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{"id": "1", "name": "Item 1"},
|
||||||
|
{"id": "2", "name": "Item 2"}
|
||||||
|
],
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Error Model
|
||||||
|
|
||||||
|
### Error Code Constants
|
||||||
|
|
||||||
|
**File**: `pkg/errors/codes.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Application error codes
|
||||||
|
const (
|
||||||
|
CodeSuccess = 0 // Success
|
||||||
|
CodeInternalError = 1000 // Internal server error
|
||||||
|
CodeMissingToken = 1001 // Missing authentication token
|
||||||
|
CodeInvalidToken = 1002 // Invalid or expired token
|
||||||
|
CodeTooManyRequests = 1003 // Too many requests (rate limited)
|
||||||
|
CodeAuthServiceUnavailable = 1004 // Authentication service unavailable (Redis down)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error messages (bilingual)
|
||||||
|
var errorMessages = map[int]struct {
|
||||||
|
EN string
|
||||||
|
ZH string
|
||||||
|
}{
|
||||||
|
CodeSuccess: {"Success", "成功"},
|
||||||
|
CodeInternalError: {"Internal server error", "内部服务器错误"},
|
||||||
|
CodeMissingToken: {"Missing authentication token", "缺失认证令牌"},
|
||||||
|
CodeInvalidToken: {"Invalid or expired token", "令牌无效或已过期"},
|
||||||
|
CodeTooManyRequests: {"Too many requests", "请求过于频繁"},
|
||||||
|
CodeAuthServiceUnavailable: {"Authentication service unavailable", "认证服务不可用"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage returns error message for given code and language
|
||||||
|
func GetMessage(code int, lang string) string {
|
||||||
|
msg, ok := errorMessages[code]
|
||||||
|
if !ok {
|
||||||
|
return "Unknown error"
|
||||||
|
}
|
||||||
|
if lang == "zh" {
|
||||||
|
return msg.ZH
|
||||||
|
}
|
||||||
|
return msg.EN
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Error Types
|
||||||
|
|
||||||
|
**File**: `pkg/errors/errors.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Standard error types for middleware
|
||||||
|
var (
|
||||||
|
ErrMissingToken = errors.New("missing authentication token")
|
||||||
|
ErrInvalidToken = errors.New("invalid or expired token")
|
||||||
|
ErrRedisUnavailable = errors.New("redis unavailable")
|
||||||
|
ErrTooManyRequests = errors.New("too many requests")
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppError represents an application error with code
|
||||||
|
type AppError struct {
|
||||||
|
Code int // Application error code
|
||||||
|
Message string // Error message
|
||||||
|
Err error // Underlying error (optional)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Error() string {
|
||||||
|
if e.Err != nil {
|
||||||
|
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new AppError
|
||||||
|
func New(code int, message string) *AppError {
|
||||||
|
return &AppError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap wraps an existing error with code and message
|
||||||
|
func Wrap(code int, message string, err error) *AppError {
|
||||||
|
return &AppError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Rate Limit State Model
|
||||||
|
|
||||||
|
### Rate Limit Tracking
|
||||||
|
|
||||||
|
**Storage**: In-memory (default) or Redis (for distributed)
|
||||||
|
**Managed by**: Fiber limiter middleware (internal storage)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Rate limit state is managed internally by Fiber limiter
|
||||||
|
// For memory storage: map[string]*limiterEntry
|
||||||
|
// For Redis storage: Redis keys with TTL
|
||||||
|
|
||||||
|
// Memory storage structure (internal to Fiber)
|
||||||
|
type limiterEntry struct {
|
||||||
|
count int // Current request count
|
||||||
|
expiration time.Time // Window expiration time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis storage structure (if using Redis storage)
|
||||||
|
// Key: "ratelimit:{ip_address}"
|
||||||
|
// Value: JSON {"count": 5, "expiration": "2025-11-10T15:31:00Z"}
|
||||||
|
// TTL: Same as expiration window
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Generation** (for custom Redis storage):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// pkg/constants/redis.go
|
||||||
|
func RedisRateLimitKey(ip string) string {
|
||||||
|
return fmt.Sprintf("ratelimit:%s", ip)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access Pattern**:
|
||||||
|
- Middleware checks rate limit state before handler
|
||||||
|
- Increment counter on each request
|
||||||
|
- Reset counter when window expires
|
||||||
|
- Return 429 when limit exceeded
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Relationship Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Configuration │ (YAML file)
|
||||||
|
│ - Server │
|
||||||
|
│ - Redis │
|
||||||
|
│ - Logging │
|
||||||
|
│ - Middleware │
|
||||||
|
└────────┬────────┘
|
||||||
|
│ loads into
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌──────────────────┐
|
||||||
|
│ Application │────▶│ Redis Client │
|
||||||
|
│ (main.go) │ │ (connection │
|
||||||
|
└────────┬────────┘ │ pool) │
|
||||||
|
│ └──────────┬───────┘
|
||||||
|
│ creates │
|
||||||
|
▼ │ validates tokens
|
||||||
|
┌─────────────────┐ │
|
||||||
|
│ Middleware │ │
|
||||||
|
│ Chain: │ │
|
||||||
|
│ - Recover │ │
|
||||||
|
│ - RequestID │ │
|
||||||
|
│ - Logger │ │
|
||||||
|
│ - KeyAuth │◀───────────────┘
|
||||||
|
│ - RateLimiter │
|
||||||
|
└────────┬────────┘
|
||||||
|
│ processes
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌──────────────────┐
|
||||||
|
│ Handlers │────▶│ Response │
|
||||||
|
│ (API │ │ {code, data, │
|
||||||
|
│ endpoints) │ │ msg} │
|
||||||
|
└─────────────────┘ └──────────────────┘
|
||||||
|
│
|
||||||
|
│ logs to
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Log Files │
|
||||||
|
│ - app.log │ (Zap + Lumberjack)
|
||||||
|
│ - access.log │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow Diagram
|
||||||
|
|
||||||
|
### Request Processing Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. HTTP Request arrives
|
||||||
|
↓
|
||||||
|
2. Recover Middleware (catch panics)
|
||||||
|
↓
|
||||||
|
3. RequestID Middleware (generate UUID v4)
|
||||||
|
→ Store in c.Locals("requestid")
|
||||||
|
↓
|
||||||
|
4. Logger Middleware (start)
|
||||||
|
→ Store start_time in c.Locals("start_time")
|
||||||
|
↓
|
||||||
|
5. KeyAuth Middleware
|
||||||
|
→ Extract token from header
|
||||||
|
→ Call TokenValidator.Validate(token)
|
||||||
|
→ Validate with Redis: GET auth:token:{token}
|
||||||
|
→ If valid: Store user_id in c.Locals("user_id")
|
||||||
|
→ If invalid: Return 401 with error code
|
||||||
|
→ If Redis down: Return 503 with error code
|
||||||
|
↓
|
||||||
|
6. [RateLimiter Middleware] (if enabled)
|
||||||
|
→ Check rate limit for c.IP()
|
||||||
|
→ If exceeded: Return 429 with error code
|
||||||
|
↓
|
||||||
|
7. Handler (business logic)
|
||||||
|
→ Access c.Locals("requestid"), c.Locals("user_id")
|
||||||
|
→ Process request
|
||||||
|
→ Return response via response.Success() or response.Error()
|
||||||
|
↓
|
||||||
|
8. Logger Middleware (end)
|
||||||
|
→ Calculate duration
|
||||||
|
→ Log to access.log with all context
|
||||||
|
↓
|
||||||
|
9. HTTP Response sent
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Key Entities**:
|
||||||
|
1. **Config**: Application configuration (YAML → struct)
|
||||||
|
2. **AuthToken**: Redis key-value (token → user ID)
|
||||||
|
3. **RequestContext**: Fiber Locals (requestid, user_id, start_time)
|
||||||
|
4. **LogEntry**: JSON logs (app.log, access.log)
|
||||||
|
5. **Response**: Unified API response ({code, data, msg})
|
||||||
|
6. **Error**: Error codes and custom types
|
||||||
|
7. **RateLimitState**: Managed by Fiber limiter (memory or Redis)
|
||||||
|
|
||||||
|
**Design Principles**:
|
||||||
|
- ✅ Simple, flat structures (no deep nesting)
|
||||||
|
- ✅ Direct field access (no getters/setters)
|
||||||
|
- ✅ Composition over inheritance
|
||||||
|
- ✅ Explicit error handling
|
||||||
|
- ✅ Go naming conventions (URL, ID, HTTP)
|
||||||
|
- ✅ No Java-style patterns (no I-prefix, no Impl-suffix)
|
||||||
|
|
||||||
|
**Next**: Generate API contracts (OpenAPI specification)
|
||||||
382
specs/001-fiber-middleware-integration/plan.md
Normal file
382
specs/001-fiber-middleware-integration/plan.md
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
# Implementation Plan: Fiber Middleware Integration with Configuration Management
|
||||||
|
|
||||||
|
**Branch**: `001-fiber-middleware-integration` | **Date**: 2025-11-10 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/001-fiber-middleware-integration/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Integrate essential Fiber middleware (logger, recover, requestid, keyauth, limiter) with unified response structure, Viper configuration hot reload, and Zap+Lumberjack logging. This establishes the foundational middleware layer for the 君鸿卡管系统, ensuring proper authentication, logging, error recovery, and request tracing capabilities following Go idiomatic patterns.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: Go 1.25.1
|
||||||
|
**Primary Dependencies**:
|
||||||
|
- Fiber v2.52.9 (HTTP framework)
|
||||||
|
- Sonic v1.14.2 (JSON encoding/decoding - already integrated)
|
||||||
|
- Viper (configuration with hot reload)
|
||||||
|
- Zap (structured logging)
|
||||||
|
- Lumberjack.v2 (log rotation)
|
||||||
|
- Redis client (for keyauth token validation)
|
||||||
|
|
||||||
|
**Storage**:
|
||||||
|
- Redis (for authentication token validation - token as key, user ID as value with TTL)
|
||||||
|
- Log files (app.log for application logs, access.log for HTTP access logs)
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Go standard testing framework (`testing` package)
|
||||||
|
- Table-driven tests for middleware logic
|
||||||
|
- Integration tests for API endpoints with middleware
|
||||||
|
- Mock Redis for keyauth testing
|
||||||
|
|
||||||
|
**Target Platform**: Linux server (production), macOS (development)
|
||||||
|
|
||||||
|
**Project Type**: Single backend web service (Go web application)
|
||||||
|
|
||||||
|
**Performance Goals**:
|
||||||
|
- API response time P95 < 200ms, P99 < 500ms
|
||||||
|
- Middleware overhead < 5ms per request
|
||||||
|
- Configuration hot reload detection within 5 seconds
|
||||||
|
- Log rotation without blocking requests
|
||||||
|
|
||||||
|
**Constraints**:
|
||||||
|
- No external authentication service (Redis-only token validation)
|
||||||
|
- Fail-closed authentication (Redis unavailable = HTTP 503)
|
||||||
|
- Zero downtime for configuration changes (hot reload)
|
||||||
|
- Separate log files with independent retention policies
|
||||||
|
|
||||||
|
**Scale/Scope**:
|
||||||
|
- Foundation for multi-module card management system
|
||||||
|
- ~10 initial API endpoints (will grow)
|
||||||
|
- Expected 1000+ req/s capacity
|
||||||
|
- 24/7 production availability requirement
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
**Tech Stack Adherence**:
|
||||||
|
- [x] Feature uses Fiber + Viper + Zap + Lumberjack.v2 + sonic JSON + Redis
|
||||||
|
- [x] No native calls bypass framework (no `net/http` direct use)
|
||||||
|
- [x] All HTTP operations use Fiber framework
|
||||||
|
- [x] All async tasks use Asynq (N/A for this feature - no async tasks)
|
||||||
|
- [x] Uses Go official toolchain: `go fmt`, `go vet`, `golangci-lint`
|
||||||
|
- [x] Uses Go Modules for dependency management (already initialized)
|
||||||
|
|
||||||
|
**Code Quality Standards**:
|
||||||
|
- [x] Follows Handler → Service → Store → Model architecture (keyauth validation follows this)
|
||||||
|
- [x] Handler layer only handles HTTP, no business logic
|
||||||
|
- [x] Service layer contains business logic with cross-module support
|
||||||
|
- [x] Store layer manages all data access with transaction support
|
||||||
|
- [x] Uses dependency injection via struct fields (not constructor patterns)
|
||||||
|
- [x] Unified error codes in `pkg/errors/` (will define auth error codes: 1001-1004)
|
||||||
|
- [x] Unified API responses via `pkg/response/` (core requirement: {code, data, msg})
|
||||||
|
- [x] All constants defined in `pkg/constants/` (Redis keys, error messages)
|
||||||
|
- [x] All Redis keys managed via key generation functions (no hardcoded strings)
|
||||||
|
- [x] All exported functions/types have Go-style doc comments
|
||||||
|
- [x] Code formatted with `gofmt`
|
||||||
|
- [x] Follows Effective Go and Go Code Review Comments
|
||||||
|
|
||||||
|
**Go Idiomatic Design**:
|
||||||
|
- [x] Package structure is flat (max 2-3 levels), organized by feature
|
||||||
|
- Structure: `pkg/config/`, `pkg/logger/`, `pkg/response/`, `pkg/errors/`, `pkg/constants/`
|
||||||
|
- Middleware: `internal/middleware/` (flat, no deep nesting)
|
||||||
|
- [x] Interfaces are small (1-3 methods), defined at use site
|
||||||
|
- TokenValidator interface (2 methods: ValidateToken, IsAvailable)
|
||||||
|
- [x] No Java-style patterns: no I-prefix, no Impl-suffix, no getters/setters
|
||||||
|
- [x] Error handling is explicit (return errors, no panic/recover abuse)
|
||||||
|
- Recover middleware captures panics only, normal errors use error returns
|
||||||
|
- [x] Uses composition over inheritance
|
||||||
|
- [x] Uses goroutines and channels for config hot reload watcher
|
||||||
|
- [x] Uses `context.Context` for cancellation and timeouts
|
||||||
|
- [x] Naming follows Go conventions: short receivers, consistent abbreviations (URL, ID, HTTP)
|
||||||
|
- [x] No Hungarian notation or type prefixes
|
||||||
|
- [x] Simple constructors (New/NewXxx), no Builder pattern unless necessary
|
||||||
|
|
||||||
|
**Testing Standards**:
|
||||||
|
- [x] Unit tests for all core business logic (token validation, config loading)
|
||||||
|
- [x] Integration tests for all API endpoints with middleware chain
|
||||||
|
- [x] Tests use Go standard testing framework
|
||||||
|
- [x] Test files named `*_test.go` in same directory
|
||||||
|
- [x] Test functions use `Test` prefix, benchmarks use `Benchmark` prefix
|
||||||
|
- [x] Table-driven tests for multiple test cases (especially middleware scenarios)
|
||||||
|
- [x] Test helpers marked with `t.Helper()`
|
||||||
|
- [x] Tests are independent (mock Redis, no external dependencies)
|
||||||
|
- [x] Target coverage: 70%+ overall, 90%+ for core business (auth, config)
|
||||||
|
|
||||||
|
**User Experience Consistency**:
|
||||||
|
- [x] All APIs use unified JSON response format: `{code, data, msg}`
|
||||||
|
- [x] Error responses include clear error codes and bilingual messages
|
||||||
|
- 1001: Missing authentication token (缺失认证令牌)
|
||||||
|
- 1002: Invalid or expired token (令牌无效或已过期)
|
||||||
|
- 1003: Too many requests (请求过于频繁)
|
||||||
|
- 1004: Authentication service unavailable (认证服务不可用)
|
||||||
|
- [x] RESTful design principles followed
|
||||||
|
- [x] Unified pagination parameters (N/A for this feature)
|
||||||
|
- [x] Time fields use ISO 8601 format (RFC3339) - in log timestamps
|
||||||
|
- [x] Currency amounts use integers (N/A for this feature)
|
||||||
|
|
||||||
|
**Performance Requirements**:
|
||||||
|
- [x] API response time (P95) < 200ms, (P99) < 500ms
|
||||||
|
- Middleware overhead budgeted at < 5ms per request
|
||||||
|
- [x] Batch operations use bulk queries/inserts (N/A for this feature)
|
||||||
|
- [x] All database queries have appropriate indexes (N/A - Redis only)
|
||||||
|
- [x] List queries implement pagination (N/A for this feature)
|
||||||
|
- [x] Non-realtime operations use async tasks (N/A - all operations are realtime)
|
||||||
|
- [x] Database and Redis connection pools properly configured
|
||||||
|
- Redis: PoolSize=10, MinIdleConns=5
|
||||||
|
- [x] Uses goroutines/channels for concurrency (config watcher)
|
||||||
|
- [x] Uses `context.Context` for timeout control (Redis operations)
|
||||||
|
- [x] Uses `sync.Pool` for frequently allocated objects (if needed for performance)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/001-fiber-middleware-integration/
|
||||||
|
├── plan.md # This file (/speckit.plan command output)
|
||||||
|
├── research.md # Phase 0 output (/speckit.plan command)
|
||||||
|
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||||
|
│ └── api.yaml # OpenAPI spec for unified response format
|
||||||
|
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
junhong_cmp_fiber/
|
||||||
|
├── cmd/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ └── main.go # HTTP server entry point (updated)
|
||||||
|
│ └── worker/
|
||||||
|
│ └── main.go # Worker process (no changes for this feature)
|
||||||
|
│
|
||||||
|
├── internal/
|
||||||
|
│ └── middleware/ # Fiber middleware implementations
|
||||||
|
│ ├── auth.go # keyauth middleware integration
|
||||||
|
│ ├── ratelimit.go # limiter middleware integration (commented)
|
||||||
|
│ └── recover.go # Custom recover middleware with Zap logging
|
||||||
|
│
|
||||||
|
├── pkg/
|
||||||
|
│ ├── config/
|
||||||
|
│ │ ├── config.go # Viper configuration management
|
||||||
|
│ │ ├── loader.go # Config loading and validation
|
||||||
|
│ │ └── watcher.go # Hot reload implementation
|
||||||
|
│ │
|
||||||
|
│ ├── logger/
|
||||||
|
│ │ ├── logger.go # Zap logger initialization
|
||||||
|
│ │ ├── rotation.go # Lumberjack integration
|
||||||
|
│ │ └── middleware.go # Fiber logger middleware adapter
|
||||||
|
│ │
|
||||||
|
│ ├── response/
|
||||||
|
│ │ ├── response.go # Unified response structure and helpers
|
||||||
|
│ │ └── codes.go # Response code constants
|
||||||
|
│ │
|
||||||
|
│ ├── errors/
|
||||||
|
│ │ ├── errors.go # Custom error types
|
||||||
|
│ │ └── codes.go # Error code constants (1001-1004)
|
||||||
|
│ │
|
||||||
|
│ ├── constants/
|
||||||
|
│ │ ├── constants.go # Business constants
|
||||||
|
│ │ └── redis.go # Redis key generation functions
|
||||||
|
│ │
|
||||||
|
│ └── validator/
|
||||||
|
│ └── token.go # Token validation service (Redis interaction)
|
||||||
|
│
|
||||||
|
├── configs/
|
||||||
|
│ ├── config.yaml # Default configuration
|
||||||
|
│ ├── config.dev.yaml # Development environment
|
||||||
|
│ ├── config.staging.yaml # Staging environment
|
||||||
|
│ └── config.prod.yaml # Production environment
|
||||||
|
│
|
||||||
|
├── logs/ # Log output directory (gitignored)
|
||||||
|
│ ├── app.log # Application logs
|
||||||
|
│ └── access.log # HTTP access logs
|
||||||
|
│
|
||||||
|
└── tests/
|
||||||
|
├── integration/
|
||||||
|
│ ├── middleware_test.go # Integration tests for middleware chain
|
||||||
|
│ └── auth_test.go # Auth flow integration tests
|
||||||
|
└── unit/
|
||||||
|
├── config_test.go # Config loading and validation tests
|
||||||
|
├── logger_test.go # Logger tests
|
||||||
|
├── response_test.go # Response format tests
|
||||||
|
└── validator_test.go # Token validation tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Single project structure (Option 1) as this is a backend-only Go web service. The project follows Go's standard layout with `cmd/` for entry points, `internal/` for private application code, and `pkg/` for reusable packages. Configuration files are centralized in `configs/` with environment-specific overrides. This structure aligns with Go best practices and the constitution's flat package organization principle.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||||
|
|
||||||
|
*No violations detected. All requirements align with constitution principles.*
|
||||||
|
|
||||||
|
## Phase 0: Research & Discovery
|
||||||
|
|
||||||
|
**Status**: Starting research phase
|
||||||
|
|
||||||
|
**Research Tasks**:
|
||||||
|
1. Viper configuration hot reload best practices and implementation patterns
|
||||||
|
2. Zap + Lumberjack integration patterns for dual log files (app.log, access.log)
|
||||||
|
3. Fiber middleware execution order and error handling patterns
|
||||||
|
4. Fiber keyauth middleware customization for Redis token validation
|
||||||
|
5. Fiber limiter middleware configuration options and IP extraction
|
||||||
|
6. Redis client selection and connection pool configuration for Go
|
||||||
|
7. UUID v4 generation in Go (standard library vs third-party)
|
||||||
|
8. Graceful shutdown patterns for config watchers and HTTP server
|
||||||
|
|
||||||
|
**Unknowns to Resolve**:
|
||||||
|
- Best Redis client library for Go (go-redis vs redigo)
|
||||||
|
- Optimal Viper hot reload implementation (polling vs fsnotify)
|
||||||
|
- How to properly separate Fiber logger middleware output to access.log
|
||||||
|
- How to inject custom Zap logger into Fiber recover middleware
|
||||||
|
- Request ID propagation through Fiber context
|
||||||
|
- Testing strategies for middleware integration tests
|
||||||
|
|
||||||
|
**Output**: `research.md` with decisions and rationale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Design & Contracts
|
||||||
|
|
||||||
|
**Prerequisites**: Phase 0 research complete
|
||||||
|
|
||||||
|
**Design Artifacts**:
|
||||||
|
1. **data-model.md**: Entity definitions
|
||||||
|
- Configuration structure (server, redis, logging, middleware settings)
|
||||||
|
- AuthToken entity (Redis key-value structure)
|
||||||
|
- Request Context structure (request ID, user ID, metadata)
|
||||||
|
- Log Entry structure (JSON fields for app.log and access.log)
|
||||||
|
- Rate Limit State (IP-based tracking structure)
|
||||||
|
|
||||||
|
2. **contracts/api.yaml**: OpenAPI specification
|
||||||
|
- Unified response format schema
|
||||||
|
- Error response schemas (1001-1004)
|
||||||
|
- Common headers (X-Request-ID, token)
|
||||||
|
- Example endpoints demonstrating middleware integration
|
||||||
|
|
||||||
|
3. **quickstart.md**: Developer setup guide
|
||||||
|
- Environment setup (Go, Redis)
|
||||||
|
- Configuration file setup (config.yaml)
|
||||||
|
- Running the server
|
||||||
|
- Testing middleware (curl examples)
|
||||||
|
- Enabling/disabling rate limiter
|
||||||
|
|
||||||
|
**Output**: data-model.md, contracts/api.yaml, quickstart.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Task Generation
|
||||||
|
|
||||||
|
**Status**: To be completed by `/speckit.tasks` command (NOT part of `/speckit.plan`)
|
||||||
|
|
||||||
|
This phase will generate `tasks.md` with implementation steps ordered by dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes & Decisions
|
||||||
|
|
||||||
|
### Key Design Decisions (from spec clarifications):
|
||||||
|
|
||||||
|
1. **Log Separation**: Application logs (app.log) and HTTP access logs (access.log) in separate files with independent rotation and retention policies
|
||||||
|
2. **Token Storage**: Simple Redis key-value (token → user ID) with Redis TTL for expiration
|
||||||
|
3. **Auth Failure Mode**: Fail closed when Redis unavailable (HTTP 503, no fallback)
|
||||||
|
4. **Rate Limiting**: Per-IP address with configurable requests/time window, disabled by default
|
||||||
|
5. **Request ID Format**: UUID v4 for distributed tracing compatibility
|
||||||
|
6. **Config Hot Reload**: 5-second detection window, atomic updates, invalid config = keep previous
|
||||||
|
|
||||||
|
### Technical Choices (to be validated in Phase 0):
|
||||||
|
|
||||||
|
- Use `github.com/go-redis/redis/v8` for Redis client (widely adopted, good performance)
|
||||||
|
- Use `github.com/fsnotify/fsnotify` (Viper's native watcher) for hot reload
|
||||||
|
- Use `github.com/google/uuid` (already in go.mod) for UUID v4 generation
|
||||||
|
- Separate Zap logger instances for app.log and access.log
|
||||||
|
- Middleware order: recover → requestid → logger → keyauth → limiter → handler
|
||||||
|
|
||||||
|
### Risk Mitigation:
|
||||||
|
|
||||||
|
- **Risk**: Configuration hot reload causes in-flight request issues
|
||||||
|
- **Mitigation**: Use atomic pointer swap for config updates, don't affect in-flight requests
|
||||||
|
|
||||||
|
- **Risk**: Log rotation blocks request processing
|
||||||
|
- **Mitigation**: Lumberjack handles rotation atomically, Zap is non-blocking
|
||||||
|
|
||||||
|
- **Risk**: Redis connection pool exhaustion under load
|
||||||
|
- **Mitigation**: Proper pool sizing (10 connections), timeout configuration, circuit breaker consideration
|
||||||
|
|
||||||
|
- **Risk**: Rate limiter memory leak with many unique IPs
|
||||||
|
- **Mitigation**: Use Fiber's built-in limiter with storage backend (memory or Redis), TTL cleanup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 Constitution Re-Check
|
||||||
|
|
||||||
|
**Status**: ✅ PASSED - All design artifacts comply with constitution
|
||||||
|
|
||||||
|
**Verification Results**:
|
||||||
|
|
||||||
|
1. **Go Idiomatic Design** ✅
|
||||||
|
- ✅ Flat package structure: `pkg/config/`, `pkg/logger/`, `pkg/response/`, `pkg/errors/`
|
||||||
|
- ✅ Small interfaces: TokenValidator (2 methods only)
|
||||||
|
- ✅ No Java patterns: No IService, no Impl suffix, no getters/setters
|
||||||
|
- ✅ Simple structs with direct field access (Config, Response, LogEntry)
|
||||||
|
- ✅ Explicit error handling with custom error types
|
||||||
|
- ✅ Composition: Config composed of sub-configs, no inheritance
|
||||||
|
|
||||||
|
2. **Tech Stack Compliance** ✅
|
||||||
|
- ✅ Fiber for all HTTP operations (middleware, routing)
|
||||||
|
- ✅ Viper for configuration with hot reload
|
||||||
|
- ✅ Zap + Lumberjack for logging
|
||||||
|
- ✅ go-redis/redis/v8 for Redis client
|
||||||
|
- ✅ google/uuid for request ID generation
|
||||||
|
- ✅ No `net/http`, `database/sql`, or `encoding/json` direct usage
|
||||||
|
|
||||||
|
3. **Architecture Alignment** ✅
|
||||||
|
- ✅ Clear separation: Middleware → Handler → Service (TokenValidator) → Store (Redis)
|
||||||
|
- ✅ Dependency injection via struct fields
|
||||||
|
- ✅ Unified response format in `pkg/response/`
|
||||||
|
- ✅ Error codes centralized in `pkg/errors/`
|
||||||
|
- ✅ Constants and Redis key functions in `pkg/constants/`
|
||||||
|
|
||||||
|
4. **Performance Requirements** ✅
|
||||||
|
- ✅ Middleware overhead budgeted at <5ms per request
|
||||||
|
- ✅ Redis connection pool configured (10 connections, 5 idle)
|
||||||
|
- ✅ Goroutines for config watcher (non-blocking)
|
||||||
|
- ✅ Context timeouts for Redis operations (50ms)
|
||||||
|
- ✅ Lumberjack non-blocking log rotation
|
||||||
|
|
||||||
|
5. **Testing Strategy** ✅
|
||||||
|
- ✅ Table-driven tests planned (Go idiomatic)
|
||||||
|
- ✅ Mock Redis for unit tests
|
||||||
|
- ✅ Integration tests with testcontainers
|
||||||
|
- ✅ Target coverage defined (70%+ overall, 90%+ core)
|
||||||
|
|
||||||
|
**Design Quality Metrics**:
|
||||||
|
- Zero Java-style anti-patterns detected
|
||||||
|
- All error handling explicit (no panic/recover abuse)
|
||||||
|
- Configuration validation implemented
|
||||||
|
- Graceful shutdown pattern defined
|
||||||
|
- Security: Fail-closed auth, explicit timeout handling
|
||||||
|
|
||||||
|
**Conclusion**: Design is ready for implementation. All artifacts (research.md, data-model.md, contracts/api.yaml, quickstart.md) align with constitution principles. No violations or exceptions required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Plan created and constitution check passed
|
||||||
|
2. ✅ Execute Phase 0 research (research.md)
|
||||||
|
3. ✅ Execute Phase 1 design (data-model.md, contracts/, quickstart.md)
|
||||||
|
4. ✅ Update agent context with new technologies
|
||||||
|
5. ✅ Phase 1 Constitution re-check passed
|
||||||
|
6. ⏳ Run `/speckit.tasks` to generate implementation tasks
|
||||||
|
7. ⏳ Run `/speckit.implement` to execute tasks
|
||||||
|
|
||||||
|
**Command**: `/speckit.plan` complete. All design artifacts generated and validated.
|
||||||
|
|
||||||
|
**Ready for**: `/speckit.tasks` command to generate actionable implementation tasks.
|
||||||
795
specs/001-fiber-middleware-integration/quickstart.md
Normal file
795
specs/001-fiber-middleware-integration/quickstart.md
Normal file
@@ -0,0 +1,795 @@
|
|||||||
|
# Quick Start Guide: Fiber Middleware Integration
|
||||||
|
|
||||||
|
**Feature**: 001-fiber-middleware-integration
|
||||||
|
**Date**: 2025-11-10
|
||||||
|
**Phase**: 1 - Design & Contracts
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide helps developers set up and test the Fiber middleware integration locally. It covers environment setup, configuration, running the server, and testing middleware functionality.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required Software
|
||||||
|
|
||||||
|
- **Go**: 1.25.1 or higher
|
||||||
|
- **Redis**: 7.x or higher (for authentication)
|
||||||
|
- **Git**: For version control
|
||||||
|
|
||||||
|
### Check Versions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go version # Should show go1.25.1 or higher
|
||||||
|
redis-server --version # Should show Redis 7.x
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
### 1. Install Redis (if not already installed)
|
||||||
|
|
||||||
|
**macOS (Homebrew)**:
|
||||||
|
```bash
|
||||||
|
brew install redis
|
||||||
|
brew services start redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux (Ubuntu/Debian)**:
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install redis-server
|
||||||
|
sudo systemctl start redis
|
||||||
|
sudo systemctl enable redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify Redis is running**:
|
||||||
|
```bash
|
||||||
|
redis-cli ping
|
||||||
|
# Should return: PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Clone Repository and Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/break/csxjProject/junhong_cmp_fiber
|
||||||
|
|
||||||
|
# Install Go dependencies
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Configuration File
|
||||||
|
|
||||||
|
Create `configs/config.yaml` with the following content:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":3000"
|
||||||
|
read_timeout: "10s"
|
||||||
|
write_timeout: "10s"
|
||||||
|
shutdown_timeout: "30s"
|
||||||
|
prefork: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
address: "localhost:6379"
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
pool_size: 10
|
||||||
|
min_idle_conns: 5
|
||||||
|
dial_timeout: "5s"
|
||||||
|
read_timeout: "3s"
|
||||||
|
write_timeout: "3s"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
development: false
|
||||||
|
app_log:
|
||||||
|
filename: "logs/app.log"
|
||||||
|
max_size: 100 # MB
|
||||||
|
max_backups: 30
|
||||||
|
max_age: 30 # days
|
||||||
|
compress: true
|
||||||
|
access_log:
|
||||||
|
filename: "logs/access.log"
|
||||||
|
max_size: 500 # MB
|
||||||
|
max_backups: 90
|
||||||
|
max_age: 90 # days
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
middleware:
|
||||||
|
enable_auth: true
|
||||||
|
enable_rate_limiter: false # Disabled by default
|
||||||
|
rate_limiter:
|
||||||
|
max: 100 # requests
|
||||||
|
expiration: "1m" # per minute
|
||||||
|
storage: "memory" # or "redis"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Create Logs Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p logs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run API server
|
||||||
|
go run cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output**:
|
||||||
|
```
|
||||||
|
2025-11-10T15:30:00Z INFO Server starting {"address": ":3000"}
|
||||||
|
2025-11-10T15:30:00Z INFO Redis connected {"address": "localhost:6379"}
|
||||||
|
2025-11-10T15:30:00Z INFO Config watcher started
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build binary
|
||||||
|
go build -o bin/api cmd/api/main.go
|
||||||
|
|
||||||
|
# Run binary
|
||||||
|
./bin/api
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Middleware
|
||||||
|
|
||||||
|
### 1. Health Check (No Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response**:
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
},
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": "2025-11-10T15:30:45Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Check the `X-Request-ID` header - this is a UUID v4 generated by the requestid middleware.
|
||||||
|
|
||||||
|
### 2. Missing Token (401 Error)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response**:
|
||||||
|
```
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440001
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": 1001,
|
||||||
|
"data": null,
|
||||||
|
"msg": "Missing authentication token",
|
||||||
|
"timestamp": "2025-11-10T15:30:46Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Invalid Token (401 Error)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: invalid-token-123" http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response**:
|
||||||
|
```
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440002
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": 1002,
|
||||||
|
"data": null,
|
||||||
|
"msg": "Invalid or expired token",
|
||||||
|
"timestamp": "2025-11-10T15:30:47Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Create Test Token in Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a test token to Redis (expires in 1 hour)
|
||||||
|
redis-cli SETEX "auth:token:test-token-abc123" 3600 "user-789"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify token**:
|
||||||
|
```bash
|
||||||
|
redis-cli GET "auth:token:test-token-abc123"
|
||||||
|
# Should return: "user-789"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Valid Token (200 Success)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: test-token-abc123" http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response**:
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440003
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "user-123",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": "2025-11-10T15:30:48Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Test Panic Recovery
|
||||||
|
|
||||||
|
Create a test endpoint that panics (for testing recover middleware):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: test-token-abc123" http://localhost:3000/api/v1/test-panic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected behavior**:
|
||||||
|
- Server does NOT crash
|
||||||
|
- Returns HTTP 500 with error response
|
||||||
|
- Panic is logged to `logs/app.log` with stack trace
|
||||||
|
- Subsequent requests continue to work normally
|
||||||
|
|
||||||
|
### 7. Verify Logging
|
||||||
|
|
||||||
|
**Application logs** (`logs/app.log`):
|
||||||
|
```bash
|
||||||
|
tail -f logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected log entries** (JSON format):
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:30:45Z","level":"info","message":"Server starting","address":":3000"}
|
||||||
|
{"timestamp":"2025-11-10T15:30:46Z","level":"warn","message":"Token validation failed","request_id":"550e8400-e29b-41d4-a716-446655440001","error":"missing token"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access logs** (`logs/access.log`):
|
||||||
|
```bash
|
||||||
|
tail -f logs/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected log entries** (JSON format):
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:30:46Z","level":"info","method":"GET","path":"/api/v1/users","status":401,"duration_ms":12.345,"request_id":"550e8400-e29b-41d4-a716-446655440001","ip":"127.0.0.1","user_agent":"curl/7.88.1","user_id":""}
|
||||||
|
{"timestamp":"2025-11-10T15:30:48Z","level":"info","method":"GET","path":"/api/v1/users","status":200,"duration_ms":23.456,"request_id":"550e8400-e29b-41d4-a716-446655440003","ip":"127.0.0.1","user_agent":"curl/7.88.1","user_id":"user-789"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Configuration Hot Reload
|
||||||
|
|
||||||
|
### 1. Modify Configuration While Server is Running
|
||||||
|
|
||||||
|
Edit `configs/config.yaml` and change the log level:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logging:
|
||||||
|
level: "debug" # Changed from "info"
|
||||||
|
# ... rest unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify Configuration Reloaded
|
||||||
|
|
||||||
|
**Check application logs**:
|
||||||
|
```bash
|
||||||
|
tail -f logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected log entry** (within 5 seconds):
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:31:00Z","level":"info","message":"Config file changed","file":"configs/config.yaml"}
|
||||||
|
{"timestamp":"2025-11-10T15:31:00Z","level":"info","message":"Configuration reloaded successfully"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Invalid Configuration
|
||||||
|
|
||||||
|
Edit `configs/config.yaml` with invalid YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":3000"
|
||||||
|
invalid syntax here!!!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected behavior**:
|
||||||
|
- Server continues running with previous valid configuration
|
||||||
|
- Error logged to `logs/app.log`:
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:32:00Z","level":"error","message":"Failed to reload config","error":"yaml: unmarshal error"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Fix Configuration
|
||||||
|
|
||||||
|
Restore valid configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":3000"
|
||||||
|
read_timeout: "10s"
|
||||||
|
# ... rest of valid config
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: Configuration reloads successfully (logged within 5 seconds).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Rate Limiter (Optional)
|
||||||
|
|
||||||
|
Rate limiting is **disabled by default**. To enable and test:
|
||||||
|
|
||||||
|
### 1. Enable Rate Limiter in Configuration
|
||||||
|
|
||||||
|
Edit `configs/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
middleware:
|
||||||
|
enable_auth: true
|
||||||
|
enable_rate_limiter: true # Changed to true
|
||||||
|
rate_limiter:
|
||||||
|
max: 5 # Low limit for testing
|
||||||
|
expiration: "1m" # 1 minute window
|
||||||
|
storage: "memory"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restart Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ctrl+C to stop
|
||||||
|
go run cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Rate Limiting
|
||||||
|
|
||||||
|
Make multiple requests rapidly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run 10 requests in quick succession
|
||||||
|
for i in {1..10}; do
|
||||||
|
curl -w "\nRequest $i: %{http_code}\n" \
|
||||||
|
-H "token: test-token-abc123" \
|
||||||
|
http://localhost:3000/api/v1/users
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output**:
|
||||||
|
```
|
||||||
|
Request 1: 200
|
||||||
|
Request 2: 200
|
||||||
|
Request 3: 200
|
||||||
|
Request 4: 200
|
||||||
|
Request 5: 200
|
||||||
|
Request 6: 429 # Rate limit exceeded
|
||||||
|
Request 7: 429
|
||||||
|
Request 8: 429
|
||||||
|
Request 9: 429
|
||||||
|
Request 10: 429
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rate limit response** (429):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1003,
|
||||||
|
"data": null,
|
||||||
|
"msg": "Too many requests",
|
||||||
|
"timestamp": "2025-11-10T15:35:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Wait for Window to Reset
|
||||||
|
|
||||||
|
Wait 1 minute, then try again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sleep 60
|
||||||
|
curl -H "token: test-token-abc123" http://localhost:3000/api/v1/users
|
||||||
|
# Should return 200 again
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Disable Rate Limiter
|
||||||
|
|
||||||
|
Edit `configs/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
middleware:
|
||||||
|
enable_rate_limiter: false # Back to false
|
||||||
|
```
|
||||||
|
|
||||||
|
Server will reload config automatically (no restart needed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Redis Failure (Fail-Closed Behavior)
|
||||||
|
|
||||||
|
### 1. Stop Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew services stop redis
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
sudo systemctl stop redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: test-token-abc123" http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response** (503 Service Unavailable):
|
||||||
|
```
|
||||||
|
HTTP/1.1 503 Service Unavailable
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440010
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": 1004,
|
||||||
|
"data": null,
|
||||||
|
"msg": "Authentication service unavailable",
|
||||||
|
"timestamp": "2025-11-10T15:40:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check application logs**:
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:40:00Z","level":"error","message":"Redis unavailable","request_id":"550e8400-e29b-41d4-a716-446655440010","error":"dial tcp [::1]:6379: connect: connection refused"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew services start redis
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
sudo systemctl start redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Recovery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: test-token-abc123" http://localhost:3000/api/v1/users
|
||||||
|
# Should return 200 again
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request ID Tracing
|
||||||
|
|
||||||
|
Every request has a unique UUID v4 identifier that appears in:
|
||||||
|
|
||||||
|
1. **Response header**: `X-Request-ID`
|
||||||
|
2. **Access logs**: `request_id` field
|
||||||
|
3. **Application logs**: `request_id` field (when included)
|
||||||
|
|
||||||
|
### Example Request ID Flow
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```bash
|
||||||
|
curl -i -H "token: test-token-abc123" http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response header**:
|
||||||
|
```
|
||||||
|
X-Request-ID: 550e8400-e29b-41d4-a716-446655440020
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access log** (`logs/access.log`):
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:45:00Z","level":"info","method":"GET","path":"/api/v1/users","status":200,"duration_ms":15.234,"request_id":"550e8400-e29b-41d4-a716-446655440020","ip":"127.0.0.1","user_agent":"curl/7.88.1","user_id":"user-789"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Application log** (`logs/app.log`) - if handler logs something:
|
||||||
|
```json
|
||||||
|
{"timestamp":"2025-11-10T15:45:00Z","level":"info","message":"Fetching users","request_id":"550e8400-e29b-41d4-a716-446655440020","user_id":"user-789"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Logs by Request ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search access logs
|
||||||
|
grep "550e8400-e29b-41d4-a716-446655440020" logs/access.log
|
||||||
|
|
||||||
|
# Search application logs
|
||||||
|
grep "550e8400-e29b-41d4-a716-446655440020" logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log Rotation Testing
|
||||||
|
|
||||||
|
### Verify Log Rotation Settings
|
||||||
|
|
||||||
|
**Application log rotation** (100MB max, 30 day retention):
|
||||||
|
```bash
|
||||||
|
ls -lh logs/app.log*
|
||||||
|
# Should show app.log and rotated files (app.log.1, app.log.2, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access log rotation** (500MB max, 90 day retention):
|
||||||
|
```bash
|
||||||
|
ls -lh logs/access.log*
|
||||||
|
# Should show access.log and rotated files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger Log Rotation (Manual Test)
|
||||||
|
|
||||||
|
Generate large number of log entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate 1000 requests (will create logs)
|
||||||
|
for i in {1..1000}; do
|
||||||
|
curl -s -H "token: test-token-abc123" http://localhost:3000/api/v1/users > /dev/null
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check log file sizes**:
|
||||||
|
```bash
|
||||||
|
du -h logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Rotation happens automatically when size limit is reached. Old files are compressed (`.gz`) if compression is enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: Server won't start
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Port 3000 is not already in use: `lsof -i :3000`
|
||||||
|
2. Configuration file is valid YAML: `cat configs/config.yaml`
|
||||||
|
3. Logs directory exists: `ls -ld logs/`
|
||||||
|
|
||||||
|
### Problem: Redis connection fails
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Redis is running: `redis-cli ping`
|
||||||
|
2. Redis address in config is correct: `localhost:6379`
|
||||||
|
3. Redis authentication (if password is set)
|
||||||
|
|
||||||
|
### Problem: Token validation always fails
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Token exists in Redis: `redis-cli GET "auth:token:your-token"`
|
||||||
|
2. Token hasn't expired (check TTL): `redis-cli TTL "auth:token:your-token"`
|
||||||
|
3. Token key format is correct: `auth:token:{token_string}`
|
||||||
|
|
||||||
|
### Problem: Logs not appearing
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Log directory has write permissions: `ls -ld logs/`
|
||||||
|
2. Log level in config: `info` or `debug`
|
||||||
|
3. Logger is properly initialized (check server startup logs)
|
||||||
|
|
||||||
|
### Problem: Configuration hot reload not working
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Configuration file path is correct
|
||||||
|
2. File system notifications are working (fsnotify)
|
||||||
|
3. Server logs show config change events
|
||||||
|
4. New configuration is valid (invalid config is rejected)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### 1. Code Changes
|
||||||
|
|
||||||
|
Make changes to middleware or configuration code.
|
||||||
|
|
||||||
|
### 2. Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
go test -v ./internal/middleware -run TestKeyAuth
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Format Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all code
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
# Check formatting
|
||||||
|
gofmt -l .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Static Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run go vet
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
# Run golangci-lint (if installed)
|
||||||
|
golangci-lint run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Build and Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
go build -o bin/api cmd/api/main.go
|
||||||
|
|
||||||
|
# Test binary
|
||||||
|
./bin/api
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment-Specific Configurations
|
||||||
|
|
||||||
|
### Development (`configs/config.dev.yaml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logging:
|
||||||
|
level: "debug" # More verbose logging
|
||||||
|
development: true # Pretty-printed logs (non-JSON)
|
||||||
|
|
||||||
|
middleware:
|
||||||
|
enable_auth: false # Optional: disable auth for easier testing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```bash
|
||||||
|
export CONFIG_ENV=dev
|
||||||
|
go run cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging (`configs/config.staging.yaml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":8080"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
address: "redis-staging.example.com:6379"
|
||||||
|
password: "staging-password"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
|
||||||
|
middleware:
|
||||||
|
enable_rate_limiter: true
|
||||||
|
rate_limiter:
|
||||||
|
max: 1000
|
||||||
|
expiration: "1m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production (`configs/config.prod.yaml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
address: ":8080"
|
||||||
|
prefork: true # Multi-process mode for performance
|
||||||
|
|
||||||
|
redis:
|
||||||
|
address: "redis-prod.example.com:6379"
|
||||||
|
password: "prod-password"
|
||||||
|
pool_size: 50 # Larger pool for production
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "warn" # Less verbose
|
||||||
|
development: false
|
||||||
|
|
||||||
|
middleware:
|
||||||
|
enable_rate_limiter: true
|
||||||
|
rate_limiter:
|
||||||
|
max: 5000
|
||||||
|
expiration: "1m"
|
||||||
|
storage: "redis" # Distributed rate limiting
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After verifying the middleware integration works:
|
||||||
|
|
||||||
|
1. **Run `/speckit.tasks`**: Generate implementation tasks
|
||||||
|
2. **Run `/speckit.implement`**: Execute implementation
|
||||||
|
3. **Write tests**: Unit and integration tests for all middleware
|
||||||
|
4. **Update documentation**: Add API endpoint examples
|
||||||
|
5. **Deploy to staging**: Test in staging environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Redis Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set token (expires in 1 hour)
|
||||||
|
redis-cli SETEX "auth:token:TOKEN" 3600 "USER_ID"
|
||||||
|
|
||||||
|
# Get token
|
||||||
|
redis-cli GET "auth:token:TOKEN"
|
||||||
|
|
||||||
|
# Check TTL
|
||||||
|
redis-cli TTL "auth:token:TOKEN"
|
||||||
|
|
||||||
|
# Delete token
|
||||||
|
redis-cli DEL "auth:token:TOKEN"
|
||||||
|
|
||||||
|
# List all tokens (careful in production!)
|
||||||
|
redis-cli KEYS "auth:token:*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Curl Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
|
||||||
|
# With token
|
||||||
|
curl -H "token: TOKEN" http://localhost:3000/api/v1/users
|
||||||
|
|
||||||
|
# POST request
|
||||||
|
curl -X POST \
|
||||||
|
-H "token: TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"John","email":"john@example.com"}' \
|
||||||
|
http://localhost:3000/api/v1/users
|
||||||
|
|
||||||
|
# Show response headers
|
||||||
|
curl -i -H "token: TOKEN" http://localhost:3000/api/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Tailing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tail application logs
|
||||||
|
tail -f logs/app.log | jq .
|
||||||
|
|
||||||
|
# Tail access logs
|
||||||
|
tail -f logs/access.log | jq .
|
||||||
|
|
||||||
|
# Filter by request ID
|
||||||
|
tail -f logs/app.log | grep "REQUEST_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Ready for implementation
|
||||||
|
|
||||||
|
**Next**: Run `/speckit.tasks` to generate implementation task list
|
||||||
679
specs/001-fiber-middleware-integration/research.md
Normal file
679
specs/001-fiber-middleware-integration/research.md
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
# Research: Fiber Middleware Integration
|
||||||
|
|
||||||
|
**Feature**: 001-fiber-middleware-integration
|
||||||
|
**Date**: 2025-11-10
|
||||||
|
**Phase**: 0 - Research & Discovery
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document resolves technical unknowns and establishes best practices for integrating Fiber middleware with Viper configuration, Zap logging, and Redis authentication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Viper Configuration Hot Reload
|
||||||
|
|
||||||
|
### Decision: Use fsnotify (Viper Native Watcher)
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Viper has built-in `WatchConfig()` method using fsnotify
|
||||||
|
- No polling overhead, event-driven file change detection
|
||||||
|
- Cross-platform support (Linux, macOS, Windows)
|
||||||
|
- Battle-tested in production environments
|
||||||
|
- Integrates seamlessly with Viper's config merge logic
|
||||||
|
|
||||||
|
**Implementation Pattern**:
|
||||||
|
```go
|
||||||
|
viper.WatchConfig()
|
||||||
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
|
log.Info("Config file changed", zap.String("file", e.Name))
|
||||||
|
|
||||||
|
// Reload config atomically
|
||||||
|
newConfig := &Config{}
|
||||||
|
if err := viper.Unmarshal(newConfig); err != nil {
|
||||||
|
log.Error("Failed to reload config", zap.Error(err))
|
||||||
|
return // Keep existing config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate new config
|
||||||
|
if err := newConfig.Validate(); err != nil {
|
||||||
|
log.Error("Invalid config", zap.Error(err))
|
||||||
|
return // Keep existing config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic swap using sync/atomic
|
||||||
|
atomic.StorePointer(&globalConfig, unsafe.Pointer(newConfig))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practices**:
|
||||||
|
- Use atomic pointer swap to avoid race conditions
|
||||||
|
- Validate configuration before applying
|
||||||
|
- Log reload events with success/failure status
|
||||||
|
- Keep existing config if new config is invalid
|
||||||
|
- Don't restart services (logger, Redis client) on every change - only update values
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- Manual polling: Higher CPU overhead, added complexity
|
||||||
|
- Signal-based reload (SIGHUP): Requires manual triggering, not automatic
|
||||||
|
- Third-party config libraries (consul, etcd): Overkill for file-based config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Zap + Lumberjack Integration for Dual Log Files
|
||||||
|
|
||||||
|
### Decision: Two Separate Zap Logger Instances
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Clean separation of concerns (app logic vs HTTP access)
|
||||||
|
- Independent rotation policies (app.log: 100MB/30days, access.log: 500MB/90days)
|
||||||
|
- Different log levels (app: debug/info/error, access: info only)
|
||||||
|
- Easier to analyze and ship to different log aggregators
|
||||||
|
- Follows Go's simplicity principle - no complex routing logic
|
||||||
|
|
||||||
|
**Implementation Pattern**:
|
||||||
|
```go
|
||||||
|
// Application logger (app.log)
|
||||||
|
appCore := zapcore.NewCore(
|
||||||
|
zapcore.NewJSONEncoder(encoderConfig),
|
||||||
|
zapcore.AddSync(&lumberjack.Logger{
|
||||||
|
Filename: "logs/app.log",
|
||||||
|
MaxSize: 100, // MB
|
||||||
|
MaxBackups: 30,
|
||||||
|
MaxAge: 30, // days
|
||||||
|
Compress: true,
|
||||||
|
}),
|
||||||
|
zap.InfoLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Access logger (access.log)
|
||||||
|
accessCore := zapcore.NewCore(
|
||||||
|
zapcore.NewJSONEncoder(encoderConfig),
|
||||||
|
zapcore.AddSync(&lumberjack.Logger{
|
||||||
|
Filename: "logs/access.log",
|
||||||
|
MaxSize: 500, // MB
|
||||||
|
MaxBackups: 90,
|
||||||
|
MaxAge: 90, // days
|
||||||
|
Compress: true,
|
||||||
|
}),
|
||||||
|
zap.InfoLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
appLogger := zap.New(appCore, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
|
||||||
|
accessLogger := zap.New(accessCore)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logger Usage**:
|
||||||
|
- **appLogger**: Business logic, errors, debug info, system events
|
||||||
|
- **accessLogger**: HTTP requests/responses only (method, path, status, duration, request ID)
|
||||||
|
|
||||||
|
**JSON Encoder Config**:
|
||||||
|
```go
|
||||||
|
encoderConfig := zapcore.EncoderConfig{
|
||||||
|
TimeKey: "timestamp",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
MessageKey: "message",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder, // RFC3339 format
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- Single logger with routing logic: Complex, error-prone, violates separation of concerns
|
||||||
|
- Log levels for separation: Doesn't solve retention/rotation policy differences
|
||||||
|
- Multiple cores in one logger: Still requires complex routing logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fiber Middleware Execution Order
|
||||||
|
|
||||||
|
### Decision: recover → requestid → logger → keyauth → limiter → handler
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
1. **recover** first: Must catch panics from all downstream middleware
|
||||||
|
2. **requestid** second: All logs need request ID, including auth failures
|
||||||
|
3. **logger** third: Log all requests including auth failures
|
||||||
|
4. **keyauth** fourth: Authentication before business logic
|
||||||
|
5. **limiter** fifth: Rate limit after auth (only count authenticated requests)
|
||||||
|
6. **handler** last: Business logic with all context available
|
||||||
|
|
||||||
|
**Fiber Middleware Registration**:
|
||||||
|
```go
|
||||||
|
app.Use(customRecover()) // Must be first
|
||||||
|
app.Use(fiber.New(fiber.Config{
|
||||||
|
Next: nil,
|
||||||
|
Generator: uuid.NewString, // UUID v4
|
||||||
|
}))
|
||||||
|
app.Use(customLogger(accessLogger))
|
||||||
|
app.Use(customKeyAuth(validator, appLogger))
|
||||||
|
// app.Use(customLimiter()) // Commented by default
|
||||||
|
app.Get("/api/v1/users", handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Insights**:
|
||||||
|
- Middleware executes in registration order (top to bottom)
|
||||||
|
- `recover` must be first to catch panics from all middleware
|
||||||
|
- `requestid` must be before logger to include ID in access logs
|
||||||
|
- Auth middleware should have access to request ID for security logs
|
||||||
|
- Rate limiter after auth = more accurate rate limiting per user/IP combo
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- Auth before logger: Can't log auth failures with full context
|
||||||
|
- Rate limit before auth: Anonymous requests consume rate limit quota
|
||||||
|
- Request ID after logger: Access logs missing correlation IDs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fiber keyauth Middleware Customization
|
||||||
|
|
||||||
|
### Decision: Wrap Fiber's keyauth with Custom Redis Validator
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Fiber's keyauth middleware provides token extraction from headers
|
||||||
|
- Custom validator function handles Redis token validation
|
||||||
|
- Clean separation: Fiber handles HTTP, validator handles business logic
|
||||||
|
- Easy to test validator independently
|
||||||
|
- Follows constitution's Handler → Service pattern
|
||||||
|
|
||||||
|
**Implementation Pattern**:
|
||||||
|
```go
|
||||||
|
// Validator service (pkg/validator/token.go)
|
||||||
|
type TokenValidator struct {
|
||||||
|
redis *redis.Client
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TokenValidator) Validate(token string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Check Redis availability
|
||||||
|
if err := v.redis.Ping(ctx).Err(); err != nil {
|
||||||
|
return "", ErrRedisUnavailable // Fail closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from token
|
||||||
|
userID, err := v.redis.Get(ctx, constants.RedisAuthTokenKey(token)).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
return "", ErrInvalidToken
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("redis get: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware wrapper (internal/middleware/auth.go)
|
||||||
|
func KeyAuth(validator *validator.TokenValidator, logger *zap.Logger) fiber.Handler {
|
||||||
|
return keyauth.New(keyauth.Config{
|
||||||
|
KeyLookup: "header:token",
|
||||||
|
Validator: func(c *fiber.Ctx, key string) (bool, error) {
|
||||||
|
userID, err := validator.Validate(key)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Token validation failed",
|
||||||
|
zap.String("request_id", c.Locals("requestid").(string)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID in context
|
||||||
|
c.Locals("user_id", userID)
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||||
|
// Map errors to unified response format
|
||||||
|
switch err {
|
||||||
|
case keyauth.ErrMissingOrMalformedAPIKey:
|
||||||
|
return response.Error(c, 401, errors.CodeMissingToken, "Missing authentication token")
|
||||||
|
case ErrInvalidToken:
|
||||||
|
return response.Error(c, 401, errors.CodeInvalidToken, "Invalid or expired token")
|
||||||
|
case ErrRedisUnavailable:
|
||||||
|
return response.Error(c, 503, errors.CodeAuthServiceUnavailable, "Authentication service unavailable")
|
||||||
|
default:
|
||||||
|
return response.Error(c, 500, errors.CodeInternalError, "Internal server error")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practices**:
|
||||||
|
- Use context timeout for Redis operations (50ms)
|
||||||
|
- Fail closed when Redis unavailable (HTTP 503)
|
||||||
|
- Store user ID in Fiber context (`c.Locals`) for downstream handlers
|
||||||
|
- Log all auth failures with request ID for security auditing
|
||||||
|
- Use custom error types for different failure modes
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- Direct Redis calls in middleware: Violates separation of concerns
|
||||||
|
- JWT tokens: Spec requires Redis validation, not stateless tokens
|
||||||
|
- Cache validation results: Security risk, defeats Redis TTL purpose
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Redis Client Selection
|
||||||
|
|
||||||
|
### Decision: go-redis/redis/v8
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Most widely adopted Redis client in Go ecosystem (19k+ stars)
|
||||||
|
- Excellent performance and connection pooling
|
||||||
|
- Native context support for timeouts and cancellation
|
||||||
|
- Supports Redis Cluster, Sentinel, and standalone
|
||||||
|
- Active maintenance and community support
|
||||||
|
- Already compatible with Go 1.18+ (uses generics)
|
||||||
|
- Comprehensive documentation and examples
|
||||||
|
|
||||||
|
**Connection Pool Configuration**:
|
||||||
|
```go
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "", // From config
|
||||||
|
DB: 0, // From config
|
||||||
|
PoolSize: 10, // Concurrent connections
|
||||||
|
MinIdleConns: 5, // Keep-alive connections
|
||||||
|
MaxRetries: 3, // Retry failed commands
|
||||||
|
DialTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
PoolTimeout: 4 * time.Second, // Wait for connection from pool
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practices**:
|
||||||
|
- Use context with timeout for all Redis operations
|
||||||
|
- Check Redis availability with `Ping()` before critical operations
|
||||||
|
- Use `Get()` for simple token validation (O(1) complexity)
|
||||||
|
- Let Redis TTL handle token expiration (no manual cleanup)
|
||||||
|
- Monitor connection pool metrics in production
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- **redigo**: Older, no context support, more manual connection management
|
||||||
|
- **rueidis**: Very fast but newer, less community adoption
|
||||||
|
- **Native Redis module**: Doesn't exist in Go standard library
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UUID v4 Generation
|
||||||
|
|
||||||
|
### Decision: google/uuid (Already in go.mod)
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Already a dependency (via Fiber's uuid import)
|
||||||
|
- Official Google implementation, well-tested
|
||||||
|
- Simple API: `uuid.New()` or `uuid.NewString()`
|
||||||
|
- RFC 4122 compliant UUID v4 (random)
|
||||||
|
- No external dependencies
|
||||||
|
- Excellent performance (~1.5M UUIDs/sec)
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```go
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
// In requestid middleware config
|
||||||
|
fiber.New(fiber.Config{
|
||||||
|
Generator: uuid.NewString, // Returns string directly
|
||||||
|
})
|
||||||
|
|
||||||
|
// Or manual generation
|
||||||
|
requestID := uuid.NewString() // "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
```
|
||||||
|
|
||||||
|
**UUID v4 Characteristics**:
|
||||||
|
- 122 random bits (collision probability ~1 in 2^122)
|
||||||
|
- No need for special collision handling
|
||||||
|
- Compatible with distributed tracing (Jaeger, OpenTelemetry)
|
||||||
|
- Human-readable in logs and headers
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- **crypto/rand + manual formatting**: Reinventing the wheel, error-prone
|
||||||
|
- **ULID**: Lexicographically sortable but not requested in spec
|
||||||
|
- **Standard library**: No UUID support in Go stdlib
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Fiber Limiter Middleware
|
||||||
|
|
||||||
|
### Decision: Fiber Built-in Limiter with Memory Storage (Commented by Default)
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Fiber's limiter middleware supports multiple storage backends
|
||||||
|
- Memory storage sufficient for single-server deployment
|
||||||
|
- Redis storage available for multi-server deployment
|
||||||
|
- Per-IP rate limiting via client IP extraction
|
||||||
|
- Sliding window or fixed window algorithms available
|
||||||
|
|
||||||
|
**Implementation Pattern (Commented)**:
|
||||||
|
```go
|
||||||
|
// Rate limiter configuration (commented by default)
|
||||||
|
// Uncomment and configure per endpoint as needed
|
||||||
|
/*
|
||||||
|
app.Use("/api/v1/", limiter.New(limiter.Config{
|
||||||
|
Max: 100, // Max requests
|
||||||
|
Expiration: 1 * time.Minute, // Time window
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
return c.IP() // Rate limit by IP
|
||||||
|
},
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return response.Error(c, 429, errors.CodeTooManyRequests, "Too many requests")
|
||||||
|
},
|
||||||
|
Storage: nil, // nil = in-memory, or redis storage for distributed
|
||||||
|
}))
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Options**:
|
||||||
|
- **Max**: Number of requests allowed in time window (e.g., 100)
|
||||||
|
- **Expiration**: Time window duration (e.g., 1 minute)
|
||||||
|
- **KeyGenerator**: Function to extract rate limit key (IP, user ID, API key)
|
||||||
|
- **Storage**: Memory (default) or Redis for distributed rate limiting
|
||||||
|
- **LimitReached**: Custom error handler returning unified response format
|
||||||
|
|
||||||
|
**Enabling Rate Limiter**:
|
||||||
|
1. Uncomment middleware registration in `main.go`
|
||||||
|
2. Configure limits per endpoint or globally
|
||||||
|
3. Choose storage backend (memory for single server, Redis for cluster)
|
||||||
|
4. Update documentation with rate limit values
|
||||||
|
5. Monitor rate limit hits in logs
|
||||||
|
|
||||||
|
**Best Practices**:
|
||||||
|
- Apply rate limits per endpoint (different limits for read vs write)
|
||||||
|
- Use Redis storage for multi-server deployments
|
||||||
|
- Log rate limit violations for abuse detection
|
||||||
|
- Return `Retry-After` header in 429 responses
|
||||||
|
- Configure different limits for authenticated vs anonymous requests
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- **Third-party rate limiter**: Added complexity, Fiber's built-in sufficient
|
||||||
|
- **Token bucket algorithm**: Fiber supports sliding window, simpler to configure
|
||||||
|
- **Rate limit before auth**: Spec requires after auth, per-IP basis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Graceful Shutdown Pattern
|
||||||
|
|
||||||
|
### Decision: Context-Based Cancellation with Shutdown Hook
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Go's context package provides clean cancellation propagation
|
||||||
|
- Fiber supports graceful shutdown with timeout
|
||||||
|
- Config watcher must stop before application exits
|
||||||
|
- Prevents goroutine leaks and incomplete operations
|
||||||
|
|
||||||
|
**Implementation Pattern**:
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
// Create root context with cancellation
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Initialize components with context
|
||||||
|
cfg := config.Load()
|
||||||
|
go config.Watch(ctx, cfg) // Pass context to watcher
|
||||||
|
|
||||||
|
app := setupApp(cfg)
|
||||||
|
|
||||||
|
// Graceful shutdown signal handling
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := app.Listen(cfg.Server.Address); err != nil {
|
||||||
|
log.Fatal("Server failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-quit // Block until signal
|
||||||
|
log.Info("Shutting down server...")
|
||||||
|
|
||||||
|
cancel() // Cancel context (stops config watcher)
|
||||||
|
|
||||||
|
if err := app.ShutdownWithTimeout(30 * time.Second); err != nil {
|
||||||
|
log.Error("Forced shutdown", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Server stopped")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Watcher Cancellation**:
|
||||||
|
```go
|
||||||
|
func Watch(ctx context.Context, cfg *Config) {
|
||||||
|
viper.WatchConfig()
|
||||||
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return // Stop processing config changes
|
||||||
|
default:
|
||||||
|
// Reload config logic
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
<-ctx.Done() // Block until cancelled
|
||||||
|
log.Info("Config watcher stopped")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practices**:
|
||||||
|
- Use `context.Context` for all long-running goroutines
|
||||||
|
- Set reasonable shutdown timeout (30 seconds)
|
||||||
|
- Close resources in defer statements
|
||||||
|
- Log shutdown progress
|
||||||
|
- Flush logs before exit (`logger.Sync()`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Testing Strategies
|
||||||
|
|
||||||
|
### Decision: Table-Driven Tests with Mock Redis
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Table-driven tests are Go idiomatic (endorsed by Go team)
|
||||||
|
- Mock Redis avoids external dependencies in unit tests
|
||||||
|
- Integration tests use testcontainers for real Redis
|
||||||
|
- Middleware testing requires Fiber test context
|
||||||
|
|
||||||
|
**Unit Test Pattern (Token Validator)**:
|
||||||
|
```go
|
||||||
|
func TestTokenValidator_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
token string
|
||||||
|
setupMock func(*mock.Redis)
|
||||||
|
wantUser string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid token",
|
||||||
|
token: "valid-token-123",
|
||||||
|
setupMock: func(m *mock.Redis) {
|
||||||
|
m.On("Get", mock.Anything, "auth:token:valid-token-123").
|
||||||
|
Return(redis.NewStringResult("user-456", nil))
|
||||||
|
},
|
||||||
|
wantUser: "user-456",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired token",
|
||||||
|
token: "expired-token",
|
||||||
|
setupMock: func(m *mock.Redis) {
|
||||||
|
m.On("Get", mock.Anything, "auth:token:expired-token").
|
||||||
|
Return(redis.NewStringResult("", redis.Nil))
|
||||||
|
},
|
||||||
|
wantUser: "",
|
||||||
|
wantErr: ErrInvalidToken,
|
||||||
|
},
|
||||||
|
// More cases...
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockRedis := &mock.Redis{}
|
||||||
|
tt.setupMock(mockRedis)
|
||||||
|
|
||||||
|
validator := NewTokenValidator(mockRedis, zap.NewNop())
|
||||||
|
userID, err := validator.Validate(tt.token)
|
||||||
|
|
||||||
|
if err != tt.wantErr {
|
||||||
|
t.Errorf("got error %v, want %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if userID != tt.wantUser {
|
||||||
|
t.Errorf("got userID %s, want %s", userID, tt.wantUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockRedis.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration Test Pattern (Middleware Chain)**:
|
||||||
|
```go
|
||||||
|
func TestMiddlewareChain(t *testing.T) {
|
||||||
|
// Start testcontainer Redis
|
||||||
|
redisContainer, err := testcontainers.GenericContainer(ctx,
|
||||||
|
testcontainers.GenericContainerRequest{
|
||||||
|
ContainerRequest: testcontainers.ContainerRequest{
|
||||||
|
Image: "redis:7-alpine",
|
||||||
|
ExposedPorts: []string{"6379/tcp"},
|
||||||
|
},
|
||||||
|
Started: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer redisContainer.Terminate(ctx)
|
||||||
|
|
||||||
|
// Setup app with middleware
|
||||||
|
app := setupTestApp(redisContainer)
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupToken func(redis *redis.Client)
|
||||||
|
headers map[string]string
|
||||||
|
expectedStatus int
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid request with token",
|
||||||
|
setupToken: func(rdb *redis.Client) {
|
||||||
|
rdb.Set(ctx, "auth:token:valid-token", "user-123", 1*time.Hour)
|
||||||
|
},
|
||||||
|
headers: map[string]string{"token": "valid-token"},
|
||||||
|
expectedStatus: 200,
|
||||||
|
expectedCode: 0,
|
||||||
|
},
|
||||||
|
// More cases...
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.setupToken != nil {
|
||||||
|
tt.setupToken(redisClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||||
|
for k, v := range tt.headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
|
||||||
|
|
||||||
|
// Parse response body and check code
|
||||||
|
var body response.Response
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
assert.Equal(t, tt.expectedCode, body.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing Best Practices**:
|
||||||
|
- Use `testing` package (no third-party test frameworks)
|
||||||
|
- Mock external dependencies (Redis) in unit tests
|
||||||
|
- Use real services in integration tests (testcontainers)
|
||||||
|
- Test helpers marked with `t.Helper()`
|
||||||
|
- Parallel tests when possible (`t.Parallel()`)
|
||||||
|
- Clear test names describing scenario
|
||||||
|
- Assert expected errors, not just success cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Middleware Error Handling
|
||||||
|
|
||||||
|
### Decision: Custom ErrorHandler for Unified Response Format
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Fiber middleware returns errors, not HTTP responses
|
||||||
|
- ErrorHandler translates errors to unified response format
|
||||||
|
- Consistent error structure across all middleware
|
||||||
|
- Proper HTTP status codes and error codes
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
```go
|
||||||
|
// In each middleware config
|
||||||
|
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||||
|
// Map error to response
|
||||||
|
code, status, msg := mapError(err)
|
||||||
|
return response.Error(c, status, code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centralized error mapping
|
||||||
|
func mapError(err error) (code, status int, msg string) {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrMissingToken):
|
||||||
|
return errors.CodeMissingToken, 401, "Missing authentication token"
|
||||||
|
case errors.Is(err, ErrInvalidToken):
|
||||||
|
return errors.CodeInvalidToken, 401, "Invalid or expired token"
|
||||||
|
case errors.Is(err, ErrRedisUnavailable):
|
||||||
|
return errors.CodeAuthServiceUnavailable, 503, "Authentication service unavailable"
|
||||||
|
case errors.Is(err, ErrTooManyRequests):
|
||||||
|
return errors.CodeTooManyRequests, 429, "Too many requests"
|
||||||
|
default:
|
||||||
|
return errors.CodeInternalError, 500, "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of Decisions
|
||||||
|
|
||||||
|
| Component | Decision | Key Rationale |
|
||||||
|
|-----------|----------|---------------|
|
||||||
|
| Config Hot Reload | Viper + fsnotify | Native support, event-driven, atomic swap |
|
||||||
|
| Logging | Dual Zap loggers + Lumberjack | Separate concerns, independent policies |
|
||||||
|
| Middleware Order | recover → requestid → logger → keyauth → limiter | Panic safety, context propagation |
|
||||||
|
| Auth Validation | Custom validator + Fiber keyauth | Separation of concerns, testability |
|
||||||
|
| Redis Client | go-redis/redis/v8 | Industry standard, excellent performance |
|
||||||
|
| Request ID | google/uuid v4 | Already in deps, RFC 4122 compliant |
|
||||||
|
| Rate Limiting | Fiber limiter (commented) | Built-in, flexible, easy to enable |
|
||||||
|
| Graceful Shutdown | Context cancellation + signal handling | Clean resource cleanup |
|
||||||
|
| Testing | Table-driven + mocks/testcontainers | Go idiomatic, balanced approach |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Readiness Checklist
|
||||||
|
|
||||||
|
- [x] All technical unknowns resolved
|
||||||
|
- [x] Best practices established for each component
|
||||||
|
- [x] Go idiomatic patterns confirmed (no Java-style anti-patterns)
|
||||||
|
- [x] Constitution compliance verified (Fiber, Zap, Viper, Redis)
|
||||||
|
- [x] Testing strategies defined
|
||||||
|
- [x] Error handling patterns established
|
||||||
|
- [x] Performance considerations addressed
|
||||||
|
- [x] Security patterns confirmed (fail-closed auth)
|
||||||
|
|
||||||
|
**Status**: Ready for Phase 1 (Design & Contracts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next**: Generate data-model.md, contracts/api.yaml, quickstart.md
|
||||||
256
specs/001-fiber-middleware-integration/spec.md
Normal file
256
specs/001-fiber-middleware-integration/spec.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Feature Specification: Fiber Middleware Integration with Configuration Management
|
||||||
|
|
||||||
|
**Feature Branch**: `001-fiber-middleware-integration`
|
||||||
|
**Created**: 2025-11-10
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "我需要你把以下东西集成到fiber中以及我们系统中,不需要你去go get,我自己会去go mod tidy
|
||||||
|
|
||||||
|
关于fiber的各种使用方法可以访问 https://docs.gofiber.io/ 来获取
|
||||||
|
|
||||||
|
同时我还需要你建立一个统一的返回结构
|
||||||
|
结构
|
||||||
|
{
|
||||||
|
\"code\": 0000,
|
||||||
|
\"data\": {}/[],
|
||||||
|
\"msg\": \"\"
|
||||||
|
}
|
||||||
|
viper配置(需要支持热加载)
|
||||||
|
zap以及Lumberjack.v2
|
||||||
|
github.com/gofiber/fiber/v2/middleware/logger中间件
|
||||||
|
github.com/gofiber/fiber/v2/middleware/recover中间件
|
||||||
|
github.com/gofiber/fiber/v2/middleware/requestid中间件
|
||||||
|
github.com/gofiber/fiber/v2/middleware/keyauth中间件(应该是去redis去验证是否存在token,应该是从header头拿名为 token的字段来做比对)
|
||||||
|
github.com/gofiber/fiber/v2/middleware/limiter中间件(可以先做进来,做完后注释掉全部的代码,然后说明怎么用怎么改)"
|
||||||
|
|
||||||
|
## Clarifications
|
||||||
|
|
||||||
|
### Session 2025-11-10
|
||||||
|
|
||||||
|
- Q: What specific types of logs should the Zap + Lumberjack integration handle? → A: Both application logs and HTTP access logs, with configurable separation into different files (app.log, access.log) to enable independent retention policies and analysis workflows.
|
||||||
|
- Q: When Redis is unavailable during token validation (FR-016), what should the authentication behavior be? → A: Fail closed: All authentication requests fail immediately when Redis is unavailable (return HTTP 503)
|
||||||
|
- Q: What data structure and content should be stored in Redis for authentication tokens? → A: Token as key only (simple existence check): Store tokens as Redis keys with user ID as value, using Redis TTL for expiration
|
||||||
|
- Q: What identifier should the rate limiter use to track and enforce request limits? → A: Per-IP address: Rate limit based on client IP address with configurable requests per time window (e.g., 100 req/min per IP)
|
||||||
|
- Q: What format should be used for generating unique request IDs in the requestid middleware? → A: UUID v4 (random): Standard UUID format for maximum compatibility with distributed tracing systems and log aggregation tools
|
||||||
|
|
||||||
|
## User Scenarios & Testing
|
||||||
|
|
||||||
|
### User Story 1 - Configuration Hot Reload (Priority: P1)
|
||||||
|
|
||||||
|
When system administrators or DevOps engineers modify application configuration files (such as server ports, database connections, log levels), the system should automatically detect and apply these changes without requiring a service restart, ensuring zero-downtime configuration updates.
|
||||||
|
|
||||||
|
**Why this priority**: Configuration management is foundational for all other features. Without proper configuration loading and hot reload capability, the system cannot support runtime adjustments, which is critical for production environments.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by modifying a configuration value in the config file and verifying the system picks up the new value within seconds without restart, delivering immediate configuration flexibility.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the system is running with initial configuration, **When** an administrator updates the log level in the config file, **Then** the system detects the change within 5 seconds and applies the new log level to all subsequent log entries
|
||||||
|
2. **Given** the system is running, **When** configuration file contains invalid syntax, **Then** the system logs a warning and continues using the previous valid configuration
|
||||||
|
3. **Given** configuration hot reload is enabled, **When** multiple configuration parameters are changed simultaneously, **Then** all changes are applied atomically without partial updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Structured Logging and Log Rotation (Priority: P1)
|
||||||
|
|
||||||
|
When the system processes requests and business operations, all events, errors, and debugging information should be recorded in structured JSON format with automatic log file rotation based on size and time, ensuring comprehensive audit trails without disk space exhaustion. The system maintains separate log files for application logs (app.log) and HTTP access logs (access.log) with independent retention policies.
|
||||||
|
|
||||||
|
**Why this priority**: Logging is essential for debugging, monitoring, and compliance. Structured logs enable efficient querying and analysis, while automatic rotation prevents operational issues. Separating application and access logs allows for different retention policies and analysis workflows.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by generating various log events and verifying they appear in structured JSON format in the appropriate files, and that log files rotate when size/time thresholds are reached, delivering production-ready logging capability.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the system is processing requests, **When** any application operation occurs, **Then** logs are written to app.log in JSON format containing timestamp, level, message, request ID, and contextual data
|
||||||
|
2. **Given** the system is processing HTTP requests, **When** requests complete, **Then** access logs are written to access.log with request method, path, status, duration, and request ID
|
||||||
|
3. **Given** a log file reaches the configured size limit, **When** new log entries are generated, **Then** the current log file is archived and a new log file is created
|
||||||
|
4. **Given** log retention is configured for 30 days for application logs and 90 days for access logs, **When** log files exceed the retention period, **Then** older log files are automatically removed according to their respective policies
|
||||||
|
5. **Given** multiple log levels are configured (debug, info, warn, error), **When** logging at different levels, **Then** only messages at or above the configured level are written
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Unified API Response Format (Priority: P1)
|
||||||
|
|
||||||
|
When API consumers (frontend applications, mobile apps, third-party integrations) make requests to any endpoint, they should receive responses in a consistent JSON structure containing status code, data payload, and message, regardless of success or failure, enabling predictable error handling and data parsing.
|
||||||
|
|
||||||
|
**Why this priority**: Consistent response format is critical for API consumers to reliably parse responses. Without this, every endpoint integration becomes custom work, increasing development time and bug potential.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by calling any endpoint (successful or failed) and verifying the response structure matches the defined format with appropriate code, data, and message fields, delivering immediate API consistency.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a valid API request, **When** the request succeeds, **Then** the response contains `{"code": 0, "data": {...}, "msg": "success"}`
|
||||||
|
2. **Given** an invalid API request, **When** validation fails, **Then** the response contains `{"code": [error_code], "data": null, "msg": "[error description]"}`
|
||||||
|
3. **Given** any API endpoint, **When** processing completes, **Then** the response structure always includes code, data, and msg fields
|
||||||
|
4. **Given** list/array data is returned, **When** the response is generated, **Then** the data field contains an array instead of an object
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 4 - Request Logging and Tracing (Priority: P2)
|
||||||
|
|
||||||
|
When HTTP requests arrive at the system, each request should be assigned a unique identifier and all request details (method, path, duration, status) should be logged, enabling request tracking across distributed components and performance analysis.
|
||||||
|
|
||||||
|
**Why this priority**: Request logging provides visibility into system usage patterns and performance. The unique request ID enables correlation of logs across services for troubleshooting.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by making multiple concurrent requests and verifying each has a unique request ID in logs and response headers, and that request metrics are captured, delivering complete request observability.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an HTTP request arrives, **When** it enters the system, **Then** a unique request ID in UUID v4 format (e.g., "550e8400-e29b-41d4-a716-446655440000") is generated and added to the request context
|
||||||
|
2. **Given** a request is being processed, **When** any logging occurs during that request, **Then** the request ID is automatically included in log entries
|
||||||
|
3. **Given** a request completes, **When** the response is sent, **Then** the request ID is included in response headers (X-Request-ID) and a summary log entry records method, path, status, and duration
|
||||||
|
4. **Given** multiple concurrent requests, **When** processed simultaneously, **Then** each request maintains its own unique UUID v4 request ID without collision
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 5 - Automatic Error Recovery (Priority: P2)
|
||||||
|
|
||||||
|
When unexpected errors or panics occur during request processing, the system should automatically recover from the failure, log detailed error information, return an appropriate error response to the client, and continue serving subsequent requests without crashing.
|
||||||
|
|
||||||
|
**Why this priority**: Error recovery prevents cascading failures and ensures service availability. A single panic should not bring down the entire application.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by triggering a controlled panic in a handler and verifying the system returns an error response, logs the panic details, and continues processing subsequent requests normally, delivering fault tolerance.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a request handler panics, **When** the panic occurs, **Then** the middleware recovers, logs the panic stack trace, and returns HTTP 500 with error details
|
||||||
|
2. **Given** a panic is recovered, **When** subsequent requests arrive, **Then** they are processed normally without any impact from the previous panic
|
||||||
|
3. **Given** a panic includes error details, **When** logged, **Then** the log entry contains the panic message, stack trace, request ID, and request details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 6 - Token-Based Authentication (Priority: P2)
|
||||||
|
|
||||||
|
When external clients make API requests, they must provide a valid authentication token in the request header, which the system validates against stored tokens in Redis cache, ensuring only authorized requests can access protected resources.
|
||||||
|
|
||||||
|
**Why this priority**: Authentication is essential for security but depends on the foundational components (config, logging, response format) being in place first.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by making requests with valid/invalid/missing tokens and verifying that valid tokens grant access while invalid ones are rejected with appropriate error codes, delivering access control capability.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a request to a protected endpoint, **When** the "token" header is missing, **Then** the system returns HTTP 401 with `{"code": 1001, "data": null, "msg": "Missing authentication token"}`
|
||||||
|
2. **Given** a request with a token, **When** the token exists as a key in Redis, **Then** the system retrieves the user ID from the value and allows the request to proceed with user context
|
||||||
|
3. **Given** a request with a token, **When** the token does not exist in Redis (either never created or TTL expired), **Then** the system returns HTTP 401 with `{"code": 1002, "data": null, "msg": "Invalid or expired token"}`
|
||||||
|
4. **Given** Redis is unavailable, **When** token validation is attempted, **Then** the system immediately fails closed, logs the Redis connection error, and returns HTTP 503 with `{"code": 1004, "data": null, "msg": "Authentication service unavailable"}` without attempting fallback mechanisms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 7 - Rate Limiting Configuration (Priority: P3)
|
||||||
|
|
||||||
|
The system should provide configurable IP-based rate limiting capabilities that can restrict the number of requests from a specific client IP address within a time window, with the functionality initially implemented but disabled by default, allowing future activation based on specific endpoint requirements.
|
||||||
|
|
||||||
|
**Why this priority**: Rate limiting is important for production but not critical for initial deployment. It can be activated later when traffic patterns are better understood.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by enabling the limiter configuration, making repeated requests from the same IP exceeding the limit, and verifying that excess requests are rejected with rate limit error messages, delivering DoS protection capability when needed.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** rate limiting is configured and enabled for an endpoint with 100 requests per minute per IP, **When** a client IP exceeds the request limit within the time window, **Then** subsequent requests from that IP return HTTP 429 with `{"code": 1003, "data": null, "msg": "Too many requests"}`
|
||||||
|
2. **Given** the rate limit time window expires, **When** new requests arrive from the same client IP, **Then** the request counter resets and requests are allowed again
|
||||||
|
3. **Given** rate limiting is disabled (default), **When** any number of requests arrive, **Then** all requests are processed without rate limit checks
|
||||||
|
4. **Given** rate limiting is enabled, **When** requests arrive from different IP addresses, **Then** each IP address has its own independent request counter and limit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when the configuration file is deleted while the system is running? (System should log error and continue with current configuration)
|
||||||
|
- What happens when Redis connection is lost during token validation? (System immediately fails closed, returns HTTP 503 with code 1004, logs connection failure, and does not attempt any fallback authentication)
|
||||||
|
- What happens when log directory is not writable? (System should fail to start with clear error message)
|
||||||
|
- What happens when a request ID collision occurs? (With UUID v4, collision probability is negligible: ~1 in 2^122; no special handling needed)
|
||||||
|
- What happens when configuration hot reload occurs during active request processing? (Configuration changes should not affect in-flight requests)
|
||||||
|
- What happens when log rotation occurs while writing a log entry? (Log rotation should be atomic and not lose log entries)
|
||||||
|
- What happens when invalid configuration values are provided (e.g., negative numbers for limits)? (System should validate config on load and reject invalid values with clear error messages)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: System MUST load configuration from files using Viper configuration library
|
||||||
|
- **FR-002**: System MUST support hot reload of configuration files, detecting changes within 5 seconds and applying them without service restart
|
||||||
|
- **FR-003**: System MUST validate configuration values on load and reject invalid configurations with descriptive error messages
|
||||||
|
- **FR-004**: System MUST write all logs in structured JSON format using Zap logging library
|
||||||
|
- **FR-004a**: System MUST separate application logs (app.log) and HTTP access logs (access.log) into different files with independent configuration
|
||||||
|
- **FR-005**: System MUST rotate log files automatically using Lumberjack.v2 based on configurable size and age parameters for both application and access logs
|
||||||
|
- **FR-006**: System MUST retain log files according to configured retention policy and automatically remove expired logs, with separate retention settings for application and access logs
|
||||||
|
- **FR-007**: All API responses MUST follow the unified format: `{"code": [number], "data": [object/array/null], "msg": [string]}`
|
||||||
|
- **FR-008**: System MUST assign a unique request ID to every incoming HTTP request using requestid middleware
|
||||||
|
- **FR-008a**: Request IDs MUST be generated using UUID v4 format for maximum compatibility with distributed tracing systems and log aggregation tools
|
||||||
|
- **FR-009**: System MUST include the request ID in all log entries associated with that request
|
||||||
|
- **FR-010**: System MUST include the request ID in HTTP response headers for client-side tracing
|
||||||
|
- **FR-011**: System MUST log all HTTP requests with method, path, status code, duration, and request ID using logger middleware
|
||||||
|
- **FR-012**: System MUST automatically recover from panics during request processing using recover middleware
|
||||||
|
- **FR-013**: When a panic is recovered, system MUST log the full stack trace and error details
|
||||||
|
- **FR-014**: When a panic is recovered, system MUST return HTTP 500 with unified error response format
|
||||||
|
- **FR-015**: System MUST validate authentication tokens from the "token" request header using keyauth middleware
|
||||||
|
- **FR-016**: System MUST check token validity by verifying existence in Redis cache using token string as key
|
||||||
|
- **FR-016a**: System MUST store tokens in Redis as simple key-value pairs with token as key and user ID as value, using Redis TTL for expiration management
|
||||||
|
- **FR-016b**: When Redis is unavailable during token validation, system MUST fail closed and return HTTP 503 immediately without fallback or caching mechanisms
|
||||||
|
- **FR-017**: System MUST return HTTP 401 with appropriate error code and message when token is missing or invalid
|
||||||
|
- **FR-018**: System MUST provide configurable IP-based rate limiting capability using limiter middleware
|
||||||
|
- **FR-018a**: Rate limiting MUST track request counts per client IP address with configurable limits (requests per time window)
|
||||||
|
- **FR-018b**: When rate limit is exceeded, system MUST return HTTP 429 with code 1003 and appropriate error message
|
||||||
|
- **FR-019**: Rate limiting implementation MUST be provided but disabled by default in initial deployment
|
||||||
|
- **FR-020**: System MUST include documentation on how to configure and enable rate limiting per endpoint with example configurations
|
||||||
|
- **FR-021**: System MUST use consistent error codes across all error scenarios with bilingual (Chinese/English) support
|
||||||
|
- **FR-022**: Configuration MUST support different environments (development, staging, production) with separate config files
|
||||||
|
|
||||||
|
### Technical Requirements (Constitution-Driven)
|
||||||
|
|
||||||
|
**Tech Stack Compliance**:
|
||||||
|
- [x] All HTTP operations use Fiber framework (no `net/http` shortcuts)
|
||||||
|
- [x] All async tasks use Asynq (if applicable)
|
||||||
|
- [x] All logging uses Zap + Lumberjack.v2
|
||||||
|
- [x] All configuration uses Viper
|
||||||
|
|
||||||
|
**Architecture Requirements**:
|
||||||
|
- [x] Implementation follows Handler → Service → Store → Model layers (applies to auth token validation)
|
||||||
|
- [x] Dependencies injected via Service/Store structs
|
||||||
|
- [x] Unified error codes defined in `pkg/errors/`
|
||||||
|
- [x] Unified API responses via `pkg/response/`
|
||||||
|
- [x] All constants defined in `pkg/constants/` (no magic numbers/strings)
|
||||||
|
- [x] All Redis keys managed via `pkg/constants/` key generation functions
|
||||||
|
|
||||||
|
**API Design Requirements**:
|
||||||
|
- [x] All APIs follow RESTful principles
|
||||||
|
- [x] All responses use unified JSON format with code/message/data/timestamp
|
||||||
|
- [x] All error messages include error codes and bilingual descriptions
|
||||||
|
- [x] All time fields use ISO 8601 format (RFC3339)
|
||||||
|
|
||||||
|
**Performance Requirements**:
|
||||||
|
- [x] API response time (P95) < 200ms
|
||||||
|
- [x] Database queries < 50ms (if applicable)
|
||||||
|
- [x] Non-realtime operations delegated to async tasks (if applicable)
|
||||||
|
|
||||||
|
**Testing Requirements**:
|
||||||
|
- [x] Unit tests for all Service layer business logic
|
||||||
|
- [x] Integration tests for all API endpoints
|
||||||
|
- [x] Tests are independent and use mocks/testcontainers
|
||||||
|
- [x] Target coverage: 70%+ overall, 90%+ for core business logic
|
||||||
|
|
||||||
|
### Key Entities
|
||||||
|
|
||||||
|
- **Configuration**: Represents application configuration settings including server parameters, database connections, Redis settings, logging configuration (with separate settings for app.log and access.log including independent rotation and retention policies), and middleware settings. Supports hot reload capability to apply changes without restart.
|
||||||
|
|
||||||
|
- **AuthToken**: Represents an authentication token stored in Redis cache as a simple key-value pair. The token string is used as the Redis key, and the user ID is stored as the value. Token expiration is managed via Redis TTL mechanism. This structure enables O(1) existence checks for authentication validation.
|
||||||
|
|
||||||
|
- **Request Context**: Represents the execution context of an HTTP request, containing unique request ID (UUID v4 format), authentication information (user ID from token validation), request start time, and other metadata used for logging and tracing.
|
||||||
|
|
||||||
|
- **Log Entry**: Represents a structured log record containing timestamp, severity level, message, request ID, user context, and additional contextual fields, written in JSON format.
|
||||||
|
|
||||||
|
- **Rate Limit State**: Represents the current request count and time window for a specific client IP address, used to enforce per-IP rate limiting policies. Tracks remaining quota and window reset time for each unique IP.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: System administrators can modify any configuration value and see it applied within 5 seconds without service restart
|
||||||
|
- **SC-002**: All API responses follow the unified `{code, data, msg}` structure with 100% consistency across all endpoints
|
||||||
|
- **SC-003**: Every HTTP request generates a unique UUID v4 request ID that appears in the X-Request-ID response header and all associated log entries
|
||||||
|
- **SC-004**: System continues processing new requests within 100ms after recovering from a panic, with zero downtime
|
||||||
|
- **SC-005**: Log files automatically rotate when reaching configured size limits (e.g., 100MB) without manual intervention
|
||||||
|
- **SC-006**: Invalid authentication tokens are rejected within 50ms with clear error messages, preventing unauthorized access
|
||||||
|
- **SC-007**: All logs are written in valid JSON format that can be parsed by standard log aggregation tools without errors
|
||||||
|
- **SC-008**: 100% of HTTP requests are logged with method, path, status, duration, and request ID for complete audit trail
|
||||||
|
- **SC-009**: Rate limiting (when enabled) successfully blocks requests exceeding configured limits within the time window with appropriate error responses
|
||||||
|
- **SC-010**: System successfully loads configuration from different environment-specific files (dev, staging, prod) based on environment variable
|
||||||
509
specs/001-fiber-middleware-integration/tasks.md
Normal file
509
specs/001-fiber-middleware-integration/tasks.md
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
# Tasks: Fiber Middleware Integration with Configuration Management
|
||||||
|
|
||||||
|
**Feature**: 001-fiber-middleware-integration
|
||||||
|
**Input**: Design documents from `/specs/001-fiber-middleware-integration/`
|
||||||
|
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/api.yaml
|
||||||
|
|
||||||
|
**Tests**: Unit and integration tests are REQUIRED per constitution testing standards (70%+ overall, 90%+ core business)
|
||||||
|
|
||||||
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||||
|
|
||||||
|
## Format: `- [ ] [ID] [P?] [Story?] Description`
|
||||||
|
|
||||||
|
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||||
|
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
||||||
|
- Include exact file paths in descriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Infrastructure)
|
||||||
|
|
||||||
|
**Purpose**: Project initialization and basic structure
|
||||||
|
|
||||||
|
- [ ] T001 Create directory structure: pkg/config/, pkg/logger/, pkg/response/, pkg/errors/, pkg/constants/, pkg/validator/, internal/middleware/, configs/, logs/
|
||||||
|
- [ ] T002 [P] Setup unified error codes and messages in pkg/errors/codes.go
|
||||||
|
- [ ] T003 [P] Setup custom error types in pkg/errors/errors.go
|
||||||
|
- [ ] T004 [P] Setup unified response structure in pkg/response/response.go
|
||||||
|
- [ ] T005 [P] Setup response code constants in pkg/response/codes.go
|
||||||
|
- [ ] T006 [P] Setup business constants in pkg/constants/constants.go
|
||||||
|
- [ ] T007 [P] Setup Redis key generation functions in pkg/constants/redis.go
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
|
||||||
|
|
||||||
|
### Configuration Management (US1 Foundation)
|
||||||
|
|
||||||
|
- [ ] T008 Create Config structures with Viper mapstructure tags in pkg/config/config.go
|
||||||
|
- [ ] T009 Implement config loading with validation in pkg/config/loader.go
|
||||||
|
- [ ] T010 Implement config hot reload with fsnotify in pkg/config/watcher.go
|
||||||
|
- [ ] T011 Create default configuration file in configs/config.yaml
|
||||||
|
- [ ] T012 [P] Create environment-specific configs: config.dev.yaml, config.staging.yaml, config.prod.yaml
|
||||||
|
|
||||||
|
### Logging Infrastructure (US2 Foundation)
|
||||||
|
|
||||||
|
- [ ] T013 Initialize Zap logger with JSON encoder in pkg/logger/logger.go
|
||||||
|
- [ ] T014 Setup Lumberjack rotation for app.log in pkg/logger/rotation.go
|
||||||
|
- [ ] T015 Setup Lumberjack rotation for access.log in pkg/logger/rotation.go
|
||||||
|
- [ ] T016 Create Fiber logger middleware adapter in pkg/logger/middleware.go
|
||||||
|
|
||||||
|
### Redis Connection (US6 Foundation)
|
||||||
|
|
||||||
|
- [ ] T017 Setup Redis client with connection pool configuration in pkg/validator/redis.go or pkg/database/redis.go
|
||||||
|
|
||||||
|
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Configuration Hot Reload (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Enable runtime configuration updates without service restart
|
||||||
|
|
||||||
|
**Independent Test**: Modify config.yaml while server runs, verify changes applied within 5 seconds without restart
|
||||||
|
|
||||||
|
### Unit Tests for User Story 1
|
||||||
|
|
||||||
|
- [ ] T018 [P] [US1] Unit test for config loading and validation in pkg/config/loader_test.go
|
||||||
|
- [ ] T019 [P] [US1] Unit test for config hot reload mechanism in pkg/config/watcher_test.go
|
||||||
|
- [ ] T020 [P] [US1] Test invalid config handling (malformed YAML, validation errors) in pkg/config/config_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [ ] T021 [US1] Implement atomic config pointer swap in pkg/config/config.go (sync/atomic usage)
|
||||||
|
- [ ] T022 [US1] Implement config change callback with validation in pkg/config/watcher.go
|
||||||
|
- [ ] T023 [US1] Add config reload logging with Zap in pkg/config/watcher.go
|
||||||
|
- [ ] T024 [US1] Integrate config watcher with context cancellation in cmd/api/main.go
|
||||||
|
- [ ] T025 [US1] Add graceful shutdown for config watcher in cmd/api/main.go
|
||||||
|
|
||||||
|
**Checkpoint**: Config hot reload should work independently - modify config and see changes applied
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Structured Logging and Log Rotation (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Production-ready JSON logging with automatic rotation and separate app/access logs
|
||||||
|
|
||||||
|
**Independent Test**: Generate logs, verify JSON format in app.log and access.log, trigger rotation by size
|
||||||
|
|
||||||
|
### Unit Tests for User Story 2
|
||||||
|
|
||||||
|
- [ ] T026 [P] [US2] Unit test for logger initialization in pkg/logger/logger_test.go
|
||||||
|
- [ ] T027 [P] [US2] Unit test for log rotation configuration in pkg/logger/rotation_test.go
|
||||||
|
- [ ] T028 [P] [US2] Test structured logging with fields in pkg/logger/logger_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [ ] T029 [P] [US2] Create appLogger instance with Lumberjack writer in pkg/logger/logger.go
|
||||||
|
- [ ] T030 [P] [US2] Create accessLogger instance with separate Lumberjack writer in pkg/logger/logger.go
|
||||||
|
- [ ] T031 [US2] Configure JSON encoder with RFC3339 timestamps in pkg/logger/logger.go
|
||||||
|
- [ ] T032 [US2] Export GetAppLogger() and GetAccessLogger() functions in pkg/logger/logger.go
|
||||||
|
- [ ] T033 [US2] Add logger.Sync() call in graceful shutdown in cmd/api/main.go
|
||||||
|
|
||||||
|
**Checkpoint**: Both app.log and access.log should exist with JSON entries, rotate at configured size
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Unified API Response Format (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Consistent response structure across all endpoints
|
||||||
|
|
||||||
|
**Independent Test**: Call any endpoint, verify response has {code, data, msg, timestamp} structure
|
||||||
|
|
||||||
|
### Unit Tests for User Story 3
|
||||||
|
|
||||||
|
- [ ] T034 [P] [US3] Unit test for Success() response helper in pkg/response/response_test.go
|
||||||
|
- [ ] T035 [P] [US3] Unit test for Error() response helper in pkg/response/response_test.go
|
||||||
|
- [ ] T036 [P] [US3] Test response serialization with sonic JSON in pkg/response/response_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [ ] T037 [P] [US3] Implement Success() helper function in pkg/response/response.go
|
||||||
|
- [ ] T038 [P] [US3] Implement Error() helper function in pkg/response/response.go
|
||||||
|
- [ ] T039 [P] [US3] Implement SuccessWithMessage() helper function in pkg/response/response.go
|
||||||
|
- [ ] T040 [US3] Configure Fiber to use sonic as JSON serializer in cmd/api/main.go
|
||||||
|
- [ ] T041 [US3] Create example health check endpoint using response helpers in internal/handler/health.go
|
||||||
|
- [ ] T042 [US3] Register health check route in cmd/api/main.go
|
||||||
|
|
||||||
|
**Checkpoint**: Health check endpoint returns unified response format with proper structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: User Story 4 - Request Logging and Tracing (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Unique UUID v4 request ID for every request with comprehensive access logging
|
||||||
|
|
||||||
|
**Independent Test**: Make requests, verify each has unique X-Request-ID header and appears in logs
|
||||||
|
|
||||||
|
### Integration Tests for User Story 4
|
||||||
|
|
||||||
|
- [ ] T043 [P] [US4] Integration test for requestid middleware (UUID v4 generation) in tests/integration/middleware_test.go
|
||||||
|
- [ ] T044 [P] [US4] Integration test for logger middleware (access log entries) in tests/integration/middleware_test.go
|
||||||
|
- [ ] T045 [P] [US4] Test request ID propagation through middleware chain in tests/integration/middleware_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 4
|
||||||
|
|
||||||
|
- [ ] T046 [P] [US4] Configure Fiber requestid middleware with google/uuid in cmd/api/main.go
|
||||||
|
- [ ] T047 [US4] Implement custom logger middleware writing to accessLogger in internal/middleware/logger.go
|
||||||
|
- [ ] T048 [US4] Add request ID to Fiber Locals in logger middleware in internal/middleware/logger.go
|
||||||
|
- [ ] T049 [US4] Add X-Request-ID response header in logger middleware in internal/middleware/logger.go
|
||||||
|
- [ ] T050 [US4] Log request details (method, path, status, duration, IP, user_agent) to access.log in internal/middleware/logger.go
|
||||||
|
- [ ] T051 [US4] Register requestid and logger middleware in correct order in cmd/api/main.go
|
||||||
|
|
||||||
|
**Checkpoint**: Every request should have unique UUID v4 in header and access.log, with full request details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: User Story 5 - Automatic Error Recovery (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Recover from panics, log stack trace, return error response, continue serving requests
|
||||||
|
|
||||||
|
**Independent Test**: Trigger panic in handler, verify server doesn't crash, returns 500, logs stack trace
|
||||||
|
|
||||||
|
### Integration Tests for User Story 5
|
||||||
|
|
||||||
|
- [ ] T052 [P] [US5] Integration test for panic recovery in tests/integration/middleware_test.go
|
||||||
|
- [ ] T053 [P] [US5] Test panic logging with stack trace in tests/integration/middleware_test.go
|
||||||
|
- [ ] T054 [P] [US5] Test subsequent requests after panic recovery in tests/integration/middleware_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 5
|
||||||
|
|
||||||
|
- [ ] T055 [US5] Implement custom recover middleware with Zap logging in internal/middleware/recover.go
|
||||||
|
- [ ] T056 [US5] Add stack trace capture to recover middleware in internal/middleware/recover.go
|
||||||
|
- [ ] T057 [US5] Add request ID to panic logs in internal/middleware/recover.go
|
||||||
|
- [ ] T058 [US5] Return unified error response (500, code 1000) on panic in internal/middleware/recover.go
|
||||||
|
- [ ] T059 [US5] Register recover middleware as FIRST middleware in cmd/api/main.go
|
||||||
|
- [ ] T060 [US5] Create test panic endpoint for testing in internal/handler/test.go (optional, for quickstart validation)
|
||||||
|
|
||||||
|
**Checkpoint**: Panic in handler should be caught, logged with stack trace, return 500, server continues running
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: User Story 6 - Token-Based Authentication (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Validate authentication tokens against Redis, enforce access control
|
||||||
|
|
||||||
|
**Independent Test**: Request with valid/invalid/missing token, verify 200/401 responses with correct error codes
|
||||||
|
|
||||||
|
### Unit Tests for User Story 6
|
||||||
|
|
||||||
|
- [ ] T061 [P] [US6] Unit test for TokenValidator.Validate() with valid token in pkg/validator/token_test.go
|
||||||
|
- [ ] T062 [P] [US6] Unit test for expired/invalid token (redis.Nil) in pkg/validator/token_test.go
|
||||||
|
- [ ] T063 [P] [US6] Unit test for Redis unavailable (fail closed) in pkg/validator/token_test.go
|
||||||
|
- [ ] T064 [P] [US6] Unit test for context timeout in Redis operations in pkg/validator/token_test.go
|
||||||
|
|
||||||
|
### Integration Tests for User Story 6
|
||||||
|
|
||||||
|
- [ ] T065 [P] [US6] Integration test for keyauth middleware with valid token in tests/integration/auth_test.go
|
||||||
|
- [ ] T066 [P] [US6] Integration test for missing token (401, code 1001) in tests/integration/auth_test.go
|
||||||
|
- [ ] T067 [P] [US6] Integration test for invalid token (401, code 1002) in tests/integration/auth_test.go
|
||||||
|
- [ ] T068 [P] [US6] Integration test for Redis down (503, code 1004) in tests/integration/auth_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 6
|
||||||
|
|
||||||
|
- [ ] T069 [US6] Create TokenValidator struct with Redis client in pkg/validator/token.go
|
||||||
|
- [ ] T070 [US6] Implement TokenValidator.Validate() with Redis GET operation in pkg/validator/token.go
|
||||||
|
- [ ] T071 [US6] Add context timeout (50ms) for Redis operations in pkg/validator/token.go
|
||||||
|
- [ ] T072 [US6] Implement Redis availability check (Ping) with fail-closed behavior in pkg/validator/token.go
|
||||||
|
- [ ] T073 [US6] Implement custom keyauth middleware wrapper in internal/middleware/auth.go
|
||||||
|
- [ ] T074 [US6] Configure keyauth with header lookup "token" in internal/middleware/auth.go
|
||||||
|
- [ ] T075 [US6] Add validator callback to keyauth config in internal/middleware/auth.go
|
||||||
|
- [ ] T076 [US6] Store user_id in Fiber Locals after successful validation in internal/middleware/auth.go
|
||||||
|
- [ ] T077 [US6] Implement custom ErrorHandler mapping errors to response codes in internal/middleware/auth.go
|
||||||
|
- [ ] T078 [US6] Add auth failure logging with request ID in internal/middleware/auth.go
|
||||||
|
- [ ] T079 [US6] Register keyauth middleware after logger in cmd/api/main.go
|
||||||
|
- [ ] T080 [US6] Create protected example endpoint (/api/v1/users) in internal/handler/user.go
|
||||||
|
- [ ] T081 [US6] Register protected routes with middleware in cmd/api/main.go
|
||||||
|
|
||||||
|
**Checkpoint**: Protected endpoints require valid token, reject invalid/missing tokens with correct error codes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 9: User Story 7 - Rate Limiting Configuration (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Provide IP-based rate limiting capability (disabled by default, easy to enable)
|
||||||
|
|
||||||
|
**Independent Test**: Enable limiter, exceed limit, verify 429 responses; disable and verify no limiting
|
||||||
|
|
||||||
|
### Integration Tests for User Story 7
|
||||||
|
|
||||||
|
- [ ] T082 [P] [US7] Integration test for rate limiter with limit exceeded (429, code 1003) in tests/integration/ratelimit_test.go
|
||||||
|
- [ ] T083 [P] [US7] Integration test for rate limit reset after window expiration in tests/integration/ratelimit_test.go
|
||||||
|
- [ ] T084 [P] [US7] Test per-IP rate limiting (different IPs have separate limits) in tests/integration/ratelimit_test.go
|
||||||
|
|
||||||
|
### Implementation for User Story 7
|
||||||
|
|
||||||
|
- [ ] T085 [US7] Implement rate limiter middleware wrapper (COMMENTED by default) in internal/middleware/ratelimit.go
|
||||||
|
- [ ] T086 [US7] Configure limiter with IP-based key generator (c.IP()) in internal/middleware/ratelimit.go
|
||||||
|
- [ ] T087 [US7] Configure limiter with config values (Max, Expiration) in internal/middleware/ratelimit.go
|
||||||
|
- [ ] T088 [US7] Add custom LimitReached handler returning unified error response in internal/middleware/ratelimit.go
|
||||||
|
- [ ] T089 [US7] Add commented middleware registration example in cmd/api/main.go
|
||||||
|
- [ ] T090 [US7] Document rate limiter usage in quickstart.md (how to enable, configure)
|
||||||
|
- [ ] T091 [US7] Add rate limiter configuration examples to config files
|
||||||
|
|
||||||
|
**Checkpoint**: Rate limiter can be enabled via config, blocks excess requests per IP, returns 429 with code 1003
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 10: Polish & Quality Gates
|
||||||
|
|
||||||
|
**Purpose**: Final quality checks and cross-cutting improvements
|
||||||
|
|
||||||
|
### Documentation & Examples
|
||||||
|
|
||||||
|
- [ ] T092 [P] Update quickstart.md with actual file paths and final configuration
|
||||||
|
- [ ] T093 [P] Create example requests (curl commands) in quickstart.md for all scenarios
|
||||||
|
- [ ] T094 [P] Document middleware execution order in docs/ or README
|
||||||
|
- [ ] T095 [P] Add troubleshooting section to quickstart.md
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- [ ] T096 [P] Add Go doc comments to all exported functions and types
|
||||||
|
- [ ] T097 [P] Run gofmt on all Go files
|
||||||
|
- [ ] T098 [P] Run go vet and fix all issues
|
||||||
|
- [ ] T099 [P] Run golangci-lint and fix critical issues
|
||||||
|
- [ ] T100 Review all Redis key usage, ensure no hardcoded strings (use constants.RedisAuthTokenKey())
|
||||||
|
- [ ] T101 Review all error handling, ensure explicit returns (no panic abuse)
|
||||||
|
- [ ] T102 Review naming conventions (UserID not userId, HTTPServer not HttpServer)
|
||||||
|
- [ ] T103 Check for Java-style anti-patterns (no I-prefix, no Impl-suffix, no getters/setters)
|
||||||
|
|
||||||
|
### Testing & Coverage
|
||||||
|
|
||||||
|
- [ ] T104 Run all unit tests: go test ./pkg/...
|
||||||
|
- [ ] T105 Run all integration tests: go test ./tests/integration/...
|
||||||
|
- [ ] T106 Measure test coverage: go test -cover ./...
|
||||||
|
- [ ] T107 Verify core business logic coverage >= 90% (config, logger, validator)
|
||||||
|
- [ ] T108 Verify overall coverage >= 70%
|
||||||
|
|
||||||
|
### Security Audit
|
||||||
|
|
||||||
|
- [ ] T109 Review authentication fail-closed behavior (Redis unavailable = 503)
|
||||||
|
- [ ] T110 Review context timeouts on Redis operations
|
||||||
|
- [ ] T111 Check for command injection vulnerabilities
|
||||||
|
- [ ] T112 Verify no sensitive data in logs (tokens, passwords)
|
||||||
|
- [ ] T113 Review error messages (no sensitive information leakage)
|
||||||
|
|
||||||
|
### Performance Validation
|
||||||
|
|
||||||
|
- [ ] T114 Test middleware overhead < 5ms per request (load testing)
|
||||||
|
- [ ] T115 Verify log rotation doesn't block requests
|
||||||
|
- [ ] T116 Test config hot reload doesn't affect in-flight requests
|
||||||
|
- [ ] T117 Verify Redis connection pool handles load correctly
|
||||||
|
|
||||||
|
### Final Quality Gates
|
||||||
|
|
||||||
|
- [ ] T118 Quality Gate: All tests pass (go test ./...)
|
||||||
|
- [ ] T119 Quality Gate: No formatting issues (gofmt -l . returns empty)
|
||||||
|
- [ ] T120 Quality Gate: No vet issues (go vet ./...)
|
||||||
|
- [ ] T121 Quality Gate: Test coverage meets requirements (70%+ overall, 90%+ core)
|
||||||
|
- [ ] T122 Quality Gate: All TODOs/FIXMEs addressed or documented
|
||||||
|
- [ ] T123 Quality Gate: quickstart.md works end-to-end (manual validation)
|
||||||
|
- [ ] T124 Quality Gate: All middleware integrated and working together
|
||||||
|
- [ ] T125 Quality Gate: Graceful shutdown works correctly (no goroutine leaks)
|
||||||
|
- [ ] T126 Quality Gate: Constitution compliance verified (no violations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
1. **Setup (Phase 1)**: No dependencies - start immediately
|
||||||
|
2. **Foundational (Phase 2)**: Depends on Setup (Phase 1) - BLOCKS all user stories
|
||||||
|
3. **User Stories (Phases 3-9)**: All depend on Foundational (Phase 2) completion
|
||||||
|
- US1, US2, US3 can proceed in parallel (independent)
|
||||||
|
- US4 depends on US2 (needs logger)
|
||||||
|
- US5 can proceed in parallel with others
|
||||||
|
- US6 depends on US3 (needs response format)
|
||||||
|
- US7 depends on US3 (needs response format)
|
||||||
|
4. **Polish (Phase 10)**: Depends on all desired user stories being complete
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
Foundational (Phase 2) - MUST COMPLETE FIRST
|
||||||
|
├─→ US1: Config Hot Reload (independent)
|
||||||
|
├─→ US2: Logging (independent)
|
||||||
|
├─→ US3: Response Format (independent)
|
||||||
|
│
|
||||||
|
├─→ US4: Request Tracing (depends on US2: logger)
|
||||||
|
├─→ US5: Error Recovery (independent)
|
||||||
|
│
|
||||||
|
├─→ US6: Authentication (depends on US3: response format)
|
||||||
|
└─→ US7: Rate Limiting (depends on US3: response format)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Tests MUST be written FIRST and FAIL before implementation
|
||||||
|
- Models/structures before services
|
||||||
|
- Services before middleware
|
||||||
|
- Middleware before registration in main.go
|
||||||
|
- Core implementation before integration
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
**Phase 1 (Setup)**: All tasks T002-T007 marked [P] can run in parallel
|
||||||
|
|
||||||
|
**Phase 2 (Foundational)**:
|
||||||
|
- T012 (dev/staging/prod configs) can run in parallel with others
|
||||||
|
- T013-T016 (logging) can run in parallel as group
|
||||||
|
- These are independent file operations
|
||||||
|
|
||||||
|
**Phase 3-9 (User Stories)**:
|
||||||
|
- After Foundational completes, these can start in parallel:
|
||||||
|
- US1: T018-T025 (config hot reload)
|
||||||
|
- US2: T026-T033 (logging)
|
||||||
|
- US3: T034-T042 (response format)
|
||||||
|
- US5: T052-T060 (error recovery)
|
||||||
|
- After US2 completes:
|
||||||
|
- US4: T043-T051 (request tracing)
|
||||||
|
- After US3 completes:
|
||||||
|
- US6: T061-T081 (authentication)
|
||||||
|
- US7: T082-T091 (rate limiting)
|
||||||
|
|
||||||
|
**Phase 10 (Polish)**: Many tasks marked [P] can run in parallel (documentation, linting, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
### Example 1: Setup Phase (All Parallel)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch all setup tasks together (different files):
|
||||||
|
Task T002: "Setup unified error codes in pkg/errors/codes.go"
|
||||||
|
Task T003: "Setup custom error types in pkg/errors/errors.go"
|
||||||
|
Task T004: "Setup unified response structure in pkg/response/response.go"
|
||||||
|
Task T005: "Setup response code constants in pkg/response/codes.go"
|
||||||
|
Task T006: "Setup business constants in pkg/constants/constants.go"
|
||||||
|
Task T007: "Setup Redis key generation functions in pkg/constants/redis.go"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: User Story Tests (Parallel within Story)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# US6 unit tests - all can run in parallel:
|
||||||
|
Task T061: "Unit test for TokenValidator with valid token"
|
||||||
|
Task T062: "Unit test for expired/invalid token"
|
||||||
|
Task T063: "Unit test for Redis unavailable"
|
||||||
|
Task T064: "Unit test for context timeout"
|
||||||
|
|
||||||
|
# US6 integration tests - all can run in parallel:
|
||||||
|
Task T065: "Integration test with valid token"
|
||||||
|
Task T066: "Integration test for missing token"
|
||||||
|
Task T067: "Integration test for invalid token"
|
||||||
|
Task T068: "Integration test for Redis down"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Independent User Stories (After Foundation)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After Phase 2 completes, these can all start in parallel:
|
||||||
|
Agent 1: Work on US1 (Config Hot Reload) - T018 through T025
|
||||||
|
Agent 2: Work on US2 (Logging) - T026 through T033
|
||||||
|
Agent 3: Work on US3 (Response Format) - T034 through T042
|
||||||
|
Agent 4: Work on US5 (Error Recovery) - T052 through T060
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (Minimum Viable Product)
|
||||||
|
|
||||||
|
**Recommendation**: Complete Phases 1-5 (Setup + Foundation + US1 + US2 + US3) for MVP
|
||||||
|
|
||||||
|
This delivers:
|
||||||
|
- Configuration hot reload (US1)
|
||||||
|
- Production logging (US2)
|
||||||
|
- API response consistency (US3)
|
||||||
|
|
||||||
|
Then deploy and validate before adding authentication and other features.
|
||||||
|
|
||||||
|
### Incremental Delivery Roadmap
|
||||||
|
|
||||||
|
1. **Foundation** (Phases 1-2): ~2-3 days
|
||||||
|
- Setup + Foundational infrastructure
|
||||||
|
- **Deliverable**: Basic Fiber app with config, logging, response structure
|
||||||
|
|
||||||
|
2. **MVP** (Phases 3-5): ~2-3 days
|
||||||
|
- US1: Config hot reload
|
||||||
|
- US2: Logging with rotation
|
||||||
|
- US3: Unified responses
|
||||||
|
- **Deliverable**: Production-ready foundation with observability
|
||||||
|
|
||||||
|
3. **Observability+** (Phases 6-7): ~2-3 days
|
||||||
|
- US4: Request tracing
|
||||||
|
- US5: Error recovery
|
||||||
|
- **Deliverable**: Complete observability and fault tolerance
|
||||||
|
|
||||||
|
4. **Security** (Phase 8): ~2-3 days
|
||||||
|
- US6: Authentication
|
||||||
|
- **Deliverable**: Secured API with Redis token validation
|
||||||
|
|
||||||
|
5. **Production Hardening** (Phases 9-10): ~2-3 days
|
||||||
|
- US7: Rate limiting (optional)
|
||||||
|
- Polish and quality gates
|
||||||
|
- **Deliverable**: Production-ready system
|
||||||
|
|
||||||
|
**Total Estimated Time**: 10-15 days (single developer)
|
||||||
|
|
||||||
|
### Parallel Team Strategy
|
||||||
|
|
||||||
|
With 3 developers after Foundation completes:
|
||||||
|
|
||||||
|
- **Dev A**: US1 + US2 (Config + Logging) - Core infrastructure
|
||||||
|
- **Dev B**: US3 + US4 (Response + Tracing) - API foundation
|
||||||
|
- **Dev C**: US5 + US6 (Recovery + Auth) - Reliability + Security
|
||||||
|
|
||||||
|
Then converge for US7 and Polish.
|
||||||
|
|
||||||
|
**Estimated Time with Parallel Work**: 7-10 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Summary
|
||||||
|
|
||||||
|
- **Total Tasks**: 126
|
||||||
|
- **Setup Phase**: 7 tasks
|
||||||
|
- **Foundational Phase**: 10 tasks (BLOCKING)
|
||||||
|
- **User Story 1** (Config): 8 tasks (3 tests + 5 implementation)
|
||||||
|
- **User Story 2** (Logging): 8 tasks (3 tests + 5 implementation)
|
||||||
|
- **User Story 3** (Response): 9 tasks (3 tests + 6 implementation)
|
||||||
|
- **User Story 4** (Tracing): 9 tasks (3 tests + 6 implementation)
|
||||||
|
- **User Story 5** (Recovery): 9 tasks (3 tests + 6 implementation)
|
||||||
|
- **User Story 6** (Auth): 21 tasks (8 tests + 13 implementation)
|
||||||
|
- **User Story 7** (Rate Limit): 10 tasks (3 tests + 7 implementation)
|
||||||
|
- **Polish Phase**: 35 tasks (documentation, quality, security)
|
||||||
|
|
||||||
|
**Parallelizable Tasks**: 45 tasks marked [P]
|
||||||
|
|
||||||
|
**Test Coverage**:
|
||||||
|
- Unit tests: 23 tasks
|
||||||
|
- Integration tests: 18 tasks
|
||||||
|
- Total test tasks: 41 (32% of all tasks)
|
||||||
|
|
||||||
|
**Constitution Compliance**:
|
||||||
|
- ✅ All tasks follow Handler → Service → Store → Model pattern
|
||||||
|
- ✅ All Redis keys use generation functions (no hardcoded strings)
|
||||||
|
- ✅ All responses use unified format
|
||||||
|
- ✅ All error codes centralized
|
||||||
|
- ✅ Go idiomatic patterns (no Java-style anti-patterns)
|
||||||
|
- ✅ Explicit error handling throughout
|
||||||
|
- ✅ Context usage for timeouts and cancellation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Tasks.md generated and validated
|
||||||
|
2. ⏳ Review task list with team
|
||||||
|
3. ⏳ Set up development environment (Go 1.25.1, Redis 7.x)
|
||||||
|
4. ⏳ Run `/speckit.implement` to begin implementation
|
||||||
|
5. ⏳ Start with Phase 1 (Setup) - all tasks can run in parallel
|
||||||
|
|
||||||
|
**Ready for Implementation**: Yes - all tasks are specific, have file paths, and follow constitution principles.
|
||||||
Reference in New Issue
Block a user