- 新增 SIGINT/SIGTERM 信号处理与 http.Server.Shutdown,退出前确保统计数据保存、数据库正常关闭 - 为 http.Server 设置 ReadHeaderTimeout/ReadTimeout/WriteTimeout/IdleTimeout,防止慢客户端长期占用连接 - 全局配置指针改为 atomic.Pointer 并提供 GetAppConfig 访问器,消除配置热更新时的数据竞争,同时删除冗余的 manager.config 字段 - 请求统计九组计数器重构为 map[string]*atomic.Int64,统一 incrementStat/getStatsSnapshot 访问,精简约 40 行样板代码 - go mod tidy 清理 go.mod 与 go.sum 中的冗余依赖条目
210 lines
6.2 KiB
Go
210 lines
6.2 KiB
Go
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"
|
|
)
|
|
|
|
var (
|
|
logger = common.NewLogger("main", "INFO", "")
|
|
configWatcher *config.ConfigWatcher
|
|
)
|
|
|
|
func main() {
|
|
logger.Info("番茄小说API服务 - v2.0.0")
|
|
|
|
// 加载配置文件
|
|
initConfig()
|
|
|
|
// 初始化数据库
|
|
if err := fqapi.InitDB(); err != nil {
|
|
logger.Error("数据库初始化失败: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer fqapi.CloseCache() // 程序退出时关闭数据库
|
|
|
|
// 加载统计数据
|
|
handlers.LoadStatistics()
|
|
|
|
// 启动统计数据定期保存
|
|
handlers.StartStatisticsSaver()
|
|
defer handlers.StopStatisticsSaver() // 程序退出时保存统计数据
|
|
|
|
// 启动缓存自动清理任务
|
|
fqapi.StartCacheCleanup()
|
|
defer fqapi.StopCacheCleanup() // 程序退出时停止清理任务
|
|
|
|
// 初始化算法切换器
|
|
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("/self-info", handlers.RequireAccessKey(handlers.HandleSelfInfo))
|
|
http.HandleFunc("/vacuum", handlers.RequireAccessKey(handlers.HandleVacuum))
|
|
|
|
// API端点
|
|
http.HandleFunc("/search", handlers.HandleSearch)
|
|
http.HandleFunc("/detail", handlers.HandleDetail)
|
|
http.HandleFunc("/info", handlers.HandleDetail)
|
|
http.HandleFunc("/catalog", handlers.HandleCatalog)
|
|
http.HandleFunc("/directory/all_items", handlers.HandleCatalog)
|
|
http.HandleFunc("/multi-detail", handlers.HandleMultiDetail)
|
|
http.HandleFunc("/multi-content", handlers.HandleMultiContent)
|
|
http.HandleFunc("/content", handlers.HandleContent)
|
|
http.HandleFunc("/item-summary", handlers.HandleItemSummary)
|
|
http.HandleFunc("/category-front", handlers.HandleCategoryFront)
|
|
http.HandleFunc("/category-landing", handlers.HandleCategoryLanding)
|
|
http.HandleFunc("/audio", handlers.HandleAudio)
|
|
http.HandleFunc("/audio/proxy/", handlers.HandleAudioProxy)
|
|
http.HandleFunc("/video", handlers.HandleVideo)
|
|
http.HandleFunc("/manga", handlers.HandleManga)
|
|
http.HandleFunc("/manga/image/", handlers.HandleMangaImageProxy)
|
|
|
|
// 评论相关端点
|
|
http.HandleFunc("/chapter_review", handlers.HandleChapterReview)
|
|
http.HandleFunc("/review", handlers.HandleReview)
|
|
http.HandleFunc("/review_html", 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,
|
|
// 超时保护:防止慢客户端长期占用连接
|
|
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)
|
|
}
|