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

327 lines
16 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.
# AI-Customer-Service 系统性代码审查报告
> 审查日期2026-05-11
> 审查范围:全仓库代码、构建、测试、安全、架构
> 审查人Hermes Agent (go-project-review + two-stage-review)
> 基线 commit`67922c5` (HEAD)
---
## 1. 项目规模总览
| 维度 | 数值 | 备注 |
|------|------|------|
| Go 源文件 | 109 | 含测试文件 |
| 生产代码行 | ~4,347 | 不含测试、注释、空行 |
| 测试代码行 | ~11,072 | 含 integration + e2e |
| SQL migration 行 | 130 | 3 个 migration 文件 |
| 模块依赖 | 2 | `github.com/google/uuid`, `github.com/lib/pq` |
| Go 版本 | 1.22 | 与 Dockerfile 一致 |
| 单服务架构 | 是 | `cmd/ai-customer-service` 单一入口 |
### 包级覆盖率internal/
| 包 | 覆盖率 | 状态 |
|----|--------|------|
| `internal/platform/health` | 100.0% | ✅ |
| `internal/platform/logging` | 100.0% | ✅ |
| `internal/service/handoff` | 100.0% | ✅ |
| `internal/service/intent` | 100.0% | ✅ |
| `internal/service/reply` | 100.0% | ✅ |
| `internal/domain/error/cserrors` | 97.2% | ✅ |
| `internal/http/middleware` | 92.0% | ✅ |
| `internal/service/dialog` | 88.5% | ✅ |
| `internal/store/memory` | 85.6% | ✅ |
| `internal/config` | 86.1% | ✅ |
| `internal/platform/httpx` | 83.3% | ✅ |
| `internal/http/handlers` | 79.9% | ⚠️ |
| `internal/http` | 80.2% | ⚠️ |
| `internal/service/platformevents` | 84.0% | ⚠️ |
| `internal/service/platformdelivery` | 65.2% | ⚠️ |
| `internal/platformadapter` | 66.1% | ⚠️ |
| `internal/app` | 64.6% | ⚠️ |
| `internal/domain/platformevent` | 58.8% | ⚠️ |
| `internal/store/postgres` | ~10.0% | ❌ 测试因缺少 DB 失败 |
| **internal/ 总覆盖率** | **63.4%** | ⚠️ |
---
## 2. 构建与测试验证
### 2.1 静态验证
| 检查项 | 结果 | 详情 |
|--------|------|------|
| `go build ./...` | ✅ 通过 | 零错误 |
| `go vet ./...` | ✅ 通过 | 零警告 |
| `go test -race ./internal/...` | ✅ 通过 | 零 DATA RACE |
| `go test ./internal/... -count=1 -p 1` | ✅ 24/24 通过 | 全部通过 |
| `go test ./... -count=1 -p 1` | ⚠️ 部分失败 | `postgres`, `e2e`, `integration` 因缺少本地 PostgreSQL端口 5434失败 |
### 2.2 测试环境缺口
- **postgres 包**21 个测试全部因 `dial tcp 127.0.0.1:5434: connection refused` 失败
- **e2e 包**2 个测试同样因缺少 PostgreSQL 失败
- **integration 包**1 个测试同样因缺少 PostgreSQL 失败
**判断**:这属于**环境前置条件缺失**,不是代码缺陷。但 CI 若未配置共享测试 DB则本地无法完整验证全链路。
---
## 3. 安全审查
### 3.1 SQL 注入风险
| 检查项 | 结果 |
|--------|------|
| `fmt.Sprintf.*SELECT/INSERT/UPDATE/DELETE` 危险模式 | ✅ 未发现 |
| `$1, $2...` 参数化查询占位符 | ✅ 29 处 |
| 字符串拼接 SQL | ✅ 未发现 |
**结论**PostgreSQL 层全面使用参数化查询SQL 注入风险为零。
### 3.2 硬编码凭证
| 检查项 | 结果 |
|--------|------|
| 代码中硬编码 password/secret/api_key | ✅ 未发现 |
| 配置读取 secret 的代码 | ✅ 仅有正常的 `getEnv("AI_CS_WEBHOOK_SECRET", "")` |
| 环境变量示例文件 | ⚠️ `.env.platform-adapters.example` 存在,但无真实值 |
### 3.3 敏感日志泄露
| 检查项 | 结果 |
|--------|------|
| log.*password/secret/token/key | ✅ 未发现 |
| audit 日志中是否记录敏感字段 | ✅ audit payload 仅记录 intent/reply/error_code 等业务字段 |
### 3.4 Webhook 安全机制
- **HMAC-SHA256** 签名验证 ✅
- **时间戳防重放**(默认 300 秒偏移窗口)✅
- **常量时间比较** `hmac.Equal`
- **请求体回置** `r.Body = io.NopCloser(bytes.NewReader(body))`
- **平台级秘钥隔离** Sub2API / NewAPI 各自独立 secret ✅
### 3.5 goroutine 安全
- `app.go:184` 启动 callback worker`go worker.Start(workerCtx)`
- worker 通过 `context.WithCancel` 管理生命周期
- `Shutdown` 方法中调用 `cancel()` 并通过 `workerClosers` 链式关闭
- **判断**:有明确的取消路径,无泄漏风险 ✅
### 3.6 Token / 密钥文件泄露
| 检查项 | 结果 |
|--------|------|
| `*.pem`, `*.key`, `token.txt` | ✅ 未发现 |
| `.env*` 文件 | ⚠️ 仅 `.env.platform-adapters.example`(示例值,无真实凭证)|
---
## 4. 代码质量与架构审查
### 4.1 架构分层A 级)
```
cmd/ → 启动入口main.go 干净,仅装配 + signal 处理)
internal/app/ → 依赖注入装配中心New 函数显式创建所有组件)
internal/domain/ → 纯领域模型(无外部依赖)
internal/service/ → 业务逻辑dialog/intent/reply/handoff/delivery/events
internal/http/ → HTTP 传输层handlers + middleware + router
internal/store/ → 持久化抽象memory + postgres 双实现)
internal/platform/ → 基础设施health/httpx/logging
```
**优点**
- `dialog.Service` 仅通过接口依赖(`SessionRepository`, `AuditRepository`, `TicketRepository`, `DedupRepository`, `IntentRecognizer`, `HandoffDecider`),可测试性高
- `app.go` 作为装配中心,清晰表达组件依赖图
- memory store 与 postgres store 可互换,支持 test/dev/prod 多环境
### 4.2 配置管理A- 级)
- 全量环境变量驱动,无配置文件硬编码
- `config.Load()` 包含 10+ 项校验规则:
- production 强制要求 Postgres + Webhook Secret
- 平台适配器启用时强制要求 Ingress Secret
- 正整数校验timeout、batch size、retry schedule
- **建议**`getEnvInt` / `getEnvBool` 在解析失败时静默回退到 fallback 值,这在生产环境可能导致预期外行为。建议对关键配置(如 DSN、secret增加 "解析失败即报错" 的严格模式。
### 4.3 数据库设计A 级)
- Migration 文件使用 Flyway 风格命名(`0001_init.up.sql`
- 约束完整CHECK 约束覆盖 channel、status、priority 枚举值
- 索引合理:`(channel, open_id)``(status, priority, created_at)``(session_id, created_at DESC)`
- 外键正确ON DELETE CASCADE / SET NULL
- Outbox 模式:平台事件通过 `cs_platform_event_outbox` + `cs_platform_event_dead_letters` 实现可靠投递
### 4.4 限流与防护B+ 级)
- `httpx.RateLimiter`:滑动窗口限流,基于 IP / X-Forwarded-For
- `http.MaxBytesReader`:请求体大小限制
- **P1 风险**RateLimiter 的 `Allow` 方法每次请求都重新分配 `valid` 切片(`var valid []time.Time`),在高并发下产生 GC 压力。建议改为原地过滤或环形缓冲区。
- **P2 风险**`clientIP``rateLimitKey` 中对 IPv6 地址的处理可能不正确IPv6 地址包含多个 `:``strings.LastIndex(addr, ":")` 会在第一个冒号处截断)。
### 4.5 认证授权B 级)
- Webhook 层HMAC 签名验证 ✅
- 内部 API 层:基于 Header 的 RBAC`X-CS-Actor-ID`, `X-CS-Actor-Role`
- **P1 风险**`RequireRoles` 中间件**不验证 Actor 身份真实性**,仅检查 header 存在性和角色是否在白名单。这在生产环境中需要配合反向代理(如 API Gateway的 JWT/OAuth 验证,否则存在 header 伪造风险。文档中需明确标注此信任边界。
### 4.6 错误处理A- 级)
- 统一的错误码体系(`cserrors`100%+ 覆盖率)
- 错误码分级:`CS_AUTH_403x``CS_REQ_400x``CS_SYS_500x``CS_SES_400x`
- audit 写入失败不阻断主流程(` _ = s.Audit.Add(...)`),符合 P0 质量标准
- **建议**:部分 `fmt.Errorf("db is nil")` 错误信息可以更丰富(如包含调用上下文)。
### 4.7 Graceful ShutdownA 级)
- 10 秒超时 `context.WithTimeout(context.Background(), 10*time.Second)`
- 关闭顺序SetReady(false) → SetLive(false) → Server.Shutdown → closers 链式调用
- worker cancel 包含在 closers 中
---
## 5. 已知问题清单
### P0 — 阻塞级
| # | 问题 | 位置 | 影响 | 建议 |
|---|------|------|------|------|
| P0-1 | **未提交改动规模过大** | 全仓库 | 评审边界模糊、回滚困难、CI 基线不稳定 | `git status` 显示 15+ 个 modified 文件 + 3 个 untracked 文件。建议立即收口,分批次提交并打 tag |
| P0-2 | **Makefile test 目标与 README/CI 不一致** | `Makefile:2` | 本地执行 `make test` 时并发跑测试,可能污染共享 PostgreSQL 测试库 | 将 `Makefile``go test ./...` 改为 `go test ./... -count=1 -p 1`,与 README 和 CI 保持一致 |
### P1 — 必须修复
| # | 问题 | 位置 | 影响 | 建议 |
|---|------|------|------|------|
| P1-1 | **ticket_handler.List 覆盖率 0%** | `internal/http/handlers/ticket_handler.go:33` | 列表接口无回归保护 | 补充单元测试或 integration 测试 |
| P1-2 | **newapi_adapter.BuildIngressAck 覆盖率 0%** | `internal/platformadapter/newapi_adapter.go:24` | NewAPI 占位逻辑无验证 | 补充 501 响应测试 |
| P1-3 | **Authz header 伪造风险未文档化** | `internal/http/middleware/authz.go` | 内部 API 若直接暴露可被绕过 | 在 `docs/SECURITY_BOUNDARY.md` 中明确标注RequireRoles 依赖上游网关做真实身份验证 |
| P1-4 | **RateLimiter GC 压力** | `internal/platform/httpx/limits.go:67` | 高并发下频繁分配切片 | 改为原地过滤或预分配环形缓冲区 |
| P1-5 | **IPv6 地址截断风险** | `internal/platform/httpx/limits.go:110-114` | IPv6 地址在 rate limit key 中被错误截断 | 使用 `net.SplitHostPort` 替代手动字符串操作 |
### P2 — 建议修复
| # | 问题 | 位置 | 影响 | 建议 |
|---|------|------|------|------|
| P2-1 | **配置解析失败静默回退** | `internal/config/config.go:201-255` | 拼写错误的环境变量值被静默忽略 | 对生产模式的关键配置增加解析失败报错 |
| P2-2 | **callback worker 无连接池限制** | `internal/app/app.go:172` | `&http.Client{Timeout: ...}` 使用默认连接池 | 显式配置 `Transport.MaxIdleConns``MaxIdleConnsPerHost` |
| P2-3 | **缺少 SQLite/内存测试回退** | `test/integration`, `test/e2e` | 无 PostgreSQL 时无法跑 integration/e2e | 引入 testcontainers-go 或 SQLite 内存模式作为测试回退 |
| P2-4 | **平台事件 worker 缺少优雅关闭等待** | `internal/app/app.go:164-188` | `cancel()` 后立即返回,不等待 worker goroutine 真正退出 | 使用 `sync.WaitGroup` 等待 worker 完成当前轮次 |
---
## 6. 与 spec 的合规性检查Stage 1
基于 `IMPLEMENTATION_PLAN.md``README.md` 的 spec 对照:
| Spec 项 | 实现状态 | 位置 |
|---------|----------|------|
| HTTP 服务启动 | ✅ 完成 | `cmd/ai-customer-service/main.go` |
| Health/Live/Ready | ✅ 完成 | `internal/platform/health`, `internal/http/handlers/health_handler.go` |
| Webhook 接收最小 JSON | ✅ 完成 | `internal/http/handlers/webhook_handler.go` |
| Session 管理(内存 + Postgres | ✅ 完成 | `internal/store/memory/session_store.go`, `internal/store/postgres/session_store.go` |
| Intent 识别(规则版) | ✅ 完成 | `internal/service/intent/service.go` |
| Reply 生成(规则版) | ✅ 完成 | `internal/service/reply/service.go` |
| Handoff 转人工 | ✅ 完成 | `internal/service/handoff/service.go` |
| Audit 日志 | ✅ 完成 | `internal/store/memory/audit_store.go`, `internal/store/postgres/audit_store.go` |
| PostgreSQL 持久化 | ✅ 完成 | `internal/store/postgres/` |
| Migration | ✅ 完成 | `db/migration/` |
| HMAC Webhook 安全校验 | ✅ 完成 | `internal/http/handlers/webhook_security.go` |
| Sub2API 平台适配 | ✅ 完成 | `internal/platformadapter/sub2api_adapter.go` |
| NewAPI 平台适配 | ⚠️ 501 占位 | `internal/platformadapter/newapi_adapter.go` |
| OpenAPI 占位文档 | ⚠️ 未找到 | `internal/openapi/` 目录为空? |
| Rate Limiting | ✅ 完成 | `internal/platform/httpx/limits.go` |
| Ticket 工作流assign/resolve/close | ✅ 完成 | `internal/store/postgres/ticket_workflow.go` |
| Platform Event Outbox + Dead Letter | ✅ 完成 | `internal/store/postgres/platform_event_store.go` |
| Callback Worker + Retry | ✅ 完成 | `internal/service/platformdelivery/worker.go` |
**OpenAPI 文档缺口**spec 要求 "生成最小 OpenAPI 占位文档",但 `internal/openapi/` 目录为空,需确认是否已迁移到 `docs/` 或其他位置。
---
## 7. 依赖健康度
| 依赖 | 版本 | 状态 | 备注 |
|------|------|------|------|
| `github.com/google/uuid` | v1.6.0 | ✅ 最新稳定 | — |
| `github.com/lib/pq` | v1.10.9 | ✅ 最新稳定 | 注意pgx v5 是更现代的替代,但 pq 仍被维护 |
**建议**`lib/pq` 已进入维护模式,官方推荐新项目使用 `pgx`。若未来需要连接池高级功能(如 statement cache、batch insert建议迁移到 `pgx/v5`
---
## 8. 交付风险
| 风险 | 等级 | 说明 |
|------|------|------|
| Dirty worktree | 🔴 高 | 15+ modified + 3 untracked未收口前不应视为可发布基线 |
| 测试环境依赖 | 🟡 中 | postgres/e2e/integration 测试需要本地 PostgreSQL 5434 端口 |
| Authz 信任边界 | 🟡 中 | RequireRoles 不验证身份真实性,需上游网关配合 |
| 覆盖率缺口 | 🟡 中 | ticket_handler.List、newapi_adapter、postgres 包覆盖不足 |
| 依赖维护 | 🟢 低 | lib/pq 长期维护模式,但当前无功能缺口 |
---
## 9. 结论与评级
### 综合评级B+(良好,有条件可进入下一阶段)
| 维度 | 评级 | 说明 |
|------|------|------|
| 架构设计 | A | 分层清晰,依赖注入完整,接口隔离到位 |
| 安全基线 | A- | SQL 注入零风险Webhook HMAC 完善Authz 信任边界需文档化 |
| 测试覆盖 | B | 核心业务逻辑覆盖良好,但 postgres 层和 e2e 受环境限制无法验证 |
| 代码质量 | B+ | 零 vet 警告,零 race命名规范错误码体系完整 |
| 交付就绪 | C+ | **Dirty worktree 严重**,未收口前不可视为可发布基线 |
### 下一步行动
1. **立即收口**:将当前 dirty worktree 分批次提交,核心代码与文档变更分开 commit
2. **修复 P0-2**:同步 Makefile test 目标与 README/CI 的 `-p 1` 约束
3. **补充 P1 项测试**ticket_handler.List、newapi_adapter 的 501 响应
4. **文档化 Authz 信任边界**:在 `docs/SECURITY_BOUNDARY.md` 中标注 RequireRoles 的前置条件
5. **评估 PostgreSQL 测试策略**:引入 testcontainers-go 或 CI 中预置测试 DB使 e2e/integration 可在本地完整运行
6. **OpenAPI 占位文档**:确认 `internal/openapi/` 是否仍需要补充,或已迁移至其他位置
---
## 附录:审查工具链输出
```bash
# 构建
cd /home/long/project/ai-customer-service && go build ./...
# → exit 0, 零输出
# 静态分析
cd /home/long/project/ai-customer-service && go vet ./...
# → exit 0, 零输出
# Race 检测
cd /home/long/project/ai-customer-service && go test -race ./internal/... -count=1 -p 1
# → 24/24 ok, 零 DATA RACE
# 覆盖率internal/
cd /home/long/project/ai-customer-service && go test ./internal/... -coverprofile=/tmp/ai_cs_cov.out -count=1 -p 1
go tool cover -func=/tmp/ai_cs_cov.out | grep "^total"
# → total: (statements) 63.4%
# SQL 注入扫描
grep -rn "fmt\.Sprintf.*SELECT\|fmt\.Sprintf.*INSERT\|fmt\.Sprintf.*UPDATE\|fmt\.Sprintf.*DELETE" internal/ --include="*.go" | grep -v _test.go
# → 零匹配
# 参数化查询验证
grep -rn "\$[0-9]" internal/store/postgres --include="*.go" | grep -v _test.go | wc -l
# → 29
# 硬编码凭证扫描
grep -rn "password.*=\|secret.*=\|api_key.*=\|token.*=.*\"" internal/ --include="*.go" | grep -v "_test.go\|testutil\|factory\|Config\|struct {"
# → 仅 webhook security 的配置读取代码,无硬编码
# Git 状态
git status --short
# → 15+ modified, 3 untracked
```