package main import ( "context" "log" "time" "affiliate_dash/internal/config" "affiliate_dash/internal/database" "affiliate_dash/internal/handler" "affiliate_dash/internal/pkg/applog" "affiliate_dash/internal/pkg/jwt" "affiliate_dash/internal/pkg/lewan" "affiliate_dash/internal/pkg/openlog" "affiliate_dash/internal/pkg/timeutil" "affiliate_dash/internal/router" "affiliate_dash/internal/service" "github.com/gin-gonic/gin" "gorm.io/driver/postgres" "gorm.io/gorm" ) // secondsToDurations 把秒数列表转成 time.Duration 列表(回调重试序列)。 func secondsToDurations(seconds []int) []time.Duration { if len(seconds) == 0 { return nil } out := make([]time.Duration, 0, len(seconds)) for _, s := range seconds { out = append(out, time.Duration(s)*time.Second) } return out } func main() { cfg := config.Load() time.Local = timeutil.Location() // 日志:控制台 + 文件 applog.SetRetainDays(cfg.LogRetainDays) applog.SetLevel(cfg.LogLevel) logFile, err := applog.Setup(cfg.LogFile) if err != nil { log.Fatalf("setup log: %v", err) } if logFile != nil { defer logFile.Close() } gin.SetMode(cfg.Mode) // 密钥配置体检:让「示例密钥被带到真实部署」这类问题在启动日志里立刻可见。 for _, warning := range cfg.SecurityWarnings() { applog.Warnf("[CONFIG] 安全告警: %s", warning) } db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{ Logger: applog.NewGormLogger(cfg.Mode), }) if err != nil { log.Fatalf("open db: %v", err) } if err := database.Migrate(db); err != nil { log.Fatalf("migrate: %v", err) } jm := jwt.NewManager(cfg.JWTSecret) codec, err := service.NewSecretCodec(cfg.DataEncryptionKey) if err != nil { log.Fatalf("setup secret codec: %v", err) } tenantSvc := service.NewTenantService(db) authSvc := service.NewAuthService(db, jm, tenantSvc) userSvc := service.NewUserService(db, tenantSvc) callbackSvc := service.NewCallbackService(db, codec, service.WithCallbackConfig(service.CallbackConfig{ MaxAttempts: cfg.CallbackMaxAttempts, PushTimeout: time.Duration(cfg.CallbackPushTimeoutSeconds) * time.Second, RetrySchedule: secondsToDurations(cfg.CallbackRetryScheduleSeconds), })) fulfillmentSvc := service.NewFulfillmentService(db, callbackSvc) merchantSvc := service.NewMerchantService(db, codec, tenantSvc) rechargeSvc := service.NewRechargeService(db, fulfillmentSvc, cfg.UploadDir) deliverySvc := service.NewDeliveryService( fulfillmentSvc, cfg.DeliveryBFFBaseURL, cfg.DeliveryChannel, cfg.DeliveryBaseURL, cfg.DeliveryLinkSecret, cfg.DeliveryLinkTTLMinutes, ) // 乐玩开放平台客户端与履约提供商注册 lewanClient := lewan.NewClient(lewan.Config{ BaseURL: cfg.LewanBaseURL, AccessKey: cfg.LewanAccessKey, SecretKey: cfg.LewanSecretKey, ApplicationID: cfg.LewanApplicationID, WebhookSecret: cfg.LewanWebhookSecret, }, nil) lewanProvider := service.NewLewanProvider(lewanClient) deliverySvc.RegisterProvider(lewanProvider) // Rock 平台 (0666.pro) 客户端、服务与履约提供商注册 rockClient := service.NewRockClient("https://0666.pro") rockSvc := service.NewRockService(db, rockClient) rockProvider := service.NewRockProvider(rockSvc) deliverySvc.RegisterProvider(rockProvider) fulfillmentSvc.SetUpstreamCancel(deliverySvc.CancelUpstream) fulfillmentSvc.SetUpstreamSync(deliverySvc.SyncDeliveringOrder) openlog.Init(cfg.OpenAPIDebug) h := &router.Handlers{ Auth: handler.NewAuthHandler(authSvc), Dashboard: handler.NewDashboardHandler(fulfillmentSvc), Delivery: handler.NewDeliveryHandler(deliverySvc), User: handler.NewUserHandler(userSvc), Open: handler.NewOpenV1Handler(merchantSvc, fulfillmentSvc, deliverySvc), SourceOpen: handler.NewOpenHandler(fulfillmentSvc), Merchant: handler.NewMerchantHandler(merchantSvc, fulfillmentSvc, callbackSvc, deliverySvc, rechargeSvc), Recharge: handler.NewRechargeHandler(rechargeSvc), JWT: jm, Tenant: tenantSvc, OpenDB: db, SecretCodec: codec, OpenAPIKey: cfg.OpenAPIKey, OpenAPISecret: cfg.OpenAPISecret, OpenSignSkew: cfg.OpenSignSkew, OpenAPIDebug: cfg.OpenAPIDebug, UploadDir: cfg.UploadDir, CORSAllowedOrigins: cfg.CORSAllowedOrigins, LewanWebhook: handler.NewLewanWebhookHandler(fulfillmentSvc, db, cfg.LewanWebhookSecret, cfg.LewanApplicationID), PlatformLewan: handler.NewPlatformLewanHandler(merchantSvc, lewanClient), Rock: handler.NewRockHandler(rockSvc), } go callbackSvc.Run(context.Background()) go fulfillmentSvc.RunProcessingTimeoutMonitor( context.Background(), time.Duration(cfg.FulfillmentProcessingTimeoutMinutes)*time.Minute, time.Duration(cfg.FulfillmentTimeoutScanIntervalSeconds)*time.Second, ) // 定时巡检 Rock 库存低水位与自动补货(每5分钟) go func() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { rockSvc.CheckStockAndAutoPurchase(context.Background()) } }() r := router.Setup(h) addr := ":" + cfg.Port applog.Infof("游戏皮肤供货平台 API 启动: http://localhost%s (日志级别: %s)", addr, applog.GetLevel()) applog.Infof("数据库: PostgreSQL") applog.Infof("开放接口鉴权: X-App-Key + X-Timestamp + X-Nonce + X-Sign (HMAC-SHA256)") if cfg.OpenAPIDebug { applog.Infof("开放接口调试日志: 开启 (OPEN_API_DEBUG=0 可关闭)") } if cfg.LogFile != "" { applog.Infof("日志文件: %s (同时输出控制台)", cfg.LogFile) } if err := r.Run(addr); err != nil { log.Fatalf("server: %v", err) } }