Files
fanqie-go/internal/handlers/status.go
T
chenxin d710d382ee 🐛 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 中的冗余依赖条目
2026-09-29 19:47:04 +08:00

1023 lines
37 KiB
Go

package handlers
import (
"encoding/json"
"fanqie/internal/algos"
"fanqie/internal/config"
"fanqie/internal/fqapi"
"fmt"
"net/http"
"time"
)
// HandleVacuum 处理数据库压缩请求
func HandleVacuum(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 只允许 POST 请求
if r.Method != http.MethodPost {
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "仅支持 POST 请求",
})
return
}
// 执行 VACUUM
err := fqapi.VacuumDatabase()
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "压缩失败: " + err.Error(),
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "数据库压缩完成",
})
}
// 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()
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄小说API - 运行状态</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
background: #f5f5f5;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: #333;
margin-bottom: 30px;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.header h1 {
font-size: 2em;
margin-bottom: 8px;
color: #1a1a1a;
}
.header p {
font-size: 1em;
color: #666;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
}
.card-title {
font-size: 1.1em;
color: #333;
margin-bottom: 15px;
font-weight: 600;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
}
.stat-item:last-child {
border-bottom: none;
}
.stat-label {
color: #666;
font-size: 0.95em;
}
.stat-value {
color: #1a1a1a;
font-weight: 600;
font-size: 0.95em;
}
.api-docs {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
}
.api-section {
margin-bottom: 25px;
}
.api-section:last-child {
margin-bottom: 0;
}
.api-section-title {
color: #1a1a1a;
font-size: 1em;
font-weight: 600;
margin-bottom: 12px;
padding-left: 20px;
position: relative;
}
.api-section-title::before {
content: "▸";
position: absolute;
left: 0;
color: #666;
}
.api-endpoint {
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
}
.api-endpoint:last-child {
margin-bottom: 0;
}
.api-method {
display: inline-block;
background: #2196F3;
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 0.75em;
font-weight: 600;
margin-right: 8px;
}
.api-path {
font-family: "Consolas", "Monaco", monospace;
font-size: 0.9em;
color: #333;
font-weight: 500;
}
.api-desc {
color: #666;
font-size: 0.85em;
margin-top: 6px;
padding-left: 4px;
}
.api-params {
margin-top: 8px;
padding-left: 4px;
}
.param-item {
display: flex;
align-items: center;
font-size: 0.8em;
margin: 4px 0;
color: #555;
}
.param-name {
font-family: "Consolas", "Monaco", monospace;
color: #e91e63;
font-weight: 500;
margin-right: 6px;
}
.param-required {
background: #ff9800;
color: white;
padding: 1px 5px;
border-radius: 2px;
font-size: 0.75em;
margin-right: 6px;
}
.param-optional {
background: #2196F3;
color: white;
padding: 1px 5px;
border-radius: 2px;
font-size: 0.75em;
margin-right: 6px;
}
.param-desc {
color: #777;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: 600;
margin-left: 8px;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-info {
background: #d1ecf1;
color: #0c5460;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-clickable {
cursor: pointer;
transition: all 0.2s;
}
.badge-clickable:hover {
background: #ffc107;
color: #000;
transform: scale(1.05);
}
.badge-clickable:active {
transform: scale(0.95);
}
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: #333;
color: white;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 9999;
animation: slideIn 0.3s ease-out;
max-width: 300px;
}
.toast.success {
background: #4caf50;
}
.toast.error {
background: #f44336;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
</style>
<script type="text/javascript">
let autoRefreshInterval;
window.onload = function() {
startAutoRefresh();
}
function startAutoRefresh() {
autoRefreshInterval = setInterval(function() {
fetch(window.location.href)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newBody = doc.querySelector('body');
document.body.innerHTML = newBody.innerHTML;
})
.catch(err => console.error('刷新失败:', err));
}, 3000);
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = 'toast ' + type;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
}
function performVacuum() {
stopAutoRefresh();
const badge = event.target;
const originalText = badge.textContent;
badge.textContent = '压缩中...';
badge.style.cursor = 'wait';
const urlParams = new URLSearchParams(window.location.search);
const key = urlParams.get('key');
const vacuumUrl = key ? '/vacuum?key=' + encodeURIComponent(key) : '/vacuum';
fetch(vacuumUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => {
location.reload();
}, 1000);
} else {
showToast(data.message, 'error');
badge.textContent = originalText;
badge.style.cursor = 'pointer';
startAutoRefresh();
}
})
.catch(err => {
showToast('压缩失败: ' + err.message, 'error');
badge.textContent = originalText;
badge.style.cursor = 'pointer';
startAutoRefresh();
});
}
</script>
</head>
<body>
<div class="container">
<div class="header">
<h1>番茄小说API</h1>
<p>运行状态监控面板</p>
</div>
<div class="grid">
<div class="card">
<div class="card-title">● 运行状态</div>
<div class="stat-item">
<span class="stat-label">服务器时间</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">运行时长</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">启动时间</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item" style="flex-direction: column; align-items: flex-start;">
<span class="stat-label" style="margin-bottom: 8px;">签名方法</span>
<span class="stat-value" style="font-size: 13px; line-height: 1.6;">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">数据库大小</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">已用空间</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">空闲空间</span>
<span class="stat-value">%s <span class="badge badge-warning badge-clickable" onclick="performVacuum()" title="点击立即压缩数据库">立即压缩</span></span>
</div>
</div>
<div class="card">
<div class="card-title">◉ 设备密钥池</div>
<div class="stat-item">
<span class="stat-label">池状态</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">总设备数</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">有效设备</span>
<span class="stat-value">%d <span class="badge badge-success">可用</span></span>
</div>
<div class="stat-item">
<span class="stat-label">无效设备</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">当前索引</span>
<span class="stat-value">%d</span>
</div>
</div>
<div class="card">
<div class="card-title">■ 请求统计</div>
<div class="stat-item">
<span class="stat-label">总请求数</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">搜索请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">详情请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">目录请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">内容请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">漫画请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">音频请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">视频请求</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">分类请求</span>
<span class="stat-value">%d</span>
</div>
</div>
<div class="card">
<div class="card-title">◆ 缓存统计</div>
<div class="stat-item">
<span class="stat-label">正文缓存数</span>
<span class="stat-value">%d</span>
</div>
<div class="stat-item">
<span class="stat-label">正文缓存</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">自动清理</span>
<span class="stat-value">%s</span>
</div>
<div class="stat-item">
<span class="stat-label">清理间隔</span>
<span class="stat-value">%s</span>
</div>
</div>
</div>
<div class="api-docs">
<div class="card-title">◈ API 接口文档</div>
<!-- 书籍相关 -->
<div class="api-section">
<div class="api-section-title">书籍相关</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/search</span></div>
<div class="api-desc">◆ 搜索书籍</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">query</span>
<span class="param-required">必需</span>
<span class="param-desc">搜索关键字</span>
</div>
<div class="param-item">
<span class="param-name">offset</span>
<span class="param-optional">可选</span>
<span class="param-desc">偏移量,默认0</span>
</div>
<div class="param-item">
<span class="param-name">tab_type</span>
<span class="param-optional">可选</span>
<span class="param-desc">类型,默认3</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/detail</span></div>
<div class="api-desc">◆ 获取书籍详情</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/catalog</span></div>
<div class="api-desc">◆ 获取书籍目录</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/multi-detail</span></div>
<div class="api-desc">◆ 批量获取书籍详情</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID列表,逗号分隔</span>
</div>
</div>
</div>
</div>
<!-- 内容相关 -->
<div class="api-section">
<div class="api-section-title">内容相关</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/content</span></div>
<div class="api-desc">◆ 获取章节正文内容</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_id</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID,支持路径参数或查询参数</span>
</div>
<div class="param-item">
<span class="param-name">text_mode</span>
<span class="param-optional">可选</span>
<span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span>
</div>
<div class="param-item">
<span class="param-name">image_mode</span>
<span class="param-optional">可选</span>
<span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span>
</div>
<div class="param-item">
<span class="param-name">review</span>
<span class="param-optional">可选</span>
<span class="param-desc">段评功能:1=启用,0=禁用</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/multi-content</span></div>
<div class="api-desc">◆ 批量获取多个章节正文</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
<div class="param-item">
<span class="param-name">item_ids</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID列表,多个ID用逗号分隔,如:123,456,789</span>
</div>
<div class="param-item">
<span class="param-name">text_mode</span>
<span class="param-optional">可选</span>
<span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span>
</div>
<div class="param-item">
<span class="param-name">image_mode</span>
<span class="param-optional">可选</span>
<span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span>
</div>
<div class="param-item">
<span class="param-name">ignore_failed</span>
<span class="param-optional">可选</span>
<span class="param-desc">忽略失败章节:1/true=忽略,0/false=不忽略</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/item-summary</span></div>
<div class="api-desc">◆ 获取章节摘要信息</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
<div class="param-item">
<span class="param-name">item_ids</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID列表,逗号分隔</span>
</div>
</div>
</div>
</div>
<!-- 段评相关 -->
<div class="api-section">
<div class="api-section-title">段评相关</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/review</span></div>
<div class="api-desc">◆ 获取段落评论、回复</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_id</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID</span>
</div>
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
<div class="param-item">
<span class="param-name">para</span>
<span class="param-required">必需</span>
<span class="param-desc">段落索引(从0开始)</span>
</div>
<div class="param-item">
<span class="param-name">comment_id</span>
<span class="param-optional">可选</span>
<span class="param-desc">评论ID(获取回复时需要)</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/review_html</span></div>
<div class="api-desc">◆ 获取段落评论HTML页面</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_id</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID</span>
</div>
<div class="param-item">
<span class="param-name">book_id</span>
<span class="param-required">必需</span>
<span class="param-desc">书籍ID</span>
</div>
<div class="param-item">
<span class="param-name">para</span>
<span class="param-required">必需</span>
<span class="param-desc">段落索引(从0开始)</span>
</div>
</div>
</div>
</div>
<!-- 多媒体 -->
<div class="api-section">
<div class="api-section-title">多媒体</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/manga</span></div>
<div class="api-desc">◆ 获取漫画内容</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_ids</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID</span>
</div>
<div class="param-item">
<span class="param-name">output</span>
<span class="param-optional">可选</span>
<span class="param-desc">输出格式:url/proxy/data_uri,默认url</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/audio</span></div>
<div class="api-desc">◆ 获取音频信息</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_ids</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID</span>
</div>
<div class="param-item">
<span class="param-name">tone_id</span>
<span class="param-optional">可选</span>
<span class="param-desc">音色ID,默认1</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/video</span></div>
<div class="api-desc">◆ 获取视频信息</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">item_ids</span>
<span class="param-required">必需</span>
<span class="param-desc">章节ID</span>
</div>
</div>
</div>
</div>
<!-- 分类 -->
<div class="api-section">
<div class="api-section-title">分类</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/category-front</span></div>
<div class="api-desc">◆ 获取分类首页</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">tab</span>
<span class="param-optional">可选</span>
<span class="param-desc">标签页,默认0</span>
</div>
</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/category-landing</span></div>
<div class="api-desc">◆ 获取分类详情页</div>
<div class="api-params">
<div class="param-item">
<span class="param-name">category_id</span>
<span class="param-required">必需</span>
<span class="param-desc">分类ID</span>
</div>
<div class="param-item">
<span class="param-name">offset</span>
<span class="param-optional">可选</span>
<span class="param-desc">偏移量,默认0</span>
</div>
<div class="param-item">
<span class="param-name">limit</span>
<span class="param-optional">可选</span>
<span class="param-desc">每页数量,默认20</span>
</div>
</div>
</div>
</div>
<!-- 系统 -->
<div class="api-section">
<div class="api-section-title">系统</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/status</span></div>
<div class="api-desc">◆ 系统状态页面(当前页面)</div>
</div>
<div class="api-endpoint">
<div><span class="api-method">GET</span><span class="api-path">/self-info</span></div>
<div class="api-desc">◆ 设备信息(包含敏感数据)</div>
</div>
</div>
</div>
</div>
</body>
</html>
`,
time.Now().Format("2006-01-02 15:04:05"),
uptimeStr,
startTime.Format("2006-01-02 15:04:05"),
signatureDetails,
getDBTotalSize(),
getDBUsedSize(),
getDBFreeSize(),
poolStatus["status"],
poolStatus["total"],
poolStatus["valid"],
poolStatus["invalid"],
poolStatus["current_index"],
stats["total"],
stats["search"],
stats["detail"],
stats["catalog"],
stats["content"],
stats["manga"],
stats["audio"],
stats["video"],
stats["category"],
getDBCacheCount(),
getCacheStatus(),
getCleanupStatus(),
getCleanupInterval(),
)
fmt.Fprint(w, html)
}
// 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"],
}
}