package main import ( "context" "fanqie/internal/algos" "fanqie/internal/common" "fanqie/internal/config" "fanqie/internal/fqapi" "fanqie/internal/handlers" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" // 嵌入 tzdata,容器内无系统时区库时 TZ 仍可用 _ "time/tzdata" ) // Version / BuildTime 由编译时 -ldflags "-X main.Version=... -X main.BuildTime=..." 注入 var ( Version = "dev" BuildTime = "unknown" logger = common.NewLogger("main", "INFO", "") configWatcher *config.ConfigWatcher ) func main() { // 时区生效须先于一切时间显示(.env 中的 TZ) config.ApplyTimezone() logger.Info(fmt.Sprintf("番茄小说API服务 版本=%s 编译时间=%s", Version, BuildTime)) // 加载配置文件 initConfig() // 初始化数据库 if err := fqapi.InitDB(); err != nil { logger.Error("数据库初始化失败: " + err.Error()) os.Exit(1) } defer fqapi.CloseCache() // 程序退出时关闭数据库 // 初始化 Redis 缓存(可选) if err := fqapi.InitRedis(); err != nil { logger.Error("Redis 初始化失败: " + err.Error()) os.Exit(1) } defer fqapi.CloseRedis() // 加载统计数据 handlers.LoadStatistics() // 启动统计数据定期保存 handlers.StartStatisticsSaver() defer handlers.StopStatisticsSaver() // 程序退出时保存统计数据 // 启动缓存自动清理任务 fqapi.StartCacheCleanup() defer fqapi.StopCacheCleanup() // 程序退出时停止清理任务 // 启动请求日志(server.request_log.enabled 开启时生效) if err := fqapi.InitRequestLog(); err != nil { logger.Warning("请求日志初始化失败: " + err.Error()) } defer fqapi.CloseRequestLog() // 程序退出时落盘剩余日志 // 初始化算法切换器 initAlgorithm() // 初始化设备密钥池 if err := initDeviceKeyPool(); err != nil { logger.Error("设备密钥池初始化失败: " + err.Error()) os.Exit(1) } // 设置路由 setupRoutes() // 启动服务器 startServer() } // initConfig 初始化配置 func initConfig() { configPath := "./config/config.yaml" if _, err := os.Stat(configPath); os.IsNotExist(err) { os.MkdirAll("./config", 0755) } if err := config.LoadConfig(configPath); err != nil { logger.Warning(fmt.Sprintf("加载配置失败: %v", err)) } // 启动配置文件热更新监控 watcher, err := config.StartConfigWatcher(configPath) if err != nil { logger.Warning(fmt.Sprintf("配置监控失败: %v", err)) } else { configWatcher = watcher } // 注册配置更新回调 config.RegisterConfigCallback(func(cfg *config.Config) { algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(cfg.Fanqie.Algorithm) algos.GlobalSignatureSwitcher.SwitchSignature(cfg.Fanqie.Signature) }) } // initAlgorithm 初始化算法 func initAlgorithm() { if config.GetAppConfig() != nil { algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(config.GetAppConfig().Fanqie.Algorithm) logger.Info(fmt.Sprintf("当前算法: %s", config.GetAppConfig().Fanqie.Algorithm)) algos.GlobalSignatureSwitcher.SwitchSignature(config.GetAppConfig().Fanqie.Signature) // 根据签名方法显示详细信息 var signatureDetails string if config.GetAppConfig().Fanqie.Signature == "sign4" { signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos" } else { signatureDetails = "X-Gorgon + X-Khronos" } logger.Info(fmt.Sprintf("当前签名方法: %s", signatureDetails)) } else { algos.GlobalAlgorithmSwitcher.SwitchAlgorithm("gorgon") algos.GlobalSignatureSwitcher.SwitchSignature("sign2") } } // initDeviceKeyPool 初始化设备密钥池 func initDeviceKeyPool() error { pool := fqapi.GetDeviceKeyPool() if err := pool.Initialize(); err != nil { return fmt.Errorf("初始化设备密钥池失败: %w", err) } // 显示池状态 stats := pool.GetStats() logger.Info(fmt.Sprintf("设备密钥池状态 - 总数: %d, 有效: %d, 无效: %d", stats["total"], stats["valid"], stats["invalid"])) return nil } // setupRoutes 设置路由 func setupRoutes() { http.HandleFunc("/", handlers.HandleMain) http.HandleFunc("/status", handlers.RequireAccessKey(handlers.HandleStatus)) http.HandleFunc("/healthz", handlers.HandleHealth) // API端点:请求日志 + 限流 + protect_all 鉴权链(代理路由不鉴权,URL 由服务端生成不含 key) wrap := func(h http.HandlerFunc) http.HandlerFunc { return handlers.LogRequestMiddleware(handlers.RateLimit(handlers.OptionalAccessKey(h))) } http.HandleFunc("/search", wrap(handlers.HandleSearch)) http.HandleFunc("/detail", wrap(handlers.HandleDetail)) http.HandleFunc("/info", wrap(handlers.HandleDetail)) http.HandleFunc("/catalog", wrap(handlers.HandleCatalog)) http.HandleFunc("/directory/all_items", wrap(handlers.HandleCatalog)) http.HandleFunc("/multi-detail", wrap(handlers.HandleMultiDetail)) http.HandleFunc("/multi-content", wrap(handlers.HandleMultiContent)) http.HandleFunc("/content", wrap(handlers.HandleContent)) http.HandleFunc("/item-summary", wrap(handlers.HandleItemSummary)) http.HandleFunc("/category-front", wrap(handlers.HandleCategoryFront)) http.HandleFunc("/category-landing", wrap(handlers.HandleCategoryLanding)) http.HandleFunc("/audio", wrap(handlers.HandleAudio)) http.HandleFunc("/audio/proxy/", wrap(handlers.HandleAudioProxy)) http.HandleFunc("/video", wrap(handlers.HandleVideo)) http.HandleFunc("/video/proxy/", wrap(handlers.HandleVideoProxy)) http.HandleFunc("/manga", wrap(handlers.HandleManga)) http.HandleFunc("/manga/image/", wrap(handlers.HandleMangaImageProxy)) // 评论相关端点 http.HandleFunc("/chapter_review", wrap(handlers.HandleChapterReview)) http.HandleFunc("/review", wrap(handlers.HandleReview)) http.HandleFunc("/review_html", wrap(handlers.HandleReviewHTML)) // 表情图片静态文件服务 http.Handle("/emoji/", http.StripPrefix("/emoji/", http.FileServer(http.Dir("emoji")))) } // startServer 启动服务器 func startServer() { host := "0.0.0.0" port := "40180" if config.GetAppConfig() != nil { if config.GetAppConfig().Server.Host != "" { host = config.GetAppConfig().Server.Host } if config.GetAppConfig().Server.Port != "" { port = config.GetAppConfig().Server.Port } } addr := host + ":" + port server := &http.Server{ Addr: addr, Handler: handlers.Gzip(http.DefaultServeMux), // 超时保护:防止慢客户端长期占用连接 ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 60 * time.Second, WriteTimeout: 10 * time.Minute, // 代理响应整体缓冲后写出,慢客户端下载大音频/图片需要余量 IdleTimeout: 120 * time.Second, } go func() { logger.Info(fmt.Sprintf("服务器启动在 %s", addr)) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error(fmt.Sprintf("服务器启动失败: %v", err)) os.Exit(1) } }() // 等待退出信号,优雅关闭,让 main 中的 defer(保存统计、关闭数据库)得以执行 sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) <-sigCh logger.Info("收到退出信号,正在关闭服务器...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Warning(fmt.Sprintf("服务器关闭超时(30秒内未完成的连接被强制断开): %v", err)) } logger.Info("服务器已关闭") } func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) }