🐛 fix(server): 增加优雅退出与请求超时保护

- 新增 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 中的冗余依赖条目
This commit is contained in:
chenxin committed 2026-09-29 19:47:04 +08:00
1 parent 6981148727
commit d710d382ee
12 files changed
+125 -305

No files matched your search

+18 -21
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/spf13/viper"
)
@@ -67,17 +68,21 @@ type DeviceKeyPoolConfig struct {
// ConfigManager 配置管理器
type ConfigManager struct {
mu sync.RWMutex
config *Config
configPath string
callbacks []func(*Config)
}
var (
// AppConfig 全局配置实例
AppConfig *Config
// appConfig 全局配置实例(原子指针,热更新替换时读取侧无数据竞争)
appConfig atomic.Pointer[Config]
manager *ConfigManager
)
// GetAppConfig 获取当前配置,未加载时返回 nil
func GetAppConfig() *Config {
return appConfig.Load()
}
// NewConfigManager 创建配置管理器
func NewConfigManager() *ConfigManager {
return &ConfigManager{
@@ -109,10 +114,7 @@ func LoadConfig(configPath string) error {
}
// 更新配置
manager.mu.Lock()
manager.config = newConfig
AppConfig = newConfig
manager.mu.Unlock()
appConfig.Store(newConfig)
return nil
}
@@ -155,18 +157,11 @@ func setDefaults() {
// GetConfig 获取配置实例
func GetConfig() *Config {
if manager != nil {
manager.mu.RLock()
defer manager.mu.RUnlock()
if manager.config != nil {
return manager.config
}
if cfg := appConfig.Load(); cfg != nil {
return cfg
}
if AppConfig == nil {
log.Fatal("配置未初始化,请先调用 LoadConfig")
}
return AppConfig
log.Fatal("配置未初始化,请先调用 LoadConfig")
return nil
}
// ReloadConfig 重新加载配置文件
@@ -196,9 +191,11 @@ func ReloadConfig() error {
// 更新配置
manager.mu.Lock()
oldAlgorithm := manager.config.Fanqie.Algorithm
manager.config = newConfig
AppConfig = newConfig
var oldAlgorithm string
if old := appConfig.Load(); old != nil {
oldAlgorithm = old.Fanqie.Algorithm
}
appConfig.Store(newConfig)
callbacks := make([]func(*Config), len(manager.callbacks))
copy(callbacks, manager.callbacks)
manager.mu.Unlock()
+2 -2
View File
@@ -9,8 +9,8 @@ import (
// getContentConfig 获取内容配置(带默认值)
func getContentConfig() config.ContentConfig {
if config.AppConfig != nil {
return config.AppConfig.Fanqie.Content
if config.GetAppConfig() != nil {
return config.GetAppConfig().Fanqie.Content
}
// 返回默认配置
return config.ContentConfig{
+3 -6
View File
@@ -9,8 +9,7 @@ import (
// HandleSearch 搜索处理
func HandleSearch(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementSearch()
incrementStat("total", "search")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
query := r.URL.Query().Get("query")
@@ -41,8 +40,7 @@ func HandleSearch(w http.ResponseWriter, r *http.Request) {
// HandleDetail 详情处理
func HandleDetail(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementDetail()
incrementStat("total", "detail")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
bookID := r.URL.Query().Get("book_id")
@@ -58,8 +56,7 @@ func HandleDetail(w http.ResponseWriter, r *http.Request) {
// HandleCatalog 目录处理
func HandleCatalog(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCatalog()
incrementStat("total", "catalog")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
bookID := r.URL.Query().Get("book_id")
+2 -4
View File
@@ -8,8 +8,7 @@ import (
// HandleCategoryFront 分类列表处理
func HandleCategoryFront(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCategory()
incrementStat("total", "category")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
tab := r.URL.Query().Get("tab")
@@ -25,8 +24,7 @@ func HandleCategoryFront(w http.ResponseWriter, r *http.Request) {
// HandleCategoryLanding 分类详情处理
func HandleCategoryLanding(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCategory()
incrementStat("total", "category")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
categoryID := r.URL.Query().Get("category_id")
+35 -52
View File
@@ -13,42 +13,41 @@ import (
var (
startTime = time.Now()
deviceHeader string
// 请求统计
totalRequests int64
searchRequests int64
detailRequests int64
catalogRequests int64
contentRequests int64
mangaRequests int64
audioRequests int64
videoRequests int64
categoryRequests int64
requestStats = newRequestStats()
// 统计保存定时器
statsSaveTicker *time.Ticker
)
// 统计函数
func incrementTotal() { atomic.AddInt64(&totalRequests, 1) }
func incrementSearch() { atomic.AddInt64(&searchRequests, 1) }
func incrementDetail() { atomic.AddInt64(&detailRequests, 1) }
func incrementCatalog() { atomic.AddInt64(&catalogRequests, 1) }
func incrementContent() { atomic.AddInt64(&contentRequests, 1) }
func incrementManga() { atomic.AddInt64(&mangaRequests, 1) }
func incrementAudio() { atomic.AddInt64(&audioRequests, 1) }
func incrementVideo() { atomic.AddInt64(&videoRequests, 1) }
func incrementCategory() { atomic.AddInt64(&categoryRequests, 1) }
// statsKinds 统计类别
var statsKinds = []string{"total", "search", "detail", "catalog", "content", "manga", "audio", "video", "category"}
func getTotal() int64 { return atomic.LoadInt64(&totalRequests) }
func getSearch() int64 { return atomic.LoadInt64(&searchRequests) }
func getDetail() int64 { return atomic.LoadInt64(&detailRequests) }
func getCatalog() int64 { return atomic.LoadInt64(&catalogRequests) }
func getContent() int64 { return atomic.LoadInt64(&contentRequests) }
func getManga() int64 { return atomic.LoadInt64(&mangaRequests) }
func getAudio() int64 { return atomic.LoadInt64(&audioRequests) }
func getVideo() int64 { return atomic.LoadInt64(&videoRequests) }
func getCategory() int64 { return atomic.LoadInt64(&categoryRequests) }
// newRequestStats 初始化各类计数器
func newRequestStats() map[string]*atomic.Int64 {
m := make(map[string]*atomic.Int64, len(statsKinds))
for _, k := range statsKinds {
m[k] = &atomic.Int64{}
}
return m
}
// incrementStat 原子递增请求计数
func incrementStat(kinds ...string) {
for _, k := range kinds {
requestStats[k].Add(1)
}
}
// getStatsSnapshot 获取所有计数器的快照
func getStatsSnapshot() map[string]int64 {
snapshot := make(map[string]int64, len(requestStats))
for k, c := range requestStats {
snapshot[k] = c.Load()
}
return snapshot
}
// SetDeviceHeader 设置设备header
func SetDeviceHeader(header string) {
@@ -63,32 +62,16 @@ func LoadStatistics() {
return
}
atomic.StoreInt64(&totalRequests, stats["total"])
atomic.StoreInt64(&searchRequests, stats["search"])
atomic.StoreInt64(&detailRequests, stats["detail"])
atomic.StoreInt64(&catalogRequests, stats["catalog"])
atomic.StoreInt64(&contentRequests, stats["content"])
atomic.StoreInt64(&mangaRequests, stats["manga"])
atomic.StoreInt64(&audioRequests, stats["audio"])
atomic.StoreInt64(&videoRequests, stats["video"])
atomic.StoreInt64(&categoryRequests, stats["category"])
for k, v := range stats {
if c, ok := requestStats[k]; ok {
c.Store(v)
}
}
}
// SaveStatistics 保存统计数据到数据库
func SaveStatistics() {
stats := map[string]int64{
"total": atomic.LoadInt64(&totalRequests),
"search": atomic.LoadInt64(&searchRequests),
"detail": atomic.LoadInt64(&detailRequests),
"catalog": atomic.LoadInt64(&catalogRequests),
"content": atomic.LoadInt64(&contentRequests),
"manga": atomic.LoadInt64(&mangaRequests),
"audio": atomic.LoadInt64(&audioRequests),
"video": atomic.LoadInt64(&videoRequests),
"category": atomic.LoadInt64(&categoryRequests),
}
if err := fqapi.SaveStatistics(stats); err != nil {
if err := fqapi.SaveStatistics(getStatsSnapshot()); err != nil {
fmt.Printf("保存统计数据失败: %v\n", err)
}
}
+1 -2
View File
@@ -9,8 +9,7 @@ import (
// HandleContent 内容处理
func HandleContent(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementContent()
incrementStat("total", "content")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 从查询参数获取item_id
+4 -7
View File
@@ -10,8 +10,7 @@ import (
// HandleAudio 音频处理
func HandleAudio(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementAudio()
incrementStat("total", "audio")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
itemIDs := r.URL.Query().Get("item_ids")
@@ -29,7 +28,7 @@ func HandleAudio(w http.ResponseWriter, r *http.Request) {
// HandleAudioProxy 音频代理处理
func HandleAudioProxy(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 从URL路径提取key: /audio/proxy/{key}
path := r.URL.Path
@@ -60,8 +59,7 @@ func HandleAudioProxy(w http.ResponseWriter, r *http.Request) {
// HandleVideo 视频处理
func HandleVideo(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementVideo()
incrementStat("total", "video")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
itemIDs := r.URL.Query().Get("item_ids")
@@ -77,8 +75,7 @@ func HandleVideo(w http.ResponseWriter, r *http.Request) {
// HandleManga 漫画处理
func HandleManga(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementManga()
incrementStat("total", "manga")
itemID := r.URL.Query().Get("item_ids")
if itemID == "" {
itemID = r.URL.Query().Get("item_id")
+3 -3
View File
@@ -70,7 +70,7 @@ func handleReviewSuccess(w http.ResponseWriter, data interface{}) {
// HandleChapterReview 处理章节评论请求(混合数据)
func HandleChapterReview(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id")
@@ -94,7 +94,7 @@ func HandleChapterReview(w http.ResponseWriter, r *http.Request) {
// HandleReview 处理段落评论请求
func HandleReview(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id")
@@ -141,7 +141,7 @@ func HandleReview(w http.ResponseWriter, r *http.Request) {
// HandleReviewHTML 处理段落评论 HTML 显示
func HandleReviewHTML(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id", "para")
+10 -18
View File
@@ -72,15 +72,7 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
}
// 获取统计数据
total := getTotal()
search := getSearch()
detail := getDetail()
catalog := getCatalog()
content := getContent()
manga := getManga()
audio := getAudio()
video := getVideo()
category := getCategory()
stats := getStatsSnapshot()
// 获取设备密钥池状态
poolStatus := getDeviceKeyPoolStatus()
@@ -857,15 +849,15 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
poolStatus["valid"],
poolStatus["invalid"],
poolStatus["current_index"],
total,
search,
detail,
catalog,
content,
manga,
audio,
video,
category,
stats["total"],
stats["search"],
stats["detail"],
stats["catalog"],
stats["content"],
stats["manga"],
stats["audio"],
stats["video"],
stats["category"],
getDBCacheCount(),
getCacheStatus(),
getCleanupStatus(),