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

499 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Supply API 项目经验总结
> 本文档总结项目实施过程中的关键经验教训
## 一、设计阶段常见问题
### 1.1 跨文档命名不一致
**问题描述**
在代码审查中发现多处字段命名不一致,如 `ClientIP` vs `SourceIP`,导致类型转换错误。
**受影响的文件**
- `auth.go` 使用 `ClientIP`
- `audit_event.go` 使用 `SourceIP`
**修复方案**
统一使用 `SourceIP`,更新所有引用。
**经验教训**
- 建立跨模块字段命名标准文档
- Code Review 时重点检查命名一致性
- 使用 linter 检测不一致的字段名
### 1.2 接口定义与实现不匹配
**问题描述**
领域层定义的 Store 接口缺少乐观锁参数,但实现层已支持。
**示例**
```go
// 接口定义(缺少版本控制)
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` 重复的健康检查处理函数。
**修复前**
```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
}
```
**修复后**
```go
// 统一使用 HealthHandler
healthHandler := httpapi.NewHealthHandlerWithDefaults(dbHealthCheck, redisHealthCheck)
mux.HandleFunc("/actuator/health", healthHandler.ServeHealth)
```
**经验教训**
- 优先使用已有的通用组件
- 避免在 main.go 中直接实现业务逻辑
- 定期清理不再使用的 inline handlers
### 2.2 结构化日志缺失
**问题描述**
Logging 中间件使用标准库 `log.Printf` 而非结构化日志。
**修复前**
```go
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)
})
}
```
**修复后**
```go
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`
**修复前**
```go
pkg.SupplierID, pkg.SupplierID, pkg.Platform, pkg.Model, // 错误SupplierID 出现两次
```
**修复后**
```go
pkg.SupplierID, pkg.AccountID, pkg.Platform, pkg.Model, // 正确映射
```
**经验教训**
- SQL 参数绑定时仔细核对字段顺序
- 使用结构体标签明确映射关系
- 编写数据库相关的单元测试
### 3.2 乐观锁与悲观锁选择
**使用场景**
| 场景 | 锁策略 | 说明 |
|------|--------|------|
| 结算状态更新 | 乐观锁 | 低频操作,冲突概率低 |
| 配额扣减 | 悲观锁 | 高并发,需要保证原子性 |
| 账户余额 | 悲观锁 | 财务敏感操作 |
**经验教训**
- 根据业务场景选择合适的锁策略
- 乐观锁需处理 `ErrConcurrencyConflict` 错误
- 悲观锁需考虑锁超时和死锁
---
## 四、中间件设计问题
### 4.1 Tracing 中间件缺失
**问题描述**
未实现 W3C Trace Context 标准,无法进行分布式追踪。
**修复方案**
```go
// 解析 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 并发安全
**问题描述**
超时中间件实现存在死锁和竞态条件,导致测试不稳定。
**错误实现(死锁)**
```go
// 错误:主 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() // 再次尝试获取锁 - 死锁!
// ...
}
```
**错误实现(竞态)**
```go
// 错误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)
}
```
**正确实现**
```go
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()` 等方法。
**修复**
```go
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 仓库中遗留大量已删除但未清理的报告文件。
**修复命令**
```bash
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 连接**
```bash
# 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 性能基准测试
**运行方式**
```bash
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 测试常见问题
**编译错误**
```go
// 错误:导入但未使用
import (
"context" // ❌ 未使用
"github.com/stretchr/testify/assert" // ❌ 未使用
)
// 修复
import (
_ "context" // 使用空白导入或删除
)
```
**变量声明未使用**
```go
// 错误
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
// 修复
_, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
_ = ctx // 如果确实需要 ctx
```
### 8.5 数据库表验证
**验证步骤**
1. 连接数据库:`psql``pg_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_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 短期改进
1. [x] 补充集成测试43个测试已通过
2. [x] 修复 E2E 测试编译错误
3. [x] 建立基准测试套件
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 测试框架