- request_log 新增 user_agent 列(含旧表 ALTER 迁移)及 ip/status 索引,记录解码后的明文 query(含 key,不脱敏) - 请求日志批量写入改为多值 INSERT 单次往返,替代事务逐条 Exec - 新增按本地时区分天聚合查询,状态页展示今日请求量与近 7 天请求趋势(峰值日高亮) - 新增 TZ 时区支持:.env 配置 TZ 后日志时间戳与状态页全部时间统一生效,二进制内嵌 tzdata 保证容器可用 - 五张表统一补齐 created_at/updated_at 字段,启动时自动补列并按业务时间回填旧行 - device_pool 的 device_header 增加唯一约束;audio_cache 写入改为 UPSERT 修复同 key 重复报错 - 状态页"最近请求"新增 UA 列与限流状态显示;config.yaml 暴露 rate_limit_qps 配置并默认启用请求日志
286 lines
7.3 KiB
Go
286 lines
7.3 KiB
Go
package handlers
|
||
|
||
import (
|
||
_ "embed"
|
||
"fanqie/internal/algos"
|
||
"fanqie/internal/config"
|
||
"fanqie/internal/fqapi"
|
||
"fmt"
|
||
"html/template"
|
||
"net/http"
|
||
"time"
|
||
)
|
||
|
||
// 状态页模板(embed 内嵌,动态数据由 html/template 自动转义)
|
||
//
|
||
//go:embed templates/status.html
|
||
var statusHTML string
|
||
|
||
var statusTmpl = template.Must(template.New("status").Parse(statusHTML))
|
||
|
||
// HandleStatus 状态页面处理
|
||
func HandleStatus(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
uptime := time.Since(startTime)
|
||
|
||
// 格式化运行时间
|
||
days := int(uptime.Hours() / 24)
|
||
hours := int(uptime.Hours()) % 24
|
||
minutes := int(uptime.Minutes()) % 60
|
||
seconds := int(uptime.Seconds()) % 60
|
||
var uptimeStr string
|
||
if days > 0 {
|
||
uptimeStr = fmt.Sprintf("%d天 %d小时 %d分钟 %d秒", days, hours, minutes, seconds)
|
||
} else if hours > 0 {
|
||
uptimeStr = fmt.Sprintf("%d小时 %d分钟 %d秒", hours, minutes, seconds)
|
||
} else if minutes > 0 {
|
||
uptimeStr = fmt.Sprintf("%d分钟 %d秒", minutes, seconds)
|
||
} else {
|
||
uptimeStr = fmt.Sprintf("%d秒", seconds)
|
||
}
|
||
|
||
// 获取签名信息
|
||
currentSign := algos.GlobalSignatureSwitcher.GetCurrentMethod()
|
||
|
||
// 生成签名详情显示
|
||
var signatureDetails string
|
||
if currentSign == "sign2" {
|
||
signatureDetails = "X-Gorgon + X-Khronos"
|
||
} else {
|
||
signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos"
|
||
}
|
||
|
||
// 获取统计数据
|
||
stats := getStatsSnapshot()
|
||
|
||
// 获取设备密钥池状态
|
||
poolStatus := getDeviceKeyPoolStatus()
|
||
|
||
// 获取最近请求(日志未启用时为空)
|
||
var recent []map[string]interface{}
|
||
for _, e := range fqapi.GetRecentRequests(20) {
|
||
recent = append(recent, map[string]interface{}{
|
||
"Time": time.Unix(e.Ts, 0).Format("15:04:05"),
|
||
"IP": e.IP,
|
||
"Method": e.Method,
|
||
"Path": e.Path,
|
||
"Query": truncateStr(e.Query, 80),
|
||
"Status": e.Status,
|
||
"IsErr": e.Status >= 400,
|
||
"Duration": fmt.Sprintf("%dms", e.DurationMs),
|
||
"UA": truncateStr(e.UserAgent, 60),
|
||
})
|
||
}
|
||
|
||
// 近 7 天请求量(日志未启用时为空);最后一天即今日
|
||
var todayCount int64
|
||
daily := fqapi.GetDailyRequestStats(7)
|
||
if n := len(daily); n > 0 {
|
||
todayCount = daily[n-1].Count
|
||
}
|
||
|
||
statusTmpl.Execute(w, map[string]interface{}{
|
||
"NowTime": time.Now().Format("2006-01-02 15:04:05"),
|
||
"Uptime": uptimeStr,
|
||
"StartTime": startTime.Format("2006-01-02 15:04:05"),
|
||
"Signature": signatureDetails,
|
||
"DBTotal": getDBTotalSize(),
|
||
"DBUsedPct": getDBUsagePercentage(),
|
||
"DBUsed": getDBUsedSize(),
|
||
"DBFree": getDBFreeSize(),
|
||
"PoolStatus": poolStatus["status"],
|
||
"PoolTotal": poolStatus["total"],
|
||
"PoolValid": poolStatus["valid"],
|
||
"PoolInvalid": poolStatus["invalid"],
|
||
"PoolIndex": poolStatus["current_index"],
|
||
"StatTotal": stats["total"],
|
||
"StatSearch": stats["search"],
|
||
"StatDetail": stats["detail"],
|
||
"StatCatalog": stats["catalog"],
|
||
"StatContent": stats["content"],
|
||
"StatManga": stats["manga"],
|
||
"StatAudio": stats["audio"],
|
||
"StatVideo": stats["video"],
|
||
"StatCategory": stats["category"],
|
||
"TodayCount": todayCount,
|
||
"DailyStats": daily,
|
||
"RateLimit": getRateLimitDesc(),
|
||
"CacheCount": getDBCacheCount(),
|
||
"CacheStatus": getCacheStatus(),
|
||
"CleanupStatus": getCleanupStatus(),
|
||
"CleanupInterval": getCleanupInterval(),
|
||
"RecentRequests": recent,
|
||
})
|
||
}
|
||
|
||
// truncateStr 截断字符串,超长加省略号
|
||
func truncateStr(s string, n int) string {
|
||
if len(s) <= n {
|
||
return s
|
||
}
|
||
return s[:n] + "…"
|
||
}
|
||
|
||
// getRateLimitDesc 当前每 IP 限流描述(server.rate_limit_qps,0=不限制)
|
||
func getRateLimitDesc() string {
|
||
cfg := config.GetAppConfig()
|
||
if cfg == nil || cfg.Server.RateLimitQPS <= 0 {
|
||
return "未启用"
|
||
}
|
||
return fmt.Sprintf("%.0f QPS/IP", cfg.Server.RateLimitQPS)
|
||
}
|
||
|
||
// getCacheStatus 获取缓存状态
|
||
func getCacheStatus() string {
|
||
cfg := config.GetConfig()
|
||
if cfg != nil && cfg.Cache.Enabled {
|
||
return "已启用"
|
||
}
|
||
return "已禁用"
|
||
}
|
||
|
||
// getCleanupStatus 获取清理状态
|
||
func getCleanupStatus() string {
|
||
cfg := config.GetConfig()
|
||
if cfg != nil && cfg.Cache.CleanupEnabled {
|
||
return "已启用"
|
||
}
|
||
return "已禁用"
|
||
}
|
||
|
||
// getCleanupInterval 获取清理间隔
|
||
func getCleanupInterval() string {
|
||
cfg := config.GetConfig()
|
||
if cfg != nil {
|
||
seconds := cfg.Cache.CleanupSeconds
|
||
if seconds == 0 {
|
||
return "不清理"
|
||
} else if seconds < 60 {
|
||
return fmt.Sprintf("%d秒", seconds)
|
||
} else if seconds < 3600 {
|
||
return fmt.Sprintf("%d分钟", seconds/60)
|
||
} else {
|
||
return fmt.Sprintf("%d小时", seconds/3600)
|
||
}
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// formatBytes 格式化字节大小
|
||
func formatBytes(bytes int64) string {
|
||
const unit = 1024
|
||
if bytes < unit {
|
||
return fmt.Sprintf("%d B", bytes)
|
||
}
|
||
div, exp := int64(unit), 0
|
||
for n := bytes / unit; n >= unit; n /= unit {
|
||
div *= unit
|
||
exp++
|
||
}
|
||
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||
}
|
||
|
||
// getDBCacheCount 获取数据库缓存记录数
|
||
func getDBCacheCount() int {
|
||
stats, err := fqapi.GetDatabaseStats()
|
||
if err != nil || stats == nil {
|
||
return 0
|
||
}
|
||
if count, ok := stats["cache_count"].(int); ok {
|
||
return count
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// getDBTotalSize 获取数据库总大小
|
||
func getDBTotalSize() string {
|
||
stats, err := fqapi.GetDatabaseStats()
|
||
if err != nil || stats == nil {
|
||
return "未知"
|
||
}
|
||
if size, ok := stats["total_size_bytes"].(int64); ok {
|
||
return formatBytes(size)
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// getDBUsedSize 获取数据库已用空间
|
||
func getDBUsedSize() string {
|
||
stats, err := fqapi.GetDatabaseStats()
|
||
if err != nil || stats == nil {
|
||
return "未知"
|
||
}
|
||
if size, ok := stats["used_size_bytes"].(int64); ok {
|
||
return formatBytes(size)
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// getDBFreeSize 获取数据库空闲空间(带颜色提示)
|
||
func getDBFreeSize() string {
|
||
stats, err := fqapi.GetDatabaseStats()
|
||
if err != nil || stats == nil {
|
||
return "未知"
|
||
}
|
||
freeSize, okFree := stats["free_size_bytes"].(int64)
|
||
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
||
|
||
if !okFree {
|
||
return "未知"
|
||
}
|
||
|
||
if freeSize == 0 {
|
||
return "0 B"
|
||
}
|
||
|
||
freeSizeStr := formatBytes(freeSize)
|
||
|
||
// 如果有总大小,计算并显示百分比
|
||
if okTotal && totalSize > 0 {
|
||
percentage := float64(freeSize) * 100 / float64(totalSize)
|
||
return fmt.Sprintf("%s (%.1f%%)", freeSizeStr, percentage)
|
||
}
|
||
|
||
return freeSizeStr
|
||
}
|
||
|
||
// getDBUsagePercentage 获取数据库使用率
|
||
func getDBUsagePercentage() float64 {
|
||
stats, err := fqapi.GetDatabaseStats()
|
||
if err != nil || stats == nil {
|
||
return 0
|
||
}
|
||
usedSize, okUsed := stats["used_size_bytes"].(int64)
|
||
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
||
|
||
if !okUsed || !okTotal || totalSize == 0 {
|
||
return 0
|
||
}
|
||
|
||
return float64(usedSize) * 100 / float64(totalSize)
|
||
}
|
||
|
||
// getDeviceKeyPoolStatus 获取设备密钥池状态
|
||
func getDeviceKeyPoolStatus() map[string]interface{} {
|
||
pool := fqapi.GetDeviceKeyPool()
|
||
|
||
if !pool.IsEnabled() {
|
||
return map[string]interface{}{
|
||
"status": "未启用",
|
||
"total": 0,
|
||
"valid": 0,
|
||
"invalid": 0,
|
||
"current_index": 0,
|
||
}
|
||
}
|
||
|
||
stats := pool.GetStats()
|
||
return map[string]interface{}{
|
||
"status": "已启用",
|
||
"total": stats["total"],
|
||
"valid": stats["valid"],
|
||
"invalid": stats["invalid"],
|
||
"current_index": stats["current_index"],
|
||
}
|
||
}
|