package main import ( "context" "database/sql" "errors" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "github.com/luxsin/app-api/internal/cache" "github.com/luxsin/app-api/internal/config" "github.com/luxsin/app-api/internal/database" "github.com/luxsin/app-api/internal/model" "github.com/luxsin/app-api/internal/repository" "github.com/luxsin/app-api/internal/router" "github.com/luxsin/app-api/internal/search" "github.com/luxsin/app-api/internal/task" "github.com/luxsin/app-api/pkg/logger" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) func main() { cfg, err := config.Load() if err != nil { panic(err) } if cfg.Env == "production" { gin.SetMode(gin.ReleaseMode) } log, err := logger.New(cfg.Env) if err != nil { panic(err) } defer log.Sync() //nolint:errcheck db, err := database.Open(cfg.Database) if err != nil { log.Fatal("database connect failed", zap.Error(err)) } defer db.Close() log.Info("database connected", zap.String("host", cfg.Database.Host), zap.Int("port", cfg.Database.Port), zap.String("database", cfg.Database.Name), ) searchClient := search.NewClient(cfg.Meilisearch) log.Info("meilisearch configured", zap.String("host", cfg.Meilisearch.Host), zap.String("index", cfg.Meilisearch.Index), ) rdb := cache.NewClient(cfg.Redis) defer rdb.Close() log.Info("redis connected", zap.String("host", cfg.Redis.Host), zap.Int("port", cfg.Redis.Port), zap.Int("db", cfg.Redis.Database), ) // 启动时预热缓存 warmUpCache(log, db, rdb) // 启动定时刷入任务:每 5 分钟从 Redis 刷入数据库 deviceRepo := repository.NewDeviceRepository(db) devicePersistTask := task.NewDevicePersistTask(rdb, deviceRepo, log) devicePersistTask.Start(5 * time.Minute) engine := router.New(log, db, searchClient, rdb) srv := &http.Server{ Addr: cfg.Addr(), Handler: engine, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Info("server starting", zap.String("addr", cfg.Addr()), zap.String("env", cfg.Env)) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal("server failed", zap.Error(err)) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Info("server shutting down") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("server shutdown failed", zap.Error(err)) } log.Info("server stopped") } // warmUpCache 启动时从数据库加载数据到 Redis func warmUpCache(log *zap.Logger, db *sql.DB, rdb *redis.Client) { ctx := context.Background() brandCache := cache.NewBrandCache(rdb) modelCache := cache.NewModelCache(rdb) brandRepo := repository.NewBrandRepository(db, brandCache) modelRepo := repository.NewModelRepository(db, modelCache) // 预热 brand:all if brands, err := brandRepo.ListFromDB(ctx, ""); err != nil { log.Warn("brand warm-up failed", zap.Error(err)) } else if err := brandCache.SetAll(ctx, brands); err != nil { log.Warn("brand cache set failed", zap.Error(err)) } else { log.Info("brand cache warmed up", zap.Int("count", len(brands))) } // 预热 model:all + model:brand:{brandName} if allModels, err := modelRepo.ListAllFromDB(ctx); err != nil { log.Warn("model warm-up failed", zap.Error(err)) } else if err := modelCache.SetAll(ctx, allModels); err != nil { log.Warn("model cache set failed", zap.Error(err)) } else { log.Info("model cache warmed up", zap.Int("count", len(allModels))) // 按品牌分组预热 model:brand:{brandName} grouped := groupModelsByBrand(allModels) for brandName, models := range grouped { if err := modelCache.SetByBrand(ctx, brandName, models); err != nil { log.Warn("model brand cache set failed", zap.String("brand", brandName), zap.Error(err)) } } log.Info("model brand cache warmed up", zap.Int("brands", len(grouped))) } } // groupModelsByBrand 按品牌分组型号列表 func groupModelsByBrand(models []model.Model) map[string][]model.Model { result := make(map[string][]model.Model) for _, m := range models { result[m.BrandName] = append(result[m.BrandName], m) } return result }