package main import ( "context" "log" "os" "os/signal" "path/filepath" "strings" "syscall" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "everypublish/server/internal/api" "everypublish/server/internal/cache" "everypublish/server/internal/config" "everypublish/server/internal/db" "everypublish/server/internal/models" "everypublish/server/internal/ws" ) func main() { _ = godotenv.Load() // 本地开发读 .env,生产用环境变量 cfg := config.Load() gdb, err := db.Connect(cfg) if err != nil { log.Fatalf("db connect failed: %v", err) } if err = db.AutoMigrate(gdb); err != nil { log.Fatalf("auto migrate failed: %v", err) } // 启动自愈:上次异常退出残留的 online 状态重置为 offline(真实状态以连接为准) _ = gdb.Model(&models.AgentDevice{}).Where("status = ?", "online").Update("status", "offline").Error log.Println("mysql connected & migrated") rds := cache.New(cfg) if err = rds.Ping(); err != nil { log.Fatalf("redis ping failed: %v", err) } log.Println("redis connected") serverKey, err := ws.LoadOrCreateServerKey("./data/server_ed25519.key") if err != nil { log.Fatalf("server ed25519 key failed: %v", err) } hub := ws.NewHub() dsp := ws.NewDispatcher(gdb, hub, cfg) ctx, stopFn := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stopFn() go dsp.Start(ctx.Done()) router := api.Router(cfg, gdb, rds, hub, serverKey, dsp) // 托管前端生产构建(STATIC_DIR 存在 index.html 时启用),SPA 回退 if cfg.StaticDir != "" { if _, err := os.Stat(filepath.Join(cfg.StaticDir, "index.html")); err == nil { router.Static("/assets", filepath.Join(cfg.StaticDir, "assets")) router.StaticFile("/favicon.ico", filepath.Join(cfg.StaticDir, "favicon.ico")) router.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/") { c.JSON(404, gin.H{"code": 404, "message": "接口不存在"}) return } c.File(filepath.Join(cfg.StaticDir, "index.html")) }) log.Printf("serving frontend from %s", cfg.StaticDir) } } log.Printf("everypublish-server listening on %s", cfg.ServerAddr) if err = router.Run(cfg.ServerAddr); err != nil { log.Fatalf("server exited: %v", err) } }