2026-04-15 18:42:06 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 19:12:27 +08:00
|
|
|
|
type runtimeTuning struct {
|
|
|
|
|
|
outboxStreamName string
|
|
|
|
|
|
outboxConsumerGroup string
|
|
|
|
|
|
idempotencyTTL time.Duration
|
|
|
|
|
|
partitionMaintenanceInterval time.Duration
|
|
|
|
|
|
compensationCheckInterval time.Duration
|
|
|
|
|
|
partitionedTables []string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 18:42:06 +08:00
|
|
|
|
// Runtime 聚合 HTTP 启动和后台任务启动所需的运行时依赖。
|
|
|
|
|
|
type Runtime struct {
|
|
|
|
|
|
env string
|
|
|
|
|
|
logger logging.Logger
|
|
|
|
|
|
now func() time.Time
|
2026-04-15 19:12:27 +08:00
|
|
|
|
tuning runtimeTuning
|
2026-04-15 18:42:06 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 18:42:06 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 20:14:59 +08:00
|
|
|
|
env, err := ResolveEnv(opts.Env)
|
2026-04-15 19:24:20 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-15 18:42:06 +08:00
|
|
|
|
now := opts.Now
|
|
|
|
|
|
if now == nil {
|
|
|
|
|
|
now = time.Now
|
|
|
|
|
|
}
|
|
|
|
|
|
initCtx := opts.InitContext
|
|
|
|
|
|
if initCtx == nil {
|
|
|
|
|
|
initCtx = context.Background()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isProd := env == "prod"
|
2026-04-15 19:12:27 +08:00
|
|
|
|
tuning := defaultRuntimeTuning()
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
db, err := factory.newDB(initCtx, opts.Config.Database)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if isProd {
|
|
|
|
|
|
return nil, fmt.Errorf("database unavailable: %w", err)
|
|
|
|
|
|
}
|
2026-04-15 19:12:27 +08:00
|
|
|
|
warnf(opts.Logger, "failed to connect to database: %v (using in-memory store)", err)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
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 {
|
2026-04-15 19:12:27 +08:00
|
|
|
|
warnf(opts.Logger, "redis unavailable at startup: %v", err)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
} else {
|
2026-04-15 19:12:27 +08:00
|
|
|
|
warnf(opts.Logger, "failed to connect to redis: %v (caching disabled)", err)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
redisCache = nil
|
|
|
|
|
|
} else if redisCache != nil {
|
|
|
|
|
|
infof(opts.Logger, "connected to redis at %s:%d", opts.Config.Redis.Host, opts.Config.Redis.Port)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
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 {
|
2026-04-15 18:42:06 +08:00
|
|
|
|
if db != nil {
|
2026-04-15 23:27:11 +08:00
|
|
|
|
bundle := buildDBStoreBundle(db)
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Info("审计存储: 使用PostgreSQL (DB-backed)", nil)
|
|
|
|
|
|
logger.Info("告警存储: 使用PostgreSQL (DB-backed)", nil)
|
|
|
|
|
|
logger.Info("外键校验器: 已初始化 (PostgreSQL-backed)", nil)
|
|
|
|
|
|
return bundle
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 23:27:11 +08:00
|
|
|
|
bundle := buildMemoryStoreBundle()
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Warn("审计存储使用内存实现 (生产环境不应使用)", nil)
|
|
|
|
|
|
logger.Warn("告警存储使用内存实现 (仅开发环境允许)", nil)
|
|
|
|
|
|
logger.Warn("外键校验器未启用 (db不可用)", nil)
|
|
|
|
|
|
return bundle
|
|
|
|
|
|
}
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
2026-04-15 23:27:11 +08:00
|
|
|
|
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()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
func buildSecurityBundle(
|
|
|
|
|
|
env string,
|
|
|
|
|
|
cfg *config.Config,
|
|
|
|
|
|
logger logging.Logger,
|
|
|
|
|
|
auditStore audit.AuditStore,
|
|
|
|
|
|
redisCache *cache.RedisCache,
|
|
|
|
|
|
tokenStatusRepo *repository.TokenStatusRepository,
|
|
|
|
|
|
) runtimeSecurityBundle {
|
2026-04-15 18:42:06 +08:00
|
|
|
|
tokenCache := middleware.NewTokenCache()
|
|
|
|
|
|
var tokenBackend middleware.TokenStatusBackend
|
|
|
|
|
|
var revocationSubscriber revocationSubscriber
|
2026-04-15 22:31:50 +08:00
|
|
|
|
|
2026-04-15 18:42:06 +08:00
|
|
|
|
if tokenStatusRepo != nil {
|
2026-04-15 22:31:50 +08:00
|
|
|
|
dbTokenBackend := middleware.NewDBTokenStatusBackend(tokenStatusRepo, redisCache, cfg.Token.RevocationCacheTTL)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
tokenBackend = dbTokenBackend
|
|
|
|
|
|
revocationSubscriber = dbTokenBackend
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Info("Token状态后端: 使用PostgreSQL (DB-backed)", nil)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
tokenBackend = adapter.NewMemoryTokenBackend()
|
2026-04-15 22:31:50 +08:00
|
|
|
|
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,
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
2026-04-15 22:31:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
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)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
var idempotencyMiddleware *middleware.IdempotencyMiddleware
|
2026-04-15 22:31:50 +08:00
|
|
|
|
if storeBundle.idempotencyRepo != nil {
|
|
|
|
|
|
idempotencyMiddleware = middleware.NewIdempotencyMiddleware(storeBundle.idempotencyRepo, middleware.IdempotencyConfig{
|
2026-04-15 19:12:27 +08:00
|
|
|
|
TTL: tuning.idempotencyTTL,
|
2026-04-15 18:42:06 +08:00
|
|
|
|
Enabled: env != "dev",
|
|
|
|
|
|
})
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Info("幂等中间件已启用(DB-backed)", nil)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if isProd {
|
2026-04-15 22:31:50 +08:00
|
|
|
|
return runtimeAPIBundle{}, errors.New("idempotency repository unavailable")
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Warn("幂等中间件未启用(db或repo不可用)- 需要幂等的写接口将返回 503", nil)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rateLimitConfig := middleware.DefaultRateLimitConfig()
|
|
|
|
|
|
rateLimitConfig.Enabled = env != "dev"
|
2026-04-15 22:31:50 +08:00
|
|
|
|
logger.Info("限流中间件已初始化", nil)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
supplyAPI, err := httpapi.NewSupplyAPI(
|
|
|
|
|
|
accountService,
|
|
|
|
|
|
packageService,
|
|
|
|
|
|
settlementService,
|
|
|
|
|
|
earningService,
|
|
|
|
|
|
idempotencyMiddleware,
|
2026-04-15 22:31:50 +08:00
|
|
|
|
storeBundle.auditStore,
|
|
|
|
|
|
storeBundle.fkValidator,
|
|
|
|
|
|
cfg.Server.DefaultSupplierID,
|
|
|
|
|
|
cfg.Server.StatementBaseURL,
|
2026-04-15 18:42:06 +08:00
|
|
|
|
now,
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
2026-04-15 22:31:50 +08:00
|
|
|
|
return runtimeAPIBundle{}, fmt.Errorf("failed to initialize supply api: %w", err)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
2026-04-15 22:31:50 +08:00
|
|
|
|
supplyAPI.SetWithdrawEnabled(cfg.Settlement.WithdrawEnabled)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
alertAPI, err := httpapi.NewAlertAPI(storeBundle.alertService)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
if err != nil {
|
2026-04-15 22:31:50 +08:00
|
|
|
|
return runtimeAPIBundle{}, fmt.Errorf("failed to initialize alert api: %w", err)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 22:31:50 +08:00
|
|
|
|
return runtimeAPIBundle{
|
|
|
|
|
|
supplyAPI: supplyAPI,
|
|
|
|
|
|
alertAPI: alertAPI,
|
|
|
|
|
|
rateLimitConfig: rateLimitConfig,
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 19:12:27 +08:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 18:42:06 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 20:14:59 +08:00
|
|
|
|
func ResolveEnv(env string) (string, error) {
|
2026-04-15 18:42:06 +08:00
|
|
|
|
normalized := strings.ToLower(strings.TrimSpace(env))
|
|
|
|
|
|
if normalized == "" {
|
2026-04-15 19:24:20 +08:00
|
|
|
|
return "dev", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
switch normalized {
|
|
|
|
|
|
case "dev", "staging", "prod":
|
|
|
|
|
|
return normalized, nil
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "", fmt.Errorf("unsupported env %q", env)
|
2026-04-15 18:42:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func infof(logger logging.Logger, format string, args ...any) {
|
|
|
|
|
|
logger.Info(fmt.Sprintf(format, args...), nil)
|
|
|
|
|
|
}
|
2026-04-15 19:12:27 +08:00
|
|
|
|
|
|
|
|
|
|
func warnf(logger logging.Logger, format string, args ...any) {
|
|
|
|
|
|
logger.Warn(fmt.Sprintf(format, args...), nil)
|
|
|
|
|
|
}
|