本次提交包含: - B-01: HTTP Server 添加超时配置 (ReadTimeout/WriteTimeout/IdleTimeout/MaxHeaderBytes) - 添加结构化日志包 internal/log/ (B-02 部分完成) - 添加 Review 报告文档 - 添加系统性修复方案文档 - 添加最佳实践审核报告文档 - 更新任务清单和执行板 测试验证: - TestServerHasTimeoutConfiguration 通过 关联文档: - docs/2026-06-01-SYSTEMATIC-REVIEW-REPORT.md - docs/2026-06-01-SYSTEMATIC-REPAIR-PLAN.md - docs/2026-06-01-BEST-PRACTICE-AUDIT-REPORT.md
20 KiB
sub2api-cn-relay-manager 系统性修复方案
基于 2026-06-01 严格生产级 Review 报告生成 目标: 修复 BLOCKER 和 HIGH 级别问题,达到生产就绪标准
📋 任务总览
| 级别 | 任务数 | 预计工时 | 负责人 |
|---|---|---|---|
| BLOCKER | 4 | 16h | 核心团队 |
| HIGH | 5 | 20h | 核心团队 |
| MEDIUM | 4 | 12h | 开发团队 |
| 合计 | 13 | 48h | - |
🚨 BLOCKER 级别任务(必须修复)
Task B-01: HTTP Server 添加超时配置
优先级: P0 | 工时: 4h | 阻塞: 是
问题描述
HTTP Server 未配置 ReadTimeout、WriteTimeout、IdleTimeout,生产环境可能遭遇 Slowloris 攻击,导致连接耗尽。
修复方案
步骤 1: 修改 internal/app/app.go
// 修改 NewServer 函数,添加超时配置
func NewServer(listenAddr string, handler http.Handler, listenerFactory ListenerFactory) *Server {
if handler == nil {
handler = NewAPIHandler("", ActionSet{})
}
server := &Server{
server: &http.Server{
Addr: listenAddr,
Handler: handler,
ReadTimeout: 30 * time.Second, // 请求读取超时
WriteTimeout: 30 * time.Second, // 响应写入超时
IdleTimeout: 120 * time.Second, // 连接空闲超时
MaxHeaderBytes: 1 << 20, // 1MB 请求头限制
},
listen: net.Listen,
}
if listenerFactory != nil {
server.listen = listenerFactory
}
return server
}
步骤 2: 验证测试
go test ./internal/app/... -v -run TestServer
验收标准
- HTTP Server 配置四项超时参数
- 集成测试通过
- 压力测试验证超时行为
Task B-02: 日志结构化改造
优先级: P0 | 工时: 6h | 阻塞: 是
问题描述
使用标准库 log 输出非结构化日志,生产环境难以解析、聚合和监控。
修复方案
步骤 1: 添加依赖
go get golang.org/x/exp/slog
步骤 2: 创建 internal/log/log.go
package log
import (
"os"
"golang.org/x/exp/slog"
)
var logger *slog.Logger
func Init() {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger = slog.New(handler)
slog.SetDefault(logger)
}
func InitWithLevel(level string) {
var slogLevel slog.Level
switch level {
case "DEBUG":
slogLevel = slog.LevelDebug
case "INFO":
slogLevel = slog.LevelInfo
case "WARN":
slogLevel = slog.LevelWarn
case "ERROR":
slogLevel = slog.LevelError
default:
slogLevel = slog.LevelInfo
}
opts := &slog.HandlerOptions{
Level: slogLevel,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger = slog.New(handler)
slog.SetDefault(logger)
}
// 包装方法
func Info(msg string, args ...interface{}) { slog.Info(msg, args...) }
func Error(msg string, args ...interface{}) { slog.Error(msg, args...) }
func Debug(msg string, args ...interface{}) { slog.Debug(msg, args...) }
func Warn(msg string, args ...interface{}) { slog.Warn(msg, args...) }
步骤 3: 修改 cmd/server/main.go
func main() {
// 初始化结构化日志
log.InitWithLevel(os.Getenv("LOG_LEVEL"))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
slog.Info("server starting", "addr", os.Getenv("SUB2API_CRM_LISTEN_ADDR"))
if err := run(ctx, app.Bootstrap, func(ctx context.Context, server *app.Server) error {
return server.Run(ctx)
}); err != nil {
slog.Error("server failed", "error", err)
os.Exit(1)
}
}
步骤 4: 替换全局 log 使用
- 替换
cmd/server/main.go中的log.Fatalf - 替换
cmd/cli/main.go中的log.Fatalf - 替换
internal/routing/logwriter.go:233中的log.Printf
验收标准
- 所有日志输出为 JSON 格式
- 支持
LOG_LEVEL环境变量控制 - 日志包含标准字段:timestamp, level, msg, 自定义字段
- 集成测试验证日志格式
Task B-03: 日志轮转配置
优先级: P0 | 工时: 4h | 阻塞: 是
问题描述
容器环境下日志无限增长,可能导致磁盘耗尽。
修复方案
选项 1: 使用外部日志收集(推荐) 配置 Docker/Kubernetes 使用外部日志收集器(如 Fluentd、Filebeat)。
选项 2: 代码层实现轮转
步骤 1: 添加依赖
go get gopkg.in/natefinch/lumberjack.v2
步骤 2: 修改 internal/log/log.go
package log
import (
"os"
"gopkg.in/natefinch/lumberjack.v2"
"golang.org/x/exp/slog"
)
func InitWithRotation(logFile string) {
// 日志轮转配置
lumberjackLogger := &lumberjack.Logger{
Filename: logFile,
MaxSize: 100, // MB
MaxBackups: 3,
MaxAge: 7, // 天
Compress: true,
}
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
}
handler := slog.NewJSONHandler(lumberjackLogger, opts)
logger := slog.New(handler)
slog.SetDefault(logger)
}
验收标准
- 单日志文件大小限制(100MB)
- 保留历史日志数量限制(3个)
- 历史日志压缩存储
- 支持日志清理(7天)
Task B-04: CI/CD 工作流配置
优先级: P0 | 工时: 4h | 阻塞: 是
问题描述
缺少 GitHub Actions 自动化测试和发布流程。
修复方案
步骤 1: 创建 .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.22.2']
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Cache Go modules
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Run go vet
run: go vet ./...
- name: Check formatting
run: |
if [ "$(gofmt -l . | wc -l)" -gt 0 ]; then
echo "Please run 'gofmt -w .' to fix formatting"
gofmt -d .
exit 1
fi
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Check coverage
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | awk '{if ($1 < 70) exit 1}'
- name: Run integration tests
run: go test ./tests/integration/... -v -count=1
- name: Build server
run: go build -o bin/server ./cmd/server
- name: Build CLI
run: go build -o bin/cli ./cmd/cli
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
fail_ci_if_error: true
docker:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: docker build -t sub2api-cn-relay-manager:latest .
- name: Test Docker image
run: |
docker run --rm sub2api-cn-relay-manager:latest --help
步骤 2: 创建 .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22.2'
- name: Build binaries
run: |
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=${{ github.ref_name }}" -o bin/server-linux-amd64 ./cmd/server
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=${{ github.ref_name }}" -o bin/cli-linux-amd64 ./cmd/cli
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.Version=${{ github.ref_name }}" -o bin/server-darwin-amd64 ./cmd/server
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.Version=${{ github.ref_name }}" -o bin/server-darwin-arm64 ./cmd/server
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: bin/*
draft: false
prerelease: false
- name: Build and push Docker image
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker build -t ghcr.io/${{ github.repository }}:${{ github.ref_name }} .
docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }}
验收标准
- CI 自动运行测试、覆盖率检查、格式化检查
- PR 必须通过 CI 才能合并
- Release 自动构建多平台二进制文件
- Docker 镜像自动构建并推送
🔴 HIGH 级别任务(建议修复)
Task H-01: 补充 testutil 测试
优先级: P1 | 工时: 3h
修复方案
创建 internal/testutil/sqlite_test.go
package testutil
import (
"testing"
"context"
)
func TestNewTestDB(t *testing.T) {
db := NewTestDB(t)
defer db.Close()
// 验证数据库连接
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
t.Fatalf("failed to ping test db: %v", err)
}
}
func TestNewTestDBWithMigrations(t *testing.T) {
db := NewTestDBWithMigrations(t)
defer db.Close()
// 验证迁移表存在
var count int
row := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table'")
if err := row.Scan(&count); err != nil {
t.Fatalf("failed to query tables: %v", err)
}
if count == 0 {
t.Error("expected at least one table after migrations")
}
}
Task H-02: 补充 migrations 测试
优先级: P1 | 工时: 4h
修复方案
创建 internal/store/migrations/migrations_test.go
package migrations
import (
"testing"
"database/sql"
"os"
"path/filepath"
)
func TestMigrationScripts(t *testing.T) {
// 创建临时数据库
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
defer db.Close()
// 运行所有迁移
if err := RunMigrations(db); err != nil {
t.Fatalf("failed to run migrations: %v", err)
}
// 验证关键表存在
tables := []string{
"hosts",
"packs",
"providers",
"import_runs",
"routes",
}
for _, table := range tables {
var name string
err := db.QueryRow(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
table,
).Scan(&name)
if err != nil {
t.Errorf("expected table %s to exist: %v", table, err)
}
}
}
func TestMigrationIdempotency(t *testing.T) {
// 验证多次运行迁移不会产生错误
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
defer db.Close()
// 第一次运行
if err := RunMigrations(db); err != nil {
t.Fatalf("first migration failed: %v", err)
}
// 第二次运行(幂等性)
if err := RunMigrations(db); err != nil {
t.Fatalf("second migration failed: %v", err)
}
}
Task H-03: 日志 flush 错误监控
优先级: P1 | 工时: 3h
修复方案
修改 internal/routing/logwriter.go
type LogWriter struct {
// ... existing fields
flushErrorCount int64 // 添加错误计数
flushErrorChan chan error // 错误通知通道
}
// Flush 方法添加错误处理
func (w *LogWriter) Flush() error {
// ... existing logic
if err != nil {
w.flushErrorCount++
// 记录到 Prometheus metrics
metrics.FlushErrors.Inc()
// 超过阈值告警
if w.flushErrorCount > 10 {
slog.Error("log flush error threshold exceeded",
"count", w.flushErrorCount,
"error", err)
}
return err
}
// 成功时重置计数
w.flushErrorCount = 0
return nil
}
Task H-04: Prometheus 指标暴露
优先级: P1 | 工时: 6h
修复方案
步骤 1: 添加依赖
go get github.com/prometheus/client_golang/prometheus
步骤 2: 创建 internal/metrics/metrics.go
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// HTTP 请求指标
HTTPRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "path", "status"},
)
HTTPRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
// 业务指标
ImportRunsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "import_runs_total",
Help: "Total import runs",
},
[]string{"status"},
)
ReconcileRunsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "reconcile_runs_total",
Help: "Total reconcile runs",
},
[]string{"status"},
)
// 日志 flush 错误
LogFlushErrors = promauto.NewCounter(
prometheus.CounterOpts{
Name: "log_flush_errors_total",
Help: "Total log flush errors",
},
)
)
步骤 3: 修改 internal/app/http_api.go
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// 在路由中添加 metrics 端点
func NewAPIHandler(...) http.Handler {
mux := http.NewServeMux()
// ... existing routes
// Prometheus metrics 端点
mux.Handle("GET /metrics", promhttp.Handler())
return mux
}
步骤 4: 添加中间件记录指标
func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 获取状态码
wrapped := &responseRecorder{ResponseWriter: w}
next.ServeHTTP(wrapped, r)
duration := time.Since(start).Seconds()
metrics.HTTPRequestsTotal.WithLabelValues(
r.Method,
r.URL.Path,
strconv.Itoa(wrapped.statusCode),
).Inc()
metrics.HTTPRequestDuration.WithLabelValues(
r.Method,
r.URL.Path,
).Observe(duration)
})
}
Task H-05: 移除 Dockerfile 默认值
优先级: P1 | 工时: 1h
修复方案
修改 Dockerfile
# 移除默认空值,强制启动时配置
ENV SUB2API_CRM_ADMIN_TOKEN=""
# 添加启动检查
COPY scripts/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
创建 scripts/docker-entrypoint.sh
#!/bin/bash
set -e
# 强制检查必需环境变量
if [ -z "$SUB2API_CRM_ADMIN_TOKEN" ]; then
echo "ERROR: SUB2API_CRM_ADMIN_TOKEN is required"
exit 1
fi
exec /usr/local/bin/sub2api-cn-relay-manager "$@"
🟡 MEDIUM 级别任务
Task M-01: 测试代码 panic 替换
优先级: P2 | 工时: 2h
修复位置:
internal/store/sqlite/packs_repo_test.go:208internal/store/sqlite/providers_repo_test.go:316
// 修改前
panic("unexpected QueryRowContext")
// 修改后
t.Fatalf("unexpected QueryRowContext")
Task M-02: 错误信息字符串匹配优化
优先级: P2 | 工时: 3h
方案: 定义领域错误类型,替代字符串匹配
// 创建 `internal/errors/errors.go`
package errors
import "errors"
var (
ErrProviderNotFound = errors.New("provider not found")
ErrPackNotFound = errors.New("pack not found")
ErrHostNotFound = errors.New("host not found")
ErrInvalidConfig = errors.New("invalid config")
)
// 使用 errors.Is 替代字符串匹配
if errors.Is(err, ErrProviderNotFound) {
// handle
}
Task M-03: 边界测试补充
优先级: P2 | 工时: 4h
针对包: internal/app 边界条件
// 示例:边界值测试
func TestCreateHostWithEmptyName(t *testing.T) {
req := CreateHostRequest{Name: ""}
// 验证返回错误
}
func TestCreateHostWithMaxLengthName(t *testing.T) {
req := CreateHostRequest{Name: strings.Repeat("a", 256)}
// 验证处理
}
Task M-04: 添加版本信息端点
优先级: P2 | 工时: 3h
修改 internal/app/http_api.go
var (
Version = "dev"
Commit = "unknown"
BuildTime = "unknown"
)
func init() {
mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"version": Version,
"commit": Commit,
"build_time": BuildTime,
})
})
}
修改 Makefile
LDFLAGS := -X internal/app.Version=$(VERSION) \
-X internal/app.Commit=$(COMMIT) \
-X internal/app.BuildTime=$(BUILD_TIME)
build:
go build -ldflags "$(LDFLAGS)" -o bin/server ./cmd/server
📅 执行计划
第一阶段:BLOCKER 修复(Week 1)
| 日期 | 任务 | 负责人 | 产出 |
|---|---|---|---|
| Day 1 | B-01 HTTP 超时配置 | Dev-1 | PR #1 |
| Day 2 | B-02 日志结构化 | Dev-2 | PR #2 |
| Day 3 | B-03 日志轮转 | Dev-2 | PR #3 |
| Day 4 | B-04 CI/CD 配置 | Dev-3 | PR #4 |
| Day 5 | BLOCKER 集成测试 | QA | 测试报告 |
第二阶段:HIGH 修复(Week 2)
| 日期 | 任务 | 负责人 | 产出 |
|---|---|---|---|
| Day 1 | H-01 testutil 测试 | Dev-1 | PR #5 |
| Day 2 | H-02 migrations 测试 | Dev-1 | PR #6 |
| Day 3 | H-03 日志 flush 监控 | Dev-2 | PR #7 |
| Day 4 | H-04 Prometheus | Dev-2 | PR #8 |
| Day 5 | H-05 Dockerfile | Dev-3 | PR #9 |
第三阶段:MEDIUM + 验收(Week 3)
| 日期 | 任务 | 负责人 | 产出 |
|---|---|---|---|
| Day 1-2 | M-01 ~ M-04 | Dev-1/2/3 | PR #10-13 |
| Day 3 | 全量回归测试 | QA | 测试报告 |
| Day 4 | 性能测试 | QA | 性能报告 |
| Day 5 | 生产就绪评审 | Team | 评审报告 |
✅ 验收标准
BLOCKER 完成标准
- HTTP Server 配置四项超时参数
- 所有日志输出为 JSON 格式
- 日志轮转配置生效
- CI/CD 自动化测试通过
HIGH 完成标准
- testutil 覆盖率 > 70%
- migrations 覆盖率 > 70%
- Prometheus 指标可查询
- Dockerfile 强制检查必需环境变量
生产就绪标准
- 综合评级从 B 提升到 A
- 所有测试通过
- 性能测试达标
- 安全扫描通过
📊 风险与应对
| 风险 | 概率 | 影响 | 应对策略 |
|---|---|---|---|
| 日志改造引入新问题 | 中 | 高 | 灰度发布,逐步替换 |
| CI/CD 配置复杂 | 低 | 中 | 使用成熟模板,分步实施 |
| Prometheus 性能影响 | 低 | 中 | 性能基准测试,监控开销 |
| 测试覆盖提升困难 | 中 | 中 | 优先覆盖核心路径 |
方案生成时间: 2026-06-01 方案版本: v1.0 基于 Review 报告: 2026-06-01-SYSTEMATIC-REVIEW-REPORT.md