// oci-portal 后端服务启动入口。 package main import ( "context" "flag" "fmt" "log" "net" "net/http" "os" "os/signal" "syscall" "time" // 纯 Go 时区数据(+~450KB):distroless/scratch 容器无 tzdata, // 嵌入后 TZ 环境变量即可让 cron 按本地时区解释 _ "time/tzdata" "github.com/gin-gonic/gin" "gorm.io/gorm" // swag 生成的 OpenAPI spec(init 时注册,SWAGGER=1 的 /swagger 路由消费) _ "oci-portal/docs" "oci-portal/internal/api" "oci-portal/internal/config" "oci-portal/internal/crypto" "oci-portal/internal/database" "oci-portal/internal/oci" "oci-portal/internal/service" ) // @title OCI Portal API // @version 0.8.2 // @description 自托管 OCI 多租户管理面板 API。业务接口用 JWT(Bearer);AI 网关端点(/ai/v1/*)用独立网关密钥(Authorization: Bearer 或 x-api-key)。 // @BasePath / // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization // @description 登录接口返回的 JWT,格式: Bearer func init() { if os.Getenv("GIN_MODE") == "" { gin.SetMode(gin.ReleaseMode) } } func main() { healthcheck := flag.Bool("healthcheck", false, "探活本机服务后退出:健康 0,异常 1(容器 HEALTHCHECK 用)") flag.Parse() if *healthcheck { os.Exit(runHealthcheck()) } if err := run(); err != nil { log.Fatal(err) } } // runHealthcheck 请求本机 HTTP 端口:能响应且非 5xx 视为健康。 // distroless 镜像无 shell/wget,靠二进制自检 flag 支撑 HEALTHCHECK。 func runHealthcheck() int { addr := os.Getenv("ADDR") if addr == "" { addr = ":8080" } _, port, err := net.SplitHostPort(addr) if err != nil { fmt.Fprintf(os.Stderr, "healthcheck: bad ADDR %q: %v\n", addr, err) return 1 } client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Get("http://127.0.0.1:" + port + "/") if err != nil { fmt.Fprintf(os.Stderr, "healthcheck: %v\n", err) return 1 } defer resp.Body.Close() if resp.StatusCode >= http.StatusInternalServerError { fmt.Fprintf(os.Stderr, "healthcheck: status %d\n", resp.StatusCode) return 1 } return 0 } // bootstrap 完成配置加载、数据库打开与加密组件构建。 func bootstrap() (*config.Config, *gorm.DB, *crypto.Cipher, error) { cfg, err := config.Load() if err != nil { return nil, nil, nil, err } db, err := database.Open(cfg.DBDriver, cfg.DBDSN, cfg.DBPath) if err != nil { return nil, nil, nil, err } cipher, err := crypto.NewCipher(cfg.DataKey) if err != nil { return nil, nil, nil, err } return cfg, db, cipher, nil } func run() error { cfg, db, cipher, err := bootstrap() if err != nil { return err } auth := service.NewAuthService(db, cfg.JWTSecret) auth.SetCipher(cipher) if err := auth.EnsureAdmin(cfg.AdminUsername, cfg.AdminPassword); err != nil { return err } ociClient := oci.NewCachedClient(oci.NewClient()) ociConfigs := service.NewOciConfigService(db, cipher, ociClient) defer ociConfigs.Stop() settings := service.NewSettingService(db, cipher) settings.SetEnvPublicURL(cfg.PublicURL) if err := settings.ReloadSecurity(context.Background()); err != nil { return err } notifier := service.NewNotifier(settings) auth.SetNotifier(notifier, settings) oauth := service.NewOAuthService(db, settings, auth) passkeys := service.NewPasskeyService(db, settings, auth) wallets := service.NewWalletService(db, settings, auth) systemLogs := service.NewSystemLogService(db) logEvents := service.NewLogEventService(db) logEvents.SetRelayDeps(ociConfigs, ociClient, cfg.PublicURL) logEvents.SetNotifier(notifier, settings) cleanupCtx, stopCleanup := context.WithCancel(context.Background()) systemLogs.StartCleanup(cleanupCtx) auth.StartSessionCleanup(cleanupCtx) logEvents.StartParser(cleanupCtx) logEvents.StartCleanup(cleanupCtx) aiGateway := service.NewAiGatewayService(db, ociConfigs, ociClient) aiGateway.StartCleanup(cleanupCtx) // defer 为 LIFO:先停调度与清理,再等在途通知与日志写完 defer auth.WaitSessionCleanup() defer logEvents.Wait() defer systemLogs.Wait() defer notifier.Wait() defer aiGateway.Wait() defer stopCleanup() tasks := service.NewTaskService(db, ociConfigs, notifier, settings) ociConfigs.SetTenantCleanupDeps(tasks) tasks.AttachAiGateway(aiGateway) aiGateway.SetOnChannelsChanged(tasks.SyncAiProbeTask) if err := tasks.Start(); err != nil { return err } defer tasks.Stop() // 启动时对齐一次探测任务(已有渠道而任务缺失/暂停时自动建立或激活) tasks.SyncAiProbeTask(context.Background()) console := service.NewConsoleService(ociConfigs) proxies := service.NewProxyService(db, cipher) defer proxies.Wait() // 「关于」页存储指标需知数据库形态 api.SetAboutRuntime(cfg.DBDriver, cfg.DBPath) router := api.NewRouter(auth, oauth, passkeys, wallets, ociConfigs, tasks, console, settings, notifier, systemLogs, logEvents, proxies, aiGateway) return serveHTTP(cfg.Addr, router) } // serveHTTP 以显式超时与请求头上限启动 HTTP 服务,并在 SIGINT/SIGTERM 时 // 优雅关闭(10 秒排空);防慢连接(Slowloris)与超大头耗尽资源。 // WriteTimeout 置 0:AI 网关流式响应无固定上界,WebSocket 为 hijack 连接 // 本就不受 net/http 超时管控;读侧超时已覆盖慢请求风险。 func serveHTTP(addr string, handler http.Handler) error { srv := &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 60 * time.Second, IdleTimeout: 2 * time.Minute, MaxHeaderBytes: 64 << 10, } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() errCh := make(chan error, 1) go func() { errCh <- srv.ListenAndServe() }() select { case err := <-errCh: return err case <-ctx.Done(): log.Println("[info] shutting down http server") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) } }