Files
lijiaoqiao/supply-api/docs/project_experience_summary.md
Your Name cdb3a453bb docs: 更新项目文档,添加测试验证规范和经验总结
新增内容:
1. CLAUDE.md - 添加测试验证规范
   - 数据库连接配置
   - 测试运行命令
   - 性能基准参考值
   - 覆盖率目标
   - 常见问题与解决方案

2. project_experience_summary.md - 添加测试验证经验
   - 集成测试环境配置
   - 测试覆盖率要求
   - 性能基准测试
   - E2E测试常见问题
   - 数据库表验证步骤
   - 中间件鲁棒性验证
2026-04-09 14:32:36 +08:00

13 KiB
Raw Blame History

Supply API 项目经验总结

本文档总结项目实施过程中的关键经验教训

一、设计阶段常见问题

1.1 跨文档命名不一致

问题描述 在代码审查中发现多处字段命名不一致,如 ClientIP vs SourceIP,导致类型转换错误。

受影响的文件

  • auth.go 使用 ClientIP
  • audit_event.go 使用 SourceIP

修复方案 统一使用 SourceIP,更新所有引用。

经验教训

  • 建立跨模块字段命名标准文档
  • Code Review 时重点检查命名一致性
  • 使用 linter 检测不一致的字段名

1.2 接口定义与实现不匹配

问题描述 领域层定义的 Store 接口缺少乐观锁参数,但实现层已支持。

示例

// 接口定义(缺少版本控制)
type SettlementStore interface {
    Update(ctx context.Context, s *Settlement) error
}

// 实现(已支持乐观锁)
func (r *SettlementRepository) Update(ctx context.Context, pkg *Settlement, expectedVersion int) error

修复方案 同步更新接口定义,添加 expectedVersion 参数。

经验教训

  • 接口定义必须与实现保持同步
  • 大型重构前先梳理接口依赖
  • 使用接口适配器模式桥接新旧实现

1.3 缓存与吊销机制矛盾

问题描述 Token 缓存在有效期内无法及时吊销。

修复方案

  • 缓存 TTL 设置较短10秒
  • 吊销时主动失效缓存
  • 后端状态变更触发缓存刷新

经验教训

  • 缓存策略必须考虑吊销场景
  • 主动失效优于被动过期

二、代码实现常见问题

2.1 重复代码

问题描述 main.go 中存在与 healthcheck.go 重复的健康检查处理函数。

修复前

// main.go 中的 inline handler
mux.HandleFunc("/actuator/health", handleHealthCheck(db, redisCache))
mux.HandleFunc("/actuator/health/live", handleLiveness)
mux.HandleFunc("/actuator/health/ready", handleReadiness)

// healthcheck.go 中已有的完整实现
type HealthHandler struct {
    healthChecker  *DefaultHealthChecker
    readinessChecks []HealthChecker
    livenessChecks []HealthChecker
}

修复后

// 统一使用 HealthHandler
healthHandler := httpapi.NewHealthHandlerWithDefaults(dbHealthCheck, redisHealthCheck)
mux.HandleFunc("/actuator/health", healthHandler.ServeHealth)

经验教训

  • 优先使用已有的通用组件
  • 避免在 main.go 中直接实现业务逻辑
  • 定期清理不再使用的 inline handlers

2.2 结构化日志缺失

问题描述 Logging 中间件使用标准库 log.Printf 而非结构化日志。

修复前

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)  // 非结构化
        next.ServeHTTP(w, r)
    })
}

修复后

func Logging(next http.Handler, logger logging.Logger) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fields := map[string]interface{}{
            "method": r.Method,
            "path":   r.URL.Path,
            "trace_id": tc.TraceID,
        }
        logger.Info("HTTP request", fields)
        next.ServeHTTP(w, r)
    })
}

经验教训

  • 生产环境必须使用结构化日志
  • 日志需包含timestamp, level, trace_id, request_id, 业务字段
  • 结构化日志便于查询和分析

2.3 未使用的导入和函数

问题描述 代码变更后遗留未使用的导入和函数定义。

示例 删除 inline handler 后未删除 encoding/json 导入。

经验教训

  • 使用 go vet 和 IDE 检查未使用的导入
  • 删除废弃代码而非注释
  • 代码重构后立即清理相关引用

三、数据库设计问题

3.1 字段映射错误

问题描述 Package Repository 中 SupplierID 重复映射到 supply_account_iduser_id

修复前

pkg.SupplierID, pkg.SupplierID, pkg.Platform, pkg.Model,  // 错误SupplierID 出现两次

修复后

pkg.SupplierID, pkg.AccountID, pkg.Platform, pkg.Model,  // 正确映射

经验教训

  • SQL 参数绑定时仔细核对字段顺序
  • 使用结构体标签明确映射关系
  • 编写数据库相关的单元测试

3.2 乐观锁与悲观锁选择

使用场景

场景 锁策略 说明
结算状态更新 乐观锁 低频操作,冲突概率低
配额扣减 悲观锁 高并发,需要保证原子性
账户余额 悲观锁 财务敏感操作

经验教训

  • 根据业务场景选择合适的锁策略
  • 乐观锁需处理 ErrConcurrencyConflict 错误
  • 悲观锁需考虑锁超时和死锁

四、中间件设计问题

4.1 Tracing 中间件缺失

问题描述 未实现 W3C Trace Context 标准,无法进行分布式追踪。

修复方案

// 解析 traceparent header
func ParseTraceParent(traceParent string) (*TraceContext, error) {
    // 格式: 00-{trace-id}-{span-id}-{trace-flags}
    // 长度: 55 字符
    traceID := traceParent[3:35]
    spanID := traceParent[36:52]
}

// 注入到 context
func TracingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        traceParent := r.Header.Get("traceparent")
        // 解析并注入 context
    })
}

经验教训

  • 微服务必须实现分布式追踪
  • 遵循 W3C Trace Context 标准
  • trace_id 需要贯穿所有日志

4.2 TimeoutMiddleware 并发安全

问题描述 超时中间件实现存在死锁和竞态条件,导致测试不稳定。

错误实现(死锁)

// 错误:主 goroutine 获取锁后等待 handler goroutine
mu.Lock()
go func() {
    next.ServeHTTP(wrapped, r)  // wrapped.WriteHeader() 尝试获取同一个锁
    mu.Unlock()  // 死锁!
}()
select {
case <-done:
    return
case <-time.After(timeout):
    mu.Lock()  // 再次尝试获取锁 - 死锁!
    // ...
}

错误实现(竞态)

// 错误handler 和超时同时写入 ResponseWriter
go func() {
    next.ServeHTTP(w, r)  // 写入 200
    close(handlerDone)
}()

select {
case <-handlerDone:
    return
case <-time.After(timeout):
    // handler 可能同时写入,造成竞态
    http.Error(w, "timeout", 504)
}

正确实现

func WithTimeoutMiddleware(next http.Handler, timeout time.Duration) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var mu sync.Mutex
        responseSent := false

        handlerDone := make(chan struct{})

        go func() {
            next.ServeHTTP(w, r)
            close(handlerDone)
        }()

        select {
        case <-handlerDone:
            return
        case <-time.After(timeout):
            mu.Lock()
            if !responseSent {
                responseSent = true
                mu.Unlock()
                w.Header().Set("X-Timeout", "true")
                http.Error(w, fmt.Sprintf("middleware timeout after %v", timeout), http.StatusGatewayTimeout)
                return
            }
            mu.Unlock()
            return
        }
    })
}

经验教训

  • 中间件的锁设计必须清晰:主 goroutine 和 handler goroutine 不能同时持有锁
  • 使用 sync.Once 或互斥锁 + 标志位确保响应只发送一次
  • 超时设置必须足够长(建议 >100ms避免在 race 检测下不稳定
  • 基准测试和单元测试的超时设置需要合理匹配
  • 测试覆盖率不等于测试质量:需要真正验证并发场景

五、测试问题

5.1 Mock 对象未正确覆盖所有方法

问题描述 captureLogger 仅覆盖了 log() 方法,但测试调用的是 Info()Debug() 等方法。

修复

type captureLogger struct {
    *jsonLogger
}

func (l *captureLogger) Info(msg string, fields ...map[string]interface{}) {
    var f map[string]interface{}
    if len(fields) > 0 {
        f = fields[0]
    }
    l.log(LogLevelInfo, msg, f)
}
// 类似覆盖 Debug, Warn, Error, Fatal

经验教训

  • Go 嵌入式方法调用解析到被嵌入类型
  • Mock 对象必须覆盖所有公共方法
  • 编写测试后实际运行验证

六、项目管理问题

6.1 过期文件清理

问题描述 Git 仓库中遗留大量已删除但未清理的报告文件。

修复命令

git rm $(git status --short | grep "^ D " | sed 's/^ D //')

经验教训

  • 定期清理已删除文件的 git 跟踪状态
  • 报告文件使用归档目录而非版本控制
  • CI/CD 流程自动清理过期文件

6.2 文档与代码不同步

问题描述 代码变更后相关设计文档未同步更新。

经验教训

  • 文档更新纳入代码变更流程
  • 使用文档即代码Docs as Code实践
  • 自动化文档生成

七、关键设计决策记录

7.1 JWT Token 格式

  • 算法HS256内部服务/ RS256跨服务
  • Claimssubject_id, role, scope, tenant_id, iat, exp

7.2 审计事件采样策略

  • 成功率1% 采样
  • 失败率100% 采样

7.3 健康检查路径

  • /actuator/health - 综合健康
  • /actuator/health/live - 存活探针
  • /actuator/health/ready - 就绪探针

八、测试验证经验2026-04-09

8.1 集成测试环境配置

Unix Socket vs TCP 连接

# Unix socket开发环境推荐
export SUPPLY_API_DB_HOST="/var/run/postgresql"
dsn = "postgres://user:password@/dbname?host=/var/run/postgresql&sslmode=disable"

# TCP 连接(生产环境)
dsn = "postgres://user:password@localhost:5432/dbname?sslmode=disable"

常见错误

  • password authentication failed - 检查 pg_hba.conf 或使用 Unix socket
  • database does not exist - DSN 路径解析错误
  • server error (FATAL) - 主机名解析问题

8.2 测试覆盖率要求

模块 当前覆盖率 最低要求 优秀
audit/events 97.6% 80% 95%+
audit/handler 79.6% 75% 85%+
audit/model 93.8% 80% 90%+
audit/sanitizer 84.3% 80% 90%+
audit/service 83.0% 80% 85%+
security 88.8% 80% 90%+
domain 61.2% 70% 80%+
middleware 53.9% 70% 80%+

8.3 性能基准测试

运行方式

go test -tags=slow -bench=. -benchmem -run=^$ ./internal/benchmark/...

参考性能数据

操作 性能 Allocation
AccountService_Create 678.7 ns/op 601 B/op, 5 allocs
AccountService_Verify 3.6 ns/op 0 B/op, 0 allocs
PackageService_CreateDraft 508.8 ns/op 462 B/op, 1 allocs
SettlementService_Withdraw 625.7 ns/op 463 B/op, 2 allocs
ConcurrentAccountAccess 3.5 ns/op 0 B/op, 0 allocs
LoggingMiddleware 1.8 μs/op 5.4 KB/op, 18 allocs
TracingMiddleware 1.9 μs/op 5.7 KB/op, 19 allocs

8.4 E2E 测试常见问题

编译错误

// 错误:导入但未使用
import (
    "context"                    // ❌ 未使用
    "github.com/stretchr/testify/assert"  // ❌ 未使用
)

// 修复
import (
    _ "context"  // 使用空白导入或删除
)

变量声明未使用

// 错误
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()

// 修复
_, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
_ = ctx  // 如果确实需要 ctx

8.5 数据库表验证

验证步骤

  1. 连接数据库:psqlpg_isready
  2. 列出所有表:SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'
  3. 验证字段:SELECT column_name FROM information_schema.columns WHERE table_name = 'xxx'
  4. 验证索引:SELECT indexname FROM pg_indexes WHERE tablename = 'xxx'

核心表结构验证通过

  • supply_accounts - 包含 version 字段(乐观锁)
  • supply_packages - 包含 available_quotaversion 字段
  • supply_settlements - 支持 FOR UPDATE SKIP LOCKEDNOWAIT

8.6 中间件鲁棒性验证

TimeoutMiddleware 并发问题

  • 主 goroutine 和 handler goroutine 不能同时持有锁
  • 使用 sync.Once 或互斥锁 + 标志位确保响应只发送一次
  • 超时设置必须足够长(建议 >100ms

性能测试注意

  • 基准测试在 short mode 下会被跳过
  • testing.Short() 返回 true 时不运行基准测试
  • 使用 -short=false 覆盖默认行为

九、改进建议

9.1 短期改进

  1. 补充集成测试43个测试已通过
  2. 修复 E2E 测试编译错误
  3. 建立基准测试套件
  4. 完善 API 文档OpenAPI/Swagger
  5. 补充 middleware 模块测试覆盖率(当前 53.9%

9.2 中期改进

  1. 实现数据库连接池监控
  2. 添加 Redis 缓存命中率指标
  3. 完善错误码体系文档
  4. 补充 domain 模块测试覆盖率(当前 61.2%

9.3 长期改进

  1. 迁移到 gRPC
  2. 实现服务网格
  3. 添加 A/B 测试框架