Files
lijiaoqiao/supply-api/internal/app/runtime.go

403 lines
12 KiB
Go
Raw Normal View History

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)
}