新增内容: 1. CLAUDE.md - 添加测试验证规范 - 数据库连接配置 - 测试运行命令 - 性能基准参考值 - 覆盖率目标 - 常见问题与解决方案 2. project_experience_summary.md - 添加测试验证经验 - 集成测试环境配置 - 测试覆盖率要求 - 性能基准测试 - E2E测试常见问题 - 数据库表验证步骤 - 中间件鲁棒性验证
13 KiB
Supply API 项目经验总结
本文档总结项目实施过程中的关键经验教训
一、设计阶段常见问题
1.1 跨文档命名不一致
问题描述:
在代码审查中发现多处字段命名不一致,如 ClientIP vs SourceIP,导致类型转换错误。
受影响的文件:
auth.go使用ClientIPaudit_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_id 和 user_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(跨服务)
- Claims:subject_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 socketdatabase 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 数据库表验证
验证步骤:
- 连接数据库:
psql或pg_isready - 列出所有表:
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' - 验证字段:
SELECT column_name FROM information_schema.columns WHERE table_name = 'xxx' - 验证索引:
SELECT indexname FROM pg_indexes WHERE tablename = 'xxx'
核心表结构验证通过:
supply_accounts- 包含version字段(乐观锁)supply_packages- 包含available_quota、version字段supply_settlements- 支持FOR UPDATE SKIP LOCKED、NOWAIT
8.6 中间件鲁棒性验证
TimeoutMiddleware 并发问题:
- 主 goroutine 和 handler goroutine 不能同时持有锁
- 使用
sync.Once或互斥锁 + 标志位确保响应只发送一次 - 超时设置必须足够长(建议 >100ms)
性能测试注意:
- 基准测试在 short mode 下会被跳过
testing.Short()返回 true 时不运行基准测试- 使用
-short=false覆盖默认行为
九、改进建议
9.1 短期改进
- 补充集成测试(43个测试已通过)
- 修复 E2E 测试编译错误
- 建立基准测试套件
- 完善 API 文档(OpenAPI/Swagger)
- 补充 middleware 模块测试覆盖率(当前 53.9%)
9.2 中期改进
- 实现数据库连接池监控
- 添加 Redis 缓存命中率指标
- 完善错误码体系文档
- 补充 domain 模块测试覆盖率(当前 61.2%)
9.3 长期改进
- 迁移到 gRPC
- 实现服务网格
- 添加 A/B 测试框架