// server 是可转债轮动分析系统的 B/S 服务入口。 package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "stock-analysis/internal/controller" "stock-analysis/internal/datasource/eastmoney" "stock-analysis/internal/datasource/fallback" "stock-analysis/internal/datasource/tencent" "stock-analysis/internal/router" "stock-analysis/internal/service" "stock-analysis/internal/storage/sqlite" ) func main() { addr := ":8080" if p := os.Getenv("PORT"); p != "" { addr = ":" + p } webDir := "web" dbPath := defaultDBPath() // 东财为主数据源(实时行情+历史K线),腾讯为历史K线备用源(东财失败时自动切换) ds := fallback.New(eastmoney.NewClient(), tencent.NewClient()) svc := service.NewMarketService(ds) mc := controller.NewMarketController(svc, dbPath) store, err := sqlite.Open(dbPath) if err != nil { log.Fatalf("open sqlite: %v", err) } defer store.Close() authSvc := service.NewAuthService(store) if err := authSvc.EnsureAdmin(context.Background()); err != nil { log.Printf("ensure admin: %v", err) } ac := controller.NewAuthController(authSvc) handler := router.New(mc, ac, webDir) server := &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, } // 优雅退出:收到信号后关闭服务。 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { log.Printf("stock-analysis server listening on http://localhost%s", addr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server: %v", err) } }() <-ctx.Done() log.Println("shutting down server...") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { log.Printf("shutdown: %v", err) } } // defaultDBPath 返回默认数据库路径。 // 默认使用项目目录下的 stock.db,避免 OneDrive/AppData 的 I/O 限制。 func defaultDBPath() string { if env := os.Getenv("STOCK_DB_PATH"); env != "" { return env } exe, err := os.Executable() if err != nil { return "stock.db" } return filepath.Join(filepath.Dir(exe), "stock.db") }