From 39c4a11ff9ff7fa9d8a0e9d8d332327b6d7af9ac Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 15 Apr 2026 23:27:11 +0800 Subject: [PATCH] refactor(supply-api): split runtime store bundle builders --- ...4-15-supply-api-store-bundle-split-plan.md | 124 ++++++++++++++++++ supply-api/internal/app/runtime.go | 56 ++++---- supply-api/internal/app/runtime_test.go | 56 ++++++++ 3 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 docs/plans/2026-04-15-supply-api-store-bundle-split-plan.md diff --git a/docs/plans/2026-04-15-supply-api-store-bundle-split-plan.md b/docs/plans/2026-04-15-supply-api-store-bundle-split-plan.md new file mode 100644 index 00000000..1d12495e --- /dev/null +++ b/docs/plans/2026-04-15-supply-api-store-bundle-split-plan.md @@ -0,0 +1,124 @@ +# Supply API Store Bundle Split Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 把 `supply-api/internal/app/runtime.go` 中 `buildStoreBundle` 的 DB/in-memory 双分支拆成两个更小的 helper,降低单函数分支复杂度并保持现有语义不变。 + +**Architecture:** 保留 `buildStoreBundle` 作为总入口,只负责根据 `db` 是否存在分发到 `buildDBStoreBundle` 与 `buildMemoryStoreBundle`。先补 helper 级失败测试,再复用现有 `buildStoreBundle` 回归测试验证委派后的行为不变。 + +**Tech Stack:** Go, Go test + +--- + +### Task 1: 提取 DB-backed store helper + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Write the failing test** + +```go +func TestBuildDBStoreBundle_InitializesDatabaseOnlyDependencies(t *testing.T) { + bundle := buildDBStoreBundle(&repository.DB{}) + if bundle.tokenStatusRepo == nil { + t.Fatal("expected token status repo") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildDBStoreBundle_InitializesDatabaseOnlyDependencies' -v` +Expected: FAIL,因为 helper 尚不存在 + +**Step 3: Write minimal implementation** + +```go +func buildDBStoreBundle(db *repository.DB) runtimeStoreBundle { ... } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildDBStoreBundle_InitializesDatabaseOnlyDependencies' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): extract db store bundle builder" +``` + +### Task 2: 提取内存 store helper + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Write the failing test** + +```go +func TestBuildMemoryStoreBundle_DisablesDatabaseOnlyDependencies(t *testing.T) { + bundle := buildMemoryStoreBundle() + if bundle.idempotencyRepo != nil { + t.Fatal("expected nil idempotency repo") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildMemoryStoreBundle_DisablesDatabaseOnlyDependencies' -v` +Expected: FAIL,因为 helper 尚不存在 + +**Step 3: Write minimal implementation** + +```go +func buildMemoryStoreBundle() runtimeStoreBundle { ... } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildMemoryStoreBundle_DisablesDatabaseOnlyDependencies' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): extract memory store bundle builder" +``` + +### Task 3: 回归验证与收尾 + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Verify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Run focused tests** + +Run: `cd "supply-api" && go test ./internal/app -run 'Test(Build(DB|Memory)StoreBundle_.*|BuildStoreBundle_.*)' -v` +Expected: PASS + +**Step 2: Run package regression** + +Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi` +Expected: PASS + +**Step 3: Run repo exit verification** + +Run: `bash "scripts/ci/repo_integrity_check.sh"` +Expected: PASS + +**Step 4: Check formatting** + +Run: `git diff --check` +Expected: no output + +**Step 5: Commit** + +```bash +git add docs/plans/2026-04-15-supply-api-store-bundle-split-plan.md supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): split runtime store bundle builders" +``` diff --git a/supply-api/internal/app/runtime.go b/supply-api/internal/app/runtime.go index ebbf48bf..de378077 100644 --- a/supply-api/internal/app/runtime.go +++ b/supply-api/internal/app/runtime.go @@ -173,43 +173,51 @@ func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runt } func buildStoreBundle(db *repository.DB, logger logging.Logger) runtimeStoreBundle { - var bundle runtimeStoreBundle - if db != nil { - accountRepo := repository.NewAccountRepository(db.Pool) - packageRepo := repository.NewPackageRepository(db.Pool) - settlementRepo := repository.NewSettlementRepository(db.Pool) - usageRepo := repository.NewUsageRepository(db.Pool) - - bundle.idempotencyRepo = repository.NewIdempotencyRepository(db.Pool) - bundle.tokenStatusRepo = repository.NewTokenStatusRepository(db.Pool) - bundle.accountStore = adapter.NewDBAccountStore(accountRepo) - bundle.packageStore = adapter.NewDBPackageStore(packageRepo) - bundle.settlementStore = adapter.NewDBSettlementStore(settlementRepo, accountRepo, db.Pool) - bundle.earningStore = adapter.NewDBEarningStore(usageRepo) - bundle.auditStore = audit.NewPostgresAuditStore(auditrepo.NewPostgresAuditRepository(db.Pool)) - bundle.alertService = auditservice.NewAlertService(auditrepo.NewPostgresAlertRepository(db.Pool)) - bundle.fkValidator = repository.NewForeignKeyValidator(db.Pool) - + bundle := buildDBStoreBundle(db) logger.Info("审计存储: 使用PostgreSQL (DB-backed)", nil) logger.Info("告警存储: 使用PostgreSQL (DB-backed)", nil) logger.Info("外键校验器: 已初始化 (PostgreSQL-backed)", nil) return bundle } - bundle.accountStore = adapter.NewInMemoryAccountStoreAdapter() - bundle.packageStore = adapter.NewInMemoryPackageStoreAdapter() - bundle.settlementStore = adapter.NewInMemorySettlementStoreAdapter() - bundle.earningStore = adapter.NewInMemoryEarningStoreAdapter() - bundle.auditStore = audit.NewMemoryAuditStore() - bundle.alertService = auditservice.NewAlertService(auditservice.NewInMemoryAlertStore()) - + bundle := buildMemoryStoreBundle() logger.Warn("审计存储使用内存实现 (生产环境不应使用)", nil) logger.Warn("告警存储使用内存实现 (仅开发环境允许)", nil) logger.Warn("外键校验器未启用 (db不可用)", nil) return bundle } +func buildDBStoreBundle(db *repository.DB) runtimeStoreBundle { + accountRepo := repository.NewAccountRepository(db.Pool) + packageRepo := repository.NewPackageRepository(db.Pool) + settlementRepo := repository.NewSettlementRepository(db.Pool) + usageRepo := repository.NewUsageRepository(db.Pool) + + return runtimeStoreBundle{ + accountStore: adapter.NewDBAccountStore(accountRepo), + packageStore: adapter.NewDBPackageStore(packageRepo), + settlementStore: adapter.NewDBSettlementStore(settlementRepo, accountRepo, db.Pool), + earningStore: adapter.NewDBEarningStore(usageRepo), + auditStore: audit.NewPostgresAuditStore(auditrepo.NewPostgresAuditRepository(db.Pool)), + alertService: auditservice.NewAlertService(auditrepo.NewPostgresAlertRepository(db.Pool)), + fkValidator: repository.NewForeignKeyValidator(db.Pool), + tokenStatusRepo: repository.NewTokenStatusRepository(db.Pool), + idempotencyRepo: repository.NewIdempotencyRepository(db.Pool), + } +} + +func buildMemoryStoreBundle() runtimeStoreBundle { + return runtimeStoreBundle{ + accountStore: adapter.NewInMemoryAccountStoreAdapter(), + packageStore: adapter.NewInMemoryPackageStoreAdapter(), + settlementStore: adapter.NewInMemorySettlementStoreAdapter(), + earningStore: adapter.NewInMemoryEarningStoreAdapter(), + auditStore: audit.NewMemoryAuditStore(), + alertService: auditservice.NewAlertService(auditservice.NewInMemoryAlertStore()), + } +} + func buildSecurityBundle( env string, cfg *config.Config, diff --git a/supply-api/internal/app/runtime_test.go b/supply-api/internal/app/runtime_test.go index db668813..a579ec0e 100644 --- a/supply-api/internal/app/runtime_test.go +++ b/supply-api/internal/app/runtime_test.go @@ -93,6 +93,34 @@ func TestBuildStoreBundle_UsesInMemoryStoresWithoutDatabase(t *testing.T) { } } +func TestBuildMemoryStoreBundle_DisablesDatabaseOnlyDependencies(t *testing.T) { + bundle := buildMemoryStoreBundle() + if bundle.accountStore == nil { + t.Fatal("expected account store") + } + if bundle.packageStore == nil { + t.Fatal("expected package store") + } + if bundle.settlementStore == nil { + t.Fatal("expected settlement store") + } + if bundle.earningStore == nil { + t.Fatal("expected earning store") + } + if bundle.auditStore == nil { + t.Fatal("expected audit store") + } + if bundle.alertService == nil { + t.Fatal("expected alert service") + } + if bundle.tokenStatusRepo != nil { + t.Fatal("expected nil token status repo") + } + if bundle.idempotencyRepo != nil { + t.Fatal("expected nil idempotency repo") + } +} + func TestBuildStoreBundle_UsesDatabaseBackedStoresWithDatabase(t *testing.T) { bundle := buildStoreBundle(&repository.DB{}, testLogger{}) if bundle.accountStore == nil { @@ -121,6 +149,34 @@ func TestBuildStoreBundle_UsesDatabaseBackedStoresWithDatabase(t *testing.T) { } } +func TestBuildDBStoreBundle_InitializesDatabaseOnlyDependencies(t *testing.T) { + bundle := buildDBStoreBundle(&repository.DB{}) + if bundle.accountStore == nil { + t.Fatal("expected account store") + } + if bundle.packageStore == nil { + t.Fatal("expected package store") + } + if bundle.settlementStore == nil { + t.Fatal("expected settlement store") + } + if bundle.earningStore == nil { + t.Fatal("expected earning store") + } + if bundle.auditStore == nil { + t.Fatal("expected audit store") + } + if bundle.alertService == nil { + t.Fatal("expected alert service") + } + if bundle.tokenStatusRepo == nil { + t.Fatal("expected token status repo") + } + if bundle.idempotencyRepo == nil { + t.Fatal("expected idempotency repo") + } +} + func TestBuildSecurityBundle_UsesMemoryTokenBackendWithoutRepository(t *testing.T) { security := buildSecurityBundle("dev", testRuntimeConfig(), testLogger{}, audit.NewMemoryAuditStore(), nil, nil) if security.authMiddleware == nil {