package app import ( "context" "errors" "fmt" "net/http" "strings" "time" "lijiaoqiao/supply-api/internal/adapter" "lijiaoqiao/supply-api/internal/audit" auditrepo "lijiaoqiao/supply-api/internal/audit/repository" auditservice "lijiaoqiao/supply-api/internal/audit/service" "lijiaoqiao/supply-api/internal/cache" "lijiaoqiao/supply-api/internal/config" "lijiaoqiao/supply-api/internal/domain" "lijiaoqiao/supply-api/internal/httpapi" "lijiaoqiao/supply-api/internal/middleware" "lijiaoqiao/supply-api/internal/pkg/logging" "lijiaoqiao/supply-api/internal/repository" ) // RuntimeOptions 定义构建 supply-api 运行时所需的输入。 type RuntimeOptions struct { Env string Config *config.Config Logger logging.Logger InitContext context.Context Now func() time.Time } type runtimeTuning struct { outboxStreamName string outboxConsumerGroup string idempotencyTTL time.Duration partitionMaintenanceInterval time.Duration compensationCheckInterval time.Duration partitionedTables []string } // Runtime 聚合 HTTP 启动和后台任务启动所需的运行时依赖。 type Runtime struct { env string logger logging.Logger now func() time.Time tuning runtimeTuning serverConfig config.ServerConfig db *repository.DB redisCache *cache.RedisCache supplyAPI *httpapi.SupplyAPI alertAPI *httpapi.AlertAPI authMiddleware *middleware.AuthMiddleware rateLimitConfig *middleware.RateLimitConfig revocationSubscriber revocationSubscriber } type revocationSubscriber interface { StartRevocationSubscriber(ctx context.Context) error } type runtimeFactory struct { newDB func(ctx context.Context, cfg config.DatabaseConfig) (*repository.DB, error) newRedisCache func(cfg config.RedisConfig) (*cache.RedisCache, error) } type runtimeStoreBundle struct { accountStore domain.AccountStore packageStore domain.PackageStore settlementStore domain.SettlementStore earningStore domain.EarningStore auditStore audit.AuditStore alertService *auditservice.AlertService fkValidator *repository.ForeignKeyValidator tokenStatusRepo *repository.TokenStatusRepository idempotencyRepo *repository.IdempotencyRepository } type runtimeSecurityBundle struct { authMiddleware *middleware.AuthMiddleware revocationSubscriber revocationSubscriber } type runtimeAPIBundle struct { supplyAPI *httpapi.SupplyAPI alertAPI *httpapi.AlertAPI rateLimitConfig *middleware.RateLimitConfig } // BuildRuntime 构建 supply-api 运行时依赖。 func BuildRuntime(opts RuntimeOptions) (*Runtime, error) { return buildRuntimeWithFactory(opts, runtimeFactory{ newDB: repository.NewDB, newRedisCache: cache.NewRedisCache, }) } func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runtime, error) { if opts.Config == nil { return nil, errors.New("config is required") } if opts.Logger == nil { return nil, errors.New("logger is required") } if factory.newDB == nil { factory.newDB = repository.NewDB } if factory.newRedisCache == nil { factory.newRedisCache = cache.NewRedisCache } env, err := ResolveEnv(opts.Env) if err != nil { return nil, err } now := opts.Now if now == nil { now = time.Now } initCtx := opts.InitContext if initCtx == nil { initCtx = context.Background() } isProd := env == "prod" tuning := defaultRuntimeTuning() db, err := factory.newDB(initCtx, opts.Config.Database) if err != nil { if isProd { return nil, fmt.Errorf("database unavailable: %w", err) } warnf(opts.Logger, "failed to connect to database: %v (using in-memory store)", err) db = nil } else if db != nil { infof(opts.Logger, "connected to database at %s:%d", opts.Config.Database.Host, opts.Config.Database.Port) } redisCache, err := factory.newRedisCache(opts.Config.Redis) if err != nil { if isProd { warnf(opts.Logger, "redis unavailable at startup: %v", err) } else { warnf(opts.Logger, "failed to connect to redis: %v (caching disabled)", err) } redisCache = nil } else if redisCache != nil { infof(opts.Logger, "connected to redis at %s:%d", opts.Config.Redis.Host, opts.Config.Redis.Port) } storeBundle := buildStoreBundle(db, opts.Logger) securityBundle := buildSecurityBundle(env, opts.Config, opts.Logger, storeBundle.auditStore, redisCache, storeBundle.tokenStatusRepo) apiBundle, err := buildAPIBundle(env, opts.Config, now, tuning, opts.Logger, isProd, storeBundle) if err != nil { return nil, err } return &Runtime{ env: env, logger: opts.Logger, now: now, tuning: tuning, serverConfig: normalizeServerConfig(opts.Config.Server), db: db, redisCache: redisCache, supplyAPI: apiBundle.supplyAPI, alertAPI: apiBundle.alertAPI, authMiddleware: securityBundle.authMiddleware, rateLimitConfig: apiBundle.rateLimitConfig, revocationSubscriber: securityBundle.revocationSubscriber, }, nil } func buildStoreBundle(db *repository.DB, logger logging.Logger) runtimeStoreBundle { if db != nil { bundle := buildDBStoreBundle(db) logger.Info("审计存储: 使用PostgreSQL (DB-backed)", nil) logger.Info("告警存储: 使用PostgreSQL (DB-backed)", nil) logger.Info("外键校验器: 已初始化 (PostgreSQL-backed)", nil) return bundle } 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, logger logging.Logger, auditStore audit.AuditStore, redisCache *cache.RedisCache, tokenStatusRepo *repository.TokenStatusRepository, ) runtimeSecurityBundle { tokenCache := middleware.NewTokenCache() var tokenBackend middleware.TokenStatusBackend var revocationSubscriber revocationSubscriber if tokenStatusRepo != nil { dbTokenBackend := middleware.NewDBTokenStatusBackend(tokenStatusRepo, redisCache, cfg.Token.RevocationCacheTTL) tokenBackend = dbTokenBackend revocationSubscriber = dbTokenBackend logger.Info("Token状态后端: 使用PostgreSQL (DB-backed)", nil) } else { tokenBackend = adapter.NewMemoryTokenBackend() logger.Warn("Token状态后端使用内存实现 (生产环境不应使用)", nil) } return runtimeSecurityBundle{ authMiddleware: middleware.NewAuthMiddleware(middleware.AuthConfig{ SecretKey: cfg.Token.SecretKey, PublicKey: cfg.Token.PublicKey, Algorithm: cfg.Token.Algorithm, Issuer: cfg.Token.Issuer, CacheTTL: cfg.Token.RevocationCacheTTL, Enabled: env != "dev", }, tokenCache, tokenBackend, adapter.NewAuditEmitterAdapter(auditStore)), revocationSubscriber: revocationSubscriber, } } func buildAPIBundle( env string, cfg *config.Config, now func() time.Time, tuning runtimeTuning, logger logging.Logger, isProd bool, storeBundle runtimeStoreBundle, ) (runtimeAPIBundle, error) { _ = domain.NewInvariantChecker(storeBundle.accountStore, storeBundle.packageStore, storeBundle.settlementStore) accountService := domain.NewAccountService(storeBundle.accountStore, storeBundle.auditStore) packageService := domain.NewPackageService(storeBundle.packageStore, storeBundle.accountStore, storeBundle.auditStore) settlementService := domain.NewSettlementService(storeBundle.settlementStore, storeBundle.earningStore, storeBundle.auditStore) earningService := domain.NewEarningService(storeBundle.earningStore) var idempotencyMiddleware *middleware.IdempotencyMiddleware if storeBundle.idempotencyRepo != nil { idempotencyMiddleware = middleware.NewIdempotencyMiddleware(storeBundle.idempotencyRepo, middleware.IdempotencyConfig{ TTL: tuning.idempotencyTTL, Enabled: env != "dev", }) logger.Info("幂等中间件已启用(DB-backed)", nil) } else { if isProd { return runtimeAPIBundle{}, errors.New("idempotency repository unavailable") } logger.Warn("幂等中间件未启用(db或repo不可用)- 需要幂等的写接口将返回 503", nil) } rateLimitConfig := middleware.DefaultRateLimitConfig() rateLimitConfig.Enabled = env != "dev" logger.Info("限流中间件已初始化", nil) supplyAPI, err := httpapi.NewSupplyAPI( accountService, packageService, settlementService, earningService, idempotencyMiddleware, storeBundle.auditStore, storeBundle.fkValidator, cfg.Server.DefaultSupplierID, cfg.Server.StatementBaseURL, now, ) if err != nil { return runtimeAPIBundle{}, fmt.Errorf("failed to initialize supply api: %w", err) } supplyAPI.SetWithdrawEnabled(cfg.Settlement.WithdrawEnabled) alertAPI, err := httpapi.NewAlertAPI(storeBundle.alertService) if err != nil { return runtimeAPIBundle{}, fmt.Errorf("failed to initialize alert api: %w", err) } return runtimeAPIBundle{ supplyAPI: supplyAPI, alertAPI: alertAPI, rateLimitConfig: rateLimitConfig, }, nil } func defaultRuntimeTuning() runtimeTuning { return runtimeTuning{ outboxStreamName: "supply:outbox:stream", outboxConsumerGroup: "outbox-processor", idempotencyTTL: 24 * time.Hour, partitionMaintenanceInterval: time.Hour, compensationCheckInterval: 5 * time.Minute, partitionedTables: []string{ "audit_events", "supply_usage_records", "supply_idempotency_records", }, } } // BuildServer 使用运行时依赖构建 HTTP server。 func (r *Runtime) BuildServer() (*http.Server, error) { if r == nil { return nil, errors.New("runtime is required") } var dbHealthCheck func(context.Context) error var redisHealthCheck func(context.Context) error if r.db != nil { dbHealthCheck = r.db.HealthCheck } if r.redisCache != nil { redisHealthCheck = r.redisCache.HealthCheck } return BuildServer(BuildServerOptions{ Env: r.env, ServerConfig: r.serverConfig, Logger: r.logger, SupplyAPI: r.supplyAPI, AlertAPI: r.alertAPI, AuthMiddleware: r.authMiddleware, RateLimitConfig: r.rateLimitConfig, DBHealthCheck: dbHealthCheck, RedisHealthCheck: redisHealthCheck, }) } // Close 关闭运行时持有的外部资源。 func (r *Runtime) Close() { if r == nil { return } if r.redisCache != nil { _ = r.redisCache.Close() } if r.db != nil { r.db.Close() } } // ShutdownTimeout 返回服务优雅关闭超时时间。 func (r *Runtime) ShutdownTimeout() time.Duration { if r == nil { return 0 } return r.serverConfig.ShutdownTimeout } func ResolveEnv(env string) (string, error) { normalized := strings.ToLower(strings.TrimSpace(env)) if normalized == "" { return "dev", nil } switch normalized { case "dev", "staging", "prod": return normalized, nil default: return "", fmt.Errorf("unsupported env %q", env) } } func infof(logger logging.Logger, format string, args ...any) { logger.Info(fmt.Sprintf(format, args...), nil) } func warnf(logger logging.Logger, format string, args ...any) { logger.Warn(fmt.Sprintf(format, args...), nil) }