// Command server 是 FuelLog API 入口。 // // 启动流程: // 1. 加载 .env // 2. 解析配置 // 3. 打开 SQLite + 运行迁移 // 4. 装配路由 // 5. 监听并优雅退出 package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "gorm.io/gorm" "github.com/kennethcheng/fuellog-api/internal/config" "github.com/kennethcheng/fuellog-api/internal/db" "github.com/kennethcheng/fuellog-api/internal/middleware" "github.com/kennethcheng/fuellog-api/pkg/apperror" ) func main() { // .env 加载失败不致命(可能是生产环境用环境变量注入) if err := godotenv.Load(); err != nil && !os.IsNotExist(err) { slog.Warn(".env load failed", "err", err) } cfg, err := config.Load() if err != nil { slog.Error("config load failed", "err", err) os.Exit(1) } gin.SetMode(cfg.GinMode) slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: parseLogLevel(cfg.LogLevel), }))) gormDB, err := db.Open(cfg.DBPath) if err != nil { slog.Error("db open failed", "err", err) os.Exit(1) } if err := db.Migrate(gormDB); err != nil { slog.Error("db migrate failed", "err", err) os.Exit(1) } slog.Info("db ready", "path", cfg.DBPath) app := &application{cfg: cfg, db: gormDB} srv := &http.Server{ Addr: cfg.ServerAddr, Handler: app.router(), ReadHeaderTimeout: 10 * time.Second, } // 启动 + 优雅退出 go func() { slog.Info("server starting", "addr", cfg.ServerAddr, "mode", cfg.GinMode) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server error", "err", err) os.Exit(1) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop slog.Info("server shutting down") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { slog.Error("graceful shutdown failed", "err", err) os.Exit(1) } slog.Info("server stopped") } // application 持有所有依赖,避免散落全局变量。 type application struct { cfg *config.Config db *gorm.DB // Phase 1.3 起注入:vehicleRepo / refuelRepo / vehicleSvc / refuelSvc } // router 构建并返回 Gin 引擎。 // 当前阶段只暴露 /health;Phase 1.3 起逐步接 /api/vehicles、/api/refuels。 func (a *application) router() *gin.Engine { r := gin.New() r.Use( middleware.Recover(), middleware.Logger(), middleware.CORS(a.cfg.CORSOrigins), ) r.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{ "code": apperror.CodeNotFound, "message": "路径不存在: " + c.Request.URL.Path, }) }) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "time": a.cfg.Now().In(a.cfg.Location()).Format(time.RFC3339), }) }) // /api 路由组(Phase 1.3 起在此挂载业务路由) api := r.Group("/api") _ = api // 占位,避免未使用告警 return r } func parseLogLevel(s string) slog.Level { switch s { case "debug": return slog.LevelDebug case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }