Files
fanqie-go/cmd/fanqie/main.go
T
chenxin 889c399a0b 🚀 deploy(build): 新增一键部署脚本与构建工具链
- 新增 deploy.sh:编译部署、systemd/Nginx 初始化、软链接版本管理与交互式回滚、部署后 /healthz 健康自检、certbot 自动申请 HTTPS 证书与续期重载 Hook
- 新增 .deploy.env.example 部署配置模板,.deploy.env(不入库)覆盖脚本默认值,路径默认由 APP_NAME 派生,CERT_EMAIL 默认 admin@域名
- 新增 Makefile:build / build-linux 交叉编译 / test(含 go vet)/ clean
- main.go 新增 Version/BuildTime 变量,使 -ldflags -X 注入生效并在启动日志打印
- systemd 工作目录改为代码目录,修复 config.yaml/.env/cache.db 相对路径加载失败
- .gitignore 忽略 .deploy.env 与 fanqie-* 编译产物;README 新增部署章节与 HTTPS 说明
2026-10-09 15:53:21 +08:00

226 lines
6.9 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"
)
// Version / BuildTime 由编译时 -ldflags "-X main.Version=... -X main.BuildTime=..." 注入
var (
Version = "dev"
BuildTime = "unknown"
logger = common.NewLogger("main", "INFO", "")
configWatcher *config.ConfigWatcher
)
func main() {
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() // 程序退出时停止清理任务
// 初始化算法切换器
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))
http.HandleFunc("/healthz", handlers.HandleHealth)
// API端点:限流 + protect_all 鉴权链(代理路由不鉴权,URL 由服务端生成不含 key)
wrap := func(h http.HandlerFunc) http.HandlerFunc {
return 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("/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,
// 超时保护:防止慢客户端长期占用连接
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)
}