docs: sync review reports, runbooks, and checklists

This commit is contained in:
Your Name
2026-05-11 12:19:15 +08:00
parent 67922c589a
commit 9319583ee3
16 changed files with 1450 additions and 31 deletions

View File

@@ -0,0 +1,246 @@
# ai-customer-service 生产上线修复方案与技术任务拆解
> 生成日期2026-05-11
> 基线 commit`67922c5` (HEAD)
> 技术负责人TechLead
> 对应 Review 报告:`docs/CODE_REVIEW_REPORT_SYSTEMATIC_2026-05-11.md`
---
## 1. 设计范围
### 1.1 本次覆盖
| Review 报告项 | 优先级 | 本次是否覆盖 |
|---|---|---|
| P0-1 Dirty worktree 收口 | P0 | ✅ 覆盖 |
| P0-2 Makefile test 目标缺少 `-p 1` | P0 | ✅ 覆盖 |
| P1-1 ticket_handler.List 覆盖率 0% | P1 | ✅ 覆盖 |
| P1-2 newapi_adapter.BuildIngressAck 覆盖率 0% | P1 | ✅ 覆盖 |
| P1-3 Authz header 伪造风险未文档化 | P1 | ✅ 覆盖 |
| P1-4 RateLimiter GC 压力 | P1 | ✅ 覆盖 |
| P1-5 IPv6 地址在 rate limit key 中被错误截断 | P1 | ✅ 覆盖 |
| P2-1 配置解析失败静默回退 | P2 | ✅ 覆盖 |
| P2-2 callback worker 无连接池限制 | P2 | ✅ 覆盖 |
| P2-3 缺少 SQLite/内存测试回退 | P2 | ✅ 覆盖 |
| P2-4 worker 缺少优雅关闭等待 | P2 | ✅ 覆盖 |
### 1.2 明确不做
- **不引入 pgx 替换 lib/pq**review 报告建议项,但当前无功能缺口,不属于阻塞问题。
- **不实现 newapi 完整 ingress 逻辑**:旧 remediation boardD-01/I-01已覆盖该设计缺口本次仅补充 `BuildIngressAck` 的单元测试(与 review P1-2 对应),不改变 newapi 仍为 501 占位的事实。
- **不引入 testcontainers-go**P2-3 仅做 skip 回退与文档标注,不做完整容器化测试基础设施。
- **不改写 outbox 并发 claim / transactional outbox**:属于旧 remediation boardI-04/I-05范围本次不做。
### 1.3 与 review 报告对应关系
| 本方案章节 | Review 报告章节 | 问题编号 |
|---|---|---|
| 3.1 P0 修复 | 5. P0 — 阻塞级 | P0-1, P0-2 |
| 3.2 P1 修复 | 5. P1 — 必须修复 | P1-1 ~ P1-5 |
| 3.3 P2 修复 | 5. P2 — 建议修复 | P2-1 ~ P2-4 |
---
## 2. 修复方案总览
| 问题 | 技术方案概述 |
|---|---|
| **P0-1 Dirty worktree** | 分 2 批 commit① 文档文件8 modified + 4 untracked docs② 代码文件3 modified internal。commit 后打 tag `v0.9.1-pre`。 |
| **P0-2 Makefile test** | `Makefile:2` 改为 `go test ./... -count=1 -p 1`,与 README/CI 保持一致。 |
| **P1-1 ticket_handler.List 0%** | 在 `ticket_handler_test.go` 补充 `List` 的成功与错误分支单元测试,使用现有 `mockTicketService` + `memory.TicketStore`。 |
| **P1-2 newapi_adapter.BuildIngressAck 0%** | 新建 `internal/platformadapter/newapi_adapter_test.go`,覆盖 `BuildIngressAck(meta=nil)``meta!=nil` 两个分支。 |
| **P1-3 Authz 伪造风险** | 新建 `docs/SECURITY_BOUNDARY.md`,明确 `RequireRoles` 的信任边界;在 `authz.go` 函数注释中标注“依赖上游网关完成真实身份验证”。 |
| **P1-4 RateLimiter GC 压力** | `limits.go:67-73``var valid []time.Time` 新分配改为原地双指针过滤,复用 `sw.tokens` 底层数组。 |
| **P1-5 IPv6 截断** | `limits.go:110-114` 将手动 `lastIndexByte(addr, ':')` 替换为 `net.SplitHostPort`,正确提取 IPv6 host补充 IPv6 单元测试。 |
| **P2-1 配置静默回退** | `config.go` 新增 `mustGetEnvInt` / `mustGetEnvBool`(解析失败返回 error`Load()` 生产模式下对关键数值型配置启用严格解析。 |
| **P2-2 worker 连接池** | `app.go:172` 将裸 `&http.Client{Timeout: ...}` 替换为显式配置 `Transport.MaxIdleConns` / `MaxIdleConnsPerHost` 的 client。 |
| **P2-3 测试回退** | `test/e2e/*_test.go``test/integration/*_test.go``TestMain` 或每个 `Test*` 开头增加 PostgreSQL 连通性检测,不通则 `t.Skip`。 |
| **P2-4 worker 优雅关闭** | `app.go` 在 startWorker 中引入 `sync.WaitGroup``wg.Add(1)` + goroutine `defer wg.Done()`closer 中 `cancel()` 后执行 `wg.Wait()`(带 5s 超时兜底)。 |
---
## 3. 任务拆解表
> 粒度约束:每个任务 2-5 分钟,必须有具体文件路径和函数名。
### 3.1 P0 — 阻塞级
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P0-1a** | 全仓库 | `git status` | 确认 dirty 文件清单,按 "docs" / "code" 分组 | 2 min |
| **P0-1b** | 全仓库 | `git commit` | 批次 1提交 12 个 docs 文件modified + untracked消息 `docs: sync review reports, runbooks, and checklists` | 3 min |
| **P0-1c** | 全仓库 | `git commit` | 批次 2提交 3 个 internal/ 文件;消息 `fix: platform event store and builder drift` | 3 min |
| **P0-1d** | 全仓库 | `git tag` | 打 tag `v0.9.1-pre`;推送 tag | 2 min |
| **P0-2a** | `Makefile:2` | `test` target | 将 `go test ./...` 改为 `go test ./... -count=1 -p 1` | 2 min |
### 3.2 P1 — 必须修复
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P1-1a** | `internal/http/handlers/ticket_handler_test.go` | `TestTicketHandlerList_Success` | 新增测试:向 `mockTicketService.tickets` Create 2 条 ticket调用 `h.List`,断言返回 200 且 `items` 数组长度为 2 | 3 min |
| **P1-1b** | `internal/http/handlers/ticket_handler_test.go` | `TestTicketHandlerList_ServiceError` | 新增测试:注入一个返回 error 的 `TicketService` mock或改 `ListOpen` 返回 `errors.New("db down")`),断言返回 500 且 error code 为 `CS_SYS_5002` | 3 min |
| **P1-2a** | `internal/platformadapter/newapi_adapter_test.go` | `TestNewAPIAdapter_BuildIngressAck_NilMeta` | 新建文件与测试:`adapter.BuildIngressAck(nil, nil)` 断言返回 `map[string]any{"accepted":false,"platform":"newapi"}` | 3 min |
| **P1-2b** | `internal/platformadapter/newapi_adapter_test.go` | `TestNewAPIAdapter_BuildIngressAck_WithMeta` | 同上文件:传入 `&PlatformInboundMeta{EventID:"evt-1"}`,断言返回包含 `event_id:"evt-1"` | 2 min |
| **P1-3a** | `docs/SECURITY_BOUNDARY.md` | — | 新建文档:明确标注 `internal/http/middleware/authz.go:RequireRoles` 仅做 RBAC 白名单校验,不验证 header 真实性;生产部署必须前置 API Gateway / JWT 验证 | 3 min |
| **P1-3b** | `internal/http/middleware/authz.go:42` | `RequireRoles` | 在函数注释头增加 `// SECURITY: This middleware trusts the upstream gateway to authenticate the actor headers.` | 2 min |
| **P1-4a** | `internal/platform/httpx/limits.go:67-73` | `RateLimiter.Allow` | 将 `var valid []time.Time` + `append` 循环改为原地双指针过滤:`n := 0; for _, t := range sw.tokens { if t.After(cutoff) { sw.tokens[n] = t; n++ } }; sw.tokens = sw.tokens[:n]` | 3 min |
| **P1-4b** | `internal/platform/httpx/limits_test.go` | 现有测试 | 运行 `go test -race ./internal/platform/httpx/...`,确认零 DATA RACE | 2 min |
| **P1-5a** | `internal/platform/httpx/limits.go:98-115` | `rateLimitKey` | 导入 `net`;将 `lastIndexByte(addr, ':')` 截断逻辑替换为 `host, _, err := net.SplitHostPort(addr)`err==nil 则返回 host否则返回原值 | 3 min |
| **P1-5b** | `internal/platform/httpx/limits.go:117-124` | `lastIndexByte` | 删除 `lastIndexByte` 函数(若已无其他引用) | 2 min |
| **P1-5c** | `internal/platform/httpx/limits_test.go` | `TestRateLimitKey_IPv6` | 新增测试:`rateLimitKey``req.RemoteAddr = "[::1]:8080"` 应返回 `"::1"`;对 `"[2001:db8::1]:8080"` 应返回 `"2001:db8::1"` | 3 min |
### 3.3 P2 — 建议修复
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P2-1a** | `internal/config/config.go:201-255` | `getEnvInt` / `getEnvBool` | 新增 `mustGetEnvInt(key string) (int, error)``mustGetEnvBool(key string) (bool, error)`:解析失败时返回 `fmt.Errorf` 而非静默 fallback | 3 min |
| **P2-1b** | `internal/config/config.go:66-148` | `Load()` | 生产模式下,对 `AI_CS_WEBHOOK_MAX_SKEW_SECONDS` 等关键数值配置若环境变量存在但解析失败,返回 error可选仅替换最危险的 2-3 个字段以控制范围) | 4 min |
| **P2-2a** | `internal/app/app.go:172` | `startWorker` 内 client 创建 | 将 `&http.Client{Timeout: ...}` 替换为 `&http.Client{Timeout: ..., Transport: &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 10}}` | 3 min |
| **P2-3a** | `test/e2e/*_test.go` | `TestMain` 或首个 Test | 增加 `pgCheck()`:尝试 `sql.Open("postgres", dsn).Ping()`,失败则 `t.Skip("PostgreSQL not available")` | 3 min |
| **P2-3b** | `test/integration/*_test.go` | `TestMain` 或首个 Test | 同上增加 skip 逻辑 | 3 min |
| **P2-4a** | `internal/app/app.go:158-188` | `startWorker` 闭包 | 在 `New` 函数内声明 `var workerWg sync.WaitGroup``startWorker``workerWg.Add(1)`goroutine 内 `defer workerWg.Done()``worker.Start(workerCtx)` | 3 min |
| **P2-4b** | `internal/app/app.go:164-167` | worker closer | 将 closer 改为:`cancel(); done := make(chan struct{}); go func() { workerWg.Wait(); close(done) }(); select { case <-done: return nil; case <-time.After(5 * time.Second): return fmt.Errorf("worker %s shutdown timeout", platform) }` | 4 min |
| **P2-4c** | `internal/app/app.go` | `import` | 确认新增 `sync``time`time 通常已有)导入 | 2 min |
---
## 4. 风险与保护
### 4.1 风险清单
| 风险 ID | 来源任务 | 风险描述 | 等级 |
|---|---|---|---|
| R-01 | P0-1b/c | 批量 commit 可能将未评审代码带入基线 | 🟡 中 |
| R-02 | P1-4a | 限流器原地过滤改动若索引越界,可能 panic 核心路径 | 🔴 高 |
| R-03 | P1-5a | `net.SplitHostPort` 对 IPv6 兼容但可能改变 IPv4 行为(实际不会,但需验证) | 🟡 中 |
| R-04 | P2-1b | 严格配置解析可能破坏现有开发/测试环境(如 `AI_CS_MAX_BODY_BYTES=1MB` 拼写错误导致启动失败) | 🟡 中 |
| R-05 | P2-4a/b | `sync.WaitGroup` 使用不当可能导致 `Shutdown` 死锁或 panic`wg.Add` 在 goroutine 启动后调用) | 🔴 高 |
| R-06 | 全量 | 任何代码改动引入 race condition | 🟡 中 |
### 4.2 降级策略
| 风险 ID | 降级策略 |
|---|---|
| R-01 | commit 前执行 `git diff --cached` 复核docs 与代码分开 commit一旦有问题可单独 revert。 |
| R-02 | ① 改前通读 `limits_test.go` 确保有覆盖;② 改后必跑 `go test -race ./internal/platform/httpx/...`;③ 若发现异常,立即回滚到 `var valid []time.Time` 方案。 |
| R-03 | 在 `limits_test.go` 中保留原有 IPv4 用例并追加 IPv6 用例;若 CI 失败,回滚到字符串处理但修复 IPv6 专用分支。 |
| R-04 | P2-1b 仅对生产模式 (`cfg.Runtime.Env == "production"`) 生效;开发/测试环境保持静默回退。若生产启动失败,工程师可立即切回旧 `getEnvInt`。 |
| R-05 | ① `wg.Add(1)` 必须紧接在 `go func()` 之前(同一线程);② closer 中 `wg.Wait()` 必须带 `time.After` 超时;③ 改后运行 `go test ./internal/app/...` 并手动发送 SIGTERM 验证无死锁。 |
| R-06 | 全量代码任务完成后统一执行 `go test -race ./internal/... -count=1 -p 1`;任何 race 报告阻塞合并。 |
---
## 5. QA 交接与实施约束
### 5.1 编码后漂移检查点QA 可验证)
| 检查点 ID | 验证命令 / 步骤 | 通过标准 |
|---|---|---|
| CP-01 | `cd /home/long/project/ai-customer-service && git status --short` | 零 modified / 零 untracked或仅有本次计划外的新 review 报告) |
| CP-02 | `make test` | 等价于 `go test ./... -count=1 -p 1`零失败postgres/e2e/integration skip 属于预期行为) |
| CP-03 | `go test -race ./internal/... -count=1 -p 1` | 24/24 pass零 DATA RACE |
| CP-04 | `go test ./internal/http/handlers/... -coverprofile=/tmp/handlers.out && go tool cover -func=/tmp/handlers.out \| grep ticket_handler.go` | `List` 函数覆盖率 > 0% |
| CP-05 | `go test ./internal/platformadapter/... -coverprofile=/tmp/pa.out && go tool cover -func=/tmp/pa.out \| grep newapi_adapter.go` | `BuildIngressAck` 覆盖率 > 0% |
| CP-06 | `go test ./internal/platform/httpx/...` | 全部通过,包括新增 IPv6 用例 |
| CP-07 | `ls docs/SECURITY_BOUNDARY.md && head -n 20 docs/SECURITY_BOUNDARY.md` | 文件存在,且首段包含 "RequireRoles" 和 "upstream gateway" 关键词 |
| CP-08 | `grep -n "sync.WaitGroup\|workerWg" internal/app/app.go` | 至少出现 `workerWg.Add(1)``defer workerWg.Done()``workerWg.Wait()` 三处 |
| CP-09 | `grep -n "MaxIdleConns" internal/app/app.go` | 出现 `MaxIdleConns``MaxIdleConnsPerHost` |
| CP-10 | `go vet ./...` | 零警告 |
### 5.2 必查真实调用链路
| 链路 | 验证方式 |
|---|---|
| **RateLimiter 核心路径** | `TestRateLimiter_WithRateLimit` 必须实际触发 `Allow` 并通过QA 可单步确认 `rateLimitKey` 返回预期值。 |
| **Authz 信任边界** | QA 手动阅读 `docs/SECURITY_BOUNDARY.md``authz.go` 注释,确认两者口径一致。 |
| **Worker Graceful Shutdown** | QA 本地启动服务后发送 SIGTERM`kill -TERM <pid>`),观察日志确认 worker 在 5s 内完成退出,无 `shutdown timeout` error。 |
| **Config 严格模式** | QA 设置 `AI_CS_RUNTIME_ENV=production` + `AI_CS_WEBHOOK_MAX_SKEW_SECONDS=not_a_number`,启动服务应报错并退出。 |
---
## 6. Engineer 实施说明
### 6.1 文件级落点
| 目标文件 | 落点行号 | 改动性质 |
|---|---|---|
| `Makefile` | 第 2 行 | 替换 |
| `internal/http/handlers/ticket_handler_test.go` | 文件末尾 | 追加 2 个 Test 函数 |
| `internal/platformadapter/newapi_adapter_test.go` | 新建 | 2 个 Test 函数 |
| `docs/SECURITY_BOUNDARY.md` | 新建 | Markdown 文档 |
| `internal/http/middleware/authz.go` | 第 42 行上方 | 添加注释块 |
| `internal/platform/httpx/limits.go` | 第 67-73 行 | 替换为原地过滤 |
| `internal/platform/httpx/limits.go` | 第 98-124 行 | 替换 `rateLimitKey` + 删除 `lastIndexByte` |
| `internal/platform/httpx/limits_test.go` | 文件末尾 | 追加 IPv6 Test 函数 |
| `internal/config/config.go` | 第 201-255 行之后 | 追加 `mustGetEnvInt` / `mustGetEnvBool` |
| `internal/config/config.go` | 第 66-148 行 | 条件性替换部分 `getEnvInt` 调用 |
| `internal/app/app.go` | 第 158-188 行 | 重构 startWorker 闭包 |
| `internal/app/app.go` | 第 172 行 | 替换 http.Client 创建 |
| `test/e2e/*_test.go` | 首个 Test 或 TestMain | 追加 skip 逻辑 |
| `test/integration/*_test.go` | 首个 Test 或 TestMain | 追加 skip 逻辑 |
### 6.2 最小验证项Engineer 每完成一个 P1/P2 任务必须自测)
1. `go build ./...` 零错误。
2. `go vet ./...` 零警告。
3. 涉及改动的包:`go test -race ./<changed_pkg>/...` 通过。
4. 若修改了 exported 函数签名,确认调用方编译通过。
---
## 7. 阶段门控结论
### 7.1 当前状态
- **设计完整性**:本方案已覆盖 review 报告全部 P0/P1/P2 项,任务粒度 <= 5 分钟,文件路径与函数名已精确锁定。
- **风险可控性**P1-4/P2-4 有较高风险,但已设计明确的降级策略(超时兜底 + race 检测 + 回滚路径)。
- **与旧 remediation board 兼容性**
- 本次 P1-2newapi_adapter 测试)与旧 board 的 I-01newapi 假接通)正交:本次仅补测试覆盖,不改变 newapi 仍为 501 占位的事实。
- 本次不涉及旧 board 的 D-01/D-02/D-03/D-04平台能力矩阵、callback_target 契约、outbox 并发 claim这些仍按旧 board 排期执行。
### 7.2 结论
**✅ 可进入 Engineer 实现。**
前提条件Engineer 必须严格按照本方案第 3 章的任务拆解顺序执行,禁止自行扩大范围(如顺带重写 newapi adapter 或引入 pgx
---
## 8. 下游执行约束摘要
### 8.1 Engineer 禁止偏离
- **禁止**在修复 P1-2 时顺带实现 newapi 完整 ingress 逻辑(仍保持 501 占位)。
- **禁止**改动 `lib/pq``pgx`
- **禁止**修改任何不属于本方案列出的文件,除非发现编译阻断。
- **禁止**跳过 `go test -race` 自测。
- P0-1 必须分 2 批 commitdocs / code禁止一次性混提交。
### 8.2 QA 必查链路
- `make test` 行为与 `-p 1` 一致性。
- `go test -race ./internal/... -count=1 -p 1` 零 race。
- ticket_handler.List 与 newapi_adapter.BuildIngressAck 的覆盖率从 0% 提升到 > 0%。
- `docs/SECURITY_BOUNDARY.md``authz.go` 注释口径一致。
- IPv6 rate limit key 的正确性(通过单元测试)。
- Worker graceful shutdown 的 SIGTERM 手动验证。
### 8.3 XLTechLead / 负责人)必补门控
- P0-1 commit 后复核 `git log --oneline -5``git status`,确认 worktree 已清。
- P0-1d 打 tag 后XL 必须亲自确认 tag 存在:`git describe --tags`
- 全量任务完成后XL 执行一次 `go test ./... -count=1 -p 1` 并留存输出截图/文本作为最终证据。
- 旧 remediation board 中与本方案无冲突的项D-01 ~ I-05继续保留不得因本次方案而关闭或删除。
---
## 自检清单(返回时显式列出打勾状态)
- [x] 架构设计覆盖 review 报告所有 P0/P1 项
- [x] 每个任务 < 5分钟有明确文件路径
- [x] 风险评估完整
- [x] 降级策略已设计
- [x] 实施漂移检测点已定义
- [x] 已明确标记是否可进入 Engineer 实现
- [x] 已给出 Engineer / QA / XL 的下游执行约束摘要