Files
ai-customer-service/docs/REMEDIATION_PLAN_2026-05-11.md

17 KiB
Raw Blame History

ai-customer-service 生产上线修复方案与技术任务拆解

生成日期2026-05-11
基线 commit67922c5 (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/pqreview 报告建议项,但当前无功能缺口,不属于阻塞问题。
  • 不实现 newapi 完整 ingress 逻辑:旧 remediation boardD-01/I-01已覆盖该设计缺口本次仅补充 BuildIngressAck 的单元测试(与 review P1-2 对应),不改变 newapi 仍为 501 占位的事实。
  • 不引入 testcontainers-goP2-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-73var 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(解析失败返回 errorLoad() 生产模式下对关键数值型配置启用严格解析。
P2-2 worker 连接池 app.go:172 将裸 &http.Client{Timeout: ...} 替换为显式配置 Transport.MaxIdleConns / MaxIdleConnsPerHost 的 client。
P2-3 测试回退 test/e2e/*_test.gotest/integration/*_test.goTestMain 或每个 Test* 开头增加 PostgreSQL 连通性检测,不通则 t.Skip
P2-4 worker 优雅关闭 app.go 在 startWorker 中引入 sync.WaitGroupwg.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 新增测试:rateLimitKeyreq.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.WaitGroupstartWorkerworkerWg.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 确认新增 synctimetime 通常已有)导入 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 死锁或 panicwg.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 出现 MaxIdleConnsMaxIdleConnsPerHost
CP-10 go vet ./... 零警告

5.2 必查真实调用链路

链路 验证方式
RateLimiter 核心路径 TestRateLimiter_WithRateLimit 必须实际触发 Allow 并通过QA 可单步确认 rateLimitKey 返回预期值。
Authz 信任边界 QA 手动阅读 docs/SECURITY_BOUNDARY.mdauthz.go 注释,确认两者口径一致。
Worker Graceful Shutdown QA 本地启动服务后发送 SIGTERMkill -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/pqpgx
  • 禁止修改任何不属于本方案列出的文件,除非发现编译阻断。
  • 禁止跳过 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.mdauthz.go 注释口径一致。
  • IPv6 rate limit key 的正确性(通过单元测试)。
  • Worker graceful shutdown 的 SIGTERM 手动验证。

8.3 XLTechLead / 负责人)必补门控

  • P0-1 commit 后复核 git log --oneline -5git status,确认 worktree 已清。
  • P0-1d 打 tag 后XL 必须亲自确认 tag 存在:git describe --tags
  • 全量任务完成后XL 执行一次 go test ./... -count=1 -p 1 并留存输出截图/文本作为最终证据。
  • 旧 remediation board 中与本方案无冲突的项D-01 ~ I-05继续保留不得因本次方案而关闭或删除。

自检清单(返回时显式列出打勾状态)

  • 架构设计覆盖 review 报告所有 P0/P1 项
  • 每个任务 < 5分钟有明确文件路径
  • 风险评估完整
  • 降级策略已设计
  • 实施漂移检测点已定义
  • 已明确标记是否可进入 Engineer 实现
  • 已给出 Engineer / QA / XL 的下游执行约束摘要