Files
fanqie-go/internal/handlers/status.go
T
chenxin bfb4290c15 ✨ feat(status): 新增请求日志并重设计状态页
- 新增 server.request_log 配置:记录业务请求元数据(IP/方法/路径/状态/耗时,
  key 自动脱敏)与 4xx/5xx 错误摘要(前 4KB),按保留天数自动清理
- 移除 /self-info 与 /vacuum 接口及相关文档
- 状态页重设计:毛玻璃卡片+吸顶玻璃栏、服务状态条、KPI 卡与请求分类
  条形图、可折叠 API 文档分组、行内测试面板;深浅色分别调校,
  含 reduced-motion / reduced-transparency / 无 backdrop-filter 兜底
2026-10-09 18:03:02 +08:00

266 lines
6.7 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),
})
}
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"],
"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] + "…"
}
// 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"],
}
}