新增仪表盘和统计分析数据API,优化外链过期时间处理,完善日志记录功能,更新前端样式以提升用户体验,确保数据统计准确性和可视化展示。
This commit is contained in:
parent
aad2182d0c
commit
c4ce2fc5a4
BIN
database.db
BIN
database.db
Binary file not shown.
101
main.py
101
main.py
@ -9,12 +9,31 @@ from werkzeug.routing import BaseConverter
|
||||
from utils.login import login_quark
|
||||
from utils.tools import get_cnb_weburl
|
||||
from utils.detebase import CloudDriveDatabase
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
#app = Flask(__name__,template_folder='templates') #修改模板目录
|
||||
app.jinja_env.auto_reload = True
|
||||
|
||||
# --- Logging Setup Start ---
|
||||
if not os.path.exists('logs'):
|
||||
os.mkdir('logs')
|
||||
|
||||
# Use RotatingFileHandler to prevent log file from growing indefinitely
|
||||
file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
))
|
||||
file_handler.setLevel(logging.INFO) # Set level to INFO to capture info, warning, error
|
||||
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('Flask App startup')
|
||||
# --- Logging Setup End ---
|
||||
|
||||
# 数据库配置
|
||||
DATABASE = 'database.db'
|
||||
|
||||
@ -149,17 +168,33 @@ def qrlink(id):
|
||||
|
||||
if link_info:
|
||||
# 检查是否已过期
|
||||
from datetime import datetime
|
||||
expiry_time = link_info.get('expiry_time')
|
||||
if expiry_time:
|
||||
try:
|
||||
expiry_datetime = datetime.strptime(expiry_time, '%Y-%m-%d %H:%M:%S')
|
||||
if datetime.now() > expiry_datetime:
|
||||
# 使用 fromisoformat 解析 ISO 8601 UTC 字符串
|
||||
# Python < 3.11 doesn't handle Z directly, remove it.
|
||||
if expiry_time.endswith('Z'):
|
||||
expiry_time_str = expiry_time[:-1] + '+00:00'
|
||||
else:
|
||||
expiry_time_str = expiry_time # Assume it might already be offset-aware or naive
|
||||
|
||||
expiry_datetime = datetime.fromisoformat(expiry_time_str)
|
||||
|
||||
# Ensure expiry_datetime is offset-aware (UTC)
|
||||
if expiry_datetime.tzinfo is None:
|
||||
# If parsing resulted in naive, assume it was UTC as intended
|
||||
expiry_datetime = expiry_datetime.replace(tzinfo=timezone.utc)
|
||||
|
||||
# 获取当前 UTC 时间进行比较
|
||||
if datetime.now(timezone.utc) > expiry_datetime:
|
||||
data["message"] = "此外链已过期"
|
||||
return render_template('exlink_error.html', message=data["message"])
|
||||
except (ValueError, TypeError):
|
||||
# 如果日期格式有误,忽略过期检查
|
||||
pass
|
||||
except (ValueError, TypeError) as e:
|
||||
# 解析失败,记录错误,并可能视为无效链接
|
||||
print(f"Error parsing expiry_time '{expiry_time}': {e}")
|
||||
data["message"] = "外链信息有误(无效的过期时间)"
|
||||
return render_template('exlink_error.html', message=data["message"])
|
||||
|
||||
|
||||
# 检查使用次数是否超过限制
|
||||
used_quota = link_info.get('used_quota', 0)
|
||||
@ -456,8 +491,60 @@ def delete_external_link():
|
||||
return Exlink().delete()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 新增:仪表盘数据 API
|
||||
@app.route('/admin/dashboard_data', methods=['GET'])
|
||||
def get_dashboard_data():
|
||||
db = get_db()
|
||||
try:
|
||||
user_drives_count = db.get_total_user_drives_count()
|
||||
active_links_count = db.get_active_external_links_count()
|
||||
# 使用外链总数作为"今日访问量"的简化替代
|
||||
total_links_count = db.get_total_external_links_count()
|
||||
|
||||
data = {
|
||||
"status": True,
|
||||
"data": {
|
||||
"user_drives_count": user_drives_count,
|
||||
"active_links_count": active_links_count,
|
||||
"total_links_count": total_links_count
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取仪表盘数据错误: {e}")
|
||||
data = {"status": False, "message": "获取仪表盘数据失败"}
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
# 新增:统计分析数据 API
|
||||
@app.route('/admin/statistics_data', methods=['GET'])
|
||||
def get_statistics_data():
|
||||
db = get_db()
|
||||
try:
|
||||
drives_by_provider = db.get_user_drives_count_by_provider()
|
||||
# 这里可以添加更多统计数据,例如外链访问趋势 (需要记录访问日志)
|
||||
# 暂时只返回网盘分布数据
|
||||
|
||||
data = {
|
||||
"status": True,
|
||||
"data": {
|
||||
"drives_by_provider": drives_by_provider,
|
||||
# "access_trend": [] # 示例:将来可以添加访问趋势数据
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取统计数据错误: {e}")
|
||||
data = {"status": False, "message": "获取统计数据失败"}
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 应用程序运行入口
|
||||
# -----------------------------
|
||||
if __name__ == '__main__':
|
||||
# 初始化数据库 (如果需要)
|
||||
# init_db()
|
||||
|
||||
# 启动Flask应用
|
||||
weburl = get_cnb_weburl(5000)
|
||||
print("Run_url:",weburl)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
@ -33,6 +33,89 @@
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Digital Cockpit Styles */
|
||||
body {
|
||||
/* Optional: Set a base background for the whole page */
|
||||
/* background-color: #1a1a2e; */
|
||||
/* color: #e0e0e0; */
|
||||
}
|
||||
.main-content {
|
||||
/* background-color: #1f1f38; /* Slightly lighter background for content */
|
||||
/* border-radius: 10px; */
|
||||
/* margin-top: 10px; */
|
||||
}
|
||||
#dashboard {
|
||||
/* background: linear-gradient(to bottom right, #2a2a4a, #1f1f38); */
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
/* box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); */
|
||||
}
|
||||
#dashboard h2, #statistics h2 {
|
||||
color: #343a40; /* Keep headings readable */
|
||||
border-bottom: 2px solid #0d6efd;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 30px !important; /* Override default margin */
|
||||
}
|
||||
.kpi-card {
|
||||
background-color: #ffffff; /* White card on potentially dark background */
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
margin-bottom: 20px; /* Add spacing between cards */
|
||||
color: #333;
|
||||
overflow: hidden; /* Ensure consistency */
|
||||
position: relative; /* For potential pseudo-elements */
|
||||
}
|
||||
.kpi-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
|
||||
}
|
||||
.kpi-card .card-body {
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
.kpi-card .card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d; /* Muted color for title */
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.kpi-card .kpi-value {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 700;
|
||||
color: #0d6efd; /* Highlight color for value */
|
||||
line-height: 1.2;
|
||||
}
|
||||
.kpi-card .kpi-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 10px;
|
||||
color: #0d6efd;
|
||||
opacity: 0.7;
|
||||
display: block; /* Center icon */
|
||||
}
|
||||
/* Style for charts container */
|
||||
#statistics .card {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#statistics .card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
#statistics .card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #343a40;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -74,26 +157,29 @@
|
||||
<h2 class="mb-4">系统概览</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card kpi-card">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-cloud-fill kpi-icon"></i>
|
||||
<h5 class="card-title">网盘账号总数</h5>
|
||||
<p class="card-text display-4">0</p>
|
||||
<p class="kpi-value">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card kpi-card">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-link-45deg kpi-icon"></i>
|
||||
<h5 class="card-title">活跃外链数</h5>
|
||||
<p class="card-text display-4">0</p>
|
||||
<p class="kpi-value">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card kpi-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">今日访问量</h5>
|
||||
<p class="card-text display-4">0</p>
|
||||
<i class="bi bi-bar-chart-line-fill kpi-icon"></i>
|
||||
<h5 class="card-title">外链总数</h5>
|
||||
<p class="kpi-value">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -434,33 +520,141 @@
|
||||
// showMessage('错误信息', 'error');
|
||||
// showMessage('提示信息', 'info');
|
||||
|
||||
// 初始化图表
|
||||
const accessCtx = document.getElementById('accessChart').getContext('2d');
|
||||
new Chart(accessCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
datasets: [{
|
||||
label: '访问量',
|
||||
data: [0, 0, 0, 0, 0, 0, 0],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1
|
||||
}]
|
||||
}
|
||||
});
|
||||
// 初始化图表变量
|
||||
let accessChartInstance = null;
|
||||
let storageChartInstance = null;
|
||||
|
||||
const storageCtx = document.getElementById('storageChart').getContext('2d');
|
||||
new Chart(storageCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['阿里云盘', '百度网盘'],
|
||||
datasets: [{
|
||||
data: [0, 0],
|
||||
backgroundColor: ['#ff6384', '#36a2eb']
|
||||
}]
|
||||
}
|
||||
});
|
||||
// 初始化图表函数
|
||||
function initCharts() {
|
||||
const accessCtx = document.getElementById('accessChart').getContext('2d');
|
||||
accessChartInstance = new Chart(accessCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [], // 初始为空,稍后填充
|
||||
datasets: [{
|
||||
label: '访问量', // 暂时保留,实际数据需记录日志
|
||||
data: [], // 初始为空
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#6c757d' } // X轴标签颜色
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(108, 117, 125, 0.2)' }, // Y轴网格线颜色
|
||||
ticks: { color: '#6c757d' } // Y轴标签颜色
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#343a40' } // 图例文字颜色
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const storageCtx = document.getElementById('storageChart').getContext('2d');
|
||||
storageChartInstance = new Chart(storageCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [], // 初始为空
|
||||
datasets: [{
|
||||
data: [], // 初始为空
|
||||
backgroundColor: [ // 提供更多颜色以备不时之需
|
||||
'#ff6384',
|
||||
'#36a2eb',
|
||||
'#ffce56',
|
||||
'#4bc0c0',
|
||||
'#9966ff',
|
||||
'#ff9f40'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: { color: '#343a40' } // 图例文字颜色
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新仪表盘数据函数
|
||||
function updateDashboard() {
|
||||
$.ajax({
|
||||
url: '/admin/dashboard_data',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.status && response.data) {
|
||||
// 更新美化后的KPI卡片值
|
||||
$('#dashboard .kpi-value').eq(0).text(response.data.user_drives_count || 0);
|
||||
$('#dashboard .kpi-value').eq(1).text(response.data.active_links_count || 0);
|
||||
$('#dashboard .kpi-value').eq(2).text(response.data.total_links_count || 0);
|
||||
} else {
|
||||
showMessage('获取仪表盘数据失败', 'error');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('获取仪表盘数据出错:', error);
|
||||
showMessage('获取仪表盘数据时发生网络错误', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新统计图表函数
|
||||
function updateStatisticsCharts() {
|
||||
$.ajax({
|
||||
url: '/admin/statistics_data',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.status && response.data) {
|
||||
// 更新网盘使用分布图 (Doughnut Chart)
|
||||
if (response.data.drives_by_provider && storageChartInstance) {
|
||||
const providerData = response.data.drives_by_provider;
|
||||
const labels = Object.keys(providerData);
|
||||
const data = Object.values(providerData);
|
||||
|
||||
storageChartInstance.data.labels = labels;
|
||||
storageChartInstance.data.datasets[0].data = data;
|
||||
storageChartInstance.update();
|
||||
}
|
||||
|
||||
// 更新外链访问趋势图 (Line Chart) - 需要后端实现日志记录和数据提供
|
||||
// 示例:假设 response.data.access_trend = [{date: '2023-01-01', count: 10}, ...]
|
||||
// if (response.data.access_trend && accessChartInstance) {
|
||||
// const trendData = response.data.access_trend;
|
||||
// accessChartInstance.data.labels = trendData.map(item => item.date);
|
||||
// accessChartInstance.data.datasets[0].data = trendData.map(item => item.count);
|
||||
// accessChartInstance.update();
|
||||
// } else {
|
||||
// 如果没有访问数据,显示空状态或提示
|
||||
if (accessChartInstance) {
|
||||
accessChartInstance.data.labels = ['无数据'];
|
||||
accessChartInstance.data.datasets[0].data = [0];
|
||||
accessChartInstance.update();
|
||||
}
|
||||
// }
|
||||
} else {
|
||||
showMessage('获取统计数据失败', 'error');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('获取统计数据出错:', error);
|
||||
showMessage('获取统计数据时发生网络错误', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// json编辑器处理
|
||||
function initAceEditor(editorElement) {
|
||||
const editor = ace.edit(editorElement);
|
||||
@ -536,6 +730,14 @@
|
||||
bindAddAccountEvent(); // 初始化添加账号事件
|
||||
bindAddExlinkEvents(); // 初始化添加外链事件
|
||||
loadExternalLinks(); // 加载外链列表
|
||||
setupModalFocusManagement(); // <-- 添加此行:初始化模态框焦点管理
|
||||
|
||||
initCharts(); // <-- 新增:初始化图表
|
||||
updateDashboard(); // <-- 新增:加载仪表盘数据
|
||||
updateStatisticsCharts(); // <-- 新增:加载统计图表数据
|
||||
|
||||
// 重新加载账户列表以获取初始数据(如果需要)
|
||||
refreshAccountsList();
|
||||
});
|
||||
|
||||
// 设置默认JSON模板 (Add Modal specific)
|
||||
@ -887,8 +1089,22 @@
|
||||
row.append(`<td>${remainingCount} / ${totalQuota}</td>`);
|
||||
|
||||
// 状态
|
||||
let isExpired = false;
|
||||
if (link.expiry_time) {
|
||||
try {
|
||||
const expiryDate = new Date(link.expiry_time); // Directly parse ISO string
|
||||
if (!isNaN(expiryDate.getTime()) && new Date() > expiryDate) {
|
||||
isExpired = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing expiry_time in admin list:", link.expiry_time, e);
|
||||
// Optionally treat parse error as expired or show different status
|
||||
}
|
||||
}
|
||||
|
||||
let statusBadge = '';
|
||||
if (remainingCount <= 0) {
|
||||
// 如果剩余次数用完 或 已过期,则禁用
|
||||
if (remainingCount <= 0 || isExpired) {
|
||||
statusBadge = '<span class="badge bg-danger">已禁用</span>';
|
||||
} else {
|
||||
statusBadge = '<span class="badge bg-success">正常</span>';
|
||||
@ -1021,9 +1237,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换日期时间格式:从YYYY-MM-DDThh:mm到YYYY-MM-DD hh:mm:ss
|
||||
const expiryDate = new Date(expiry_time);
|
||||
const formattedExpiryTime = expiryDate.toISOString().replace('T', ' ').substring(0, 19);
|
||||
// 转换日期时间格式:从YYYY-MM-DDThh:mm到完整的ISO 8601 UTC字符串
|
||||
const expiryDate = new Date(expiry_time); // 解析本地时间输入
|
||||
// 检查日期是否有效
|
||||
if (isNaN(expiryDate.getTime())) {
|
||||
showMessage('无效的日期时间格式', 'error');
|
||||
return;
|
||||
}
|
||||
const expiryTimeISO = expiryDate.toISOString(); // 转换为ISO 8601 UTC字符串 (e.g., "2025-04-12T11:08:00.000Z")
|
||||
|
||||
// 发送创建外链请求
|
||||
$.ajax({
|
||||
@ -1033,7 +1254,7 @@
|
||||
data: JSON.stringify({
|
||||
drive_id: drive_id,
|
||||
total_quota: total_quota,
|
||||
expiry_time: formattedExpiryTime,
|
||||
expiry_time: expiryTimeISO, // <-- 使用完整的 ISO UTC 字符串
|
||||
remarks: remarks
|
||||
}),
|
||||
success: function(response) {
|
||||
@ -1091,6 +1312,43 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Focus management for modals to fix aria-hidden warning
|
||||
function setupModalFocusManagement() {
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
let triggerElement = null;
|
||||
|
||||
modals.forEach(modal => {
|
||||
// Store the trigger element when the modal is about to show
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
// event.relatedTarget is the element that triggered the modal
|
||||
triggerElement = event.relatedTarget;
|
||||
});
|
||||
|
||||
// Return focus when the modal is hidden
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
// Small delay to ensure the modal is fully hidden and elements are interactable
|
||||
setTimeout(() => {
|
||||
if (triggerElement && typeof triggerElement.focus === 'function') {
|
||||
// Check if trigger element still exists and is focusable
|
||||
if (document.body.contains(triggerElement)) {
|
||||
triggerElement.focus();
|
||||
} else {
|
||||
// Fallback if trigger element is gone (e.g., deleted from DOM)
|
||||
// Focusing body might not be ideal UX, consider a more specific fallback if needed
|
||||
document.body.focus();
|
||||
console.log("Modal trigger element no longer exists, focusing body.");
|
||||
}
|
||||
} else {
|
||||
// Fallback if no trigger element recorded or it's not focusable
|
||||
document.body.focus();
|
||||
console.log("No modal trigger element recorded or it's not focusable, focusing body.");
|
||||
}
|
||||
triggerElement = null; // Reset for the next modal
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -212,7 +212,7 @@
|
||||
let remainingCount = parseInt("{{ remaining_count }}") || 0;
|
||||
|
||||
const expiryTimeString = "{{ expiry_time }}";
|
||||
const expiryTime = expiryTimeString ? new Date(expiryTimeString.replace(/-/g, '/')) : null;
|
||||
const expiryTime = expiryTimeString ? new Date(expiryTimeString) : null;
|
||||
|
||||
let countdownInterval = null;
|
||||
|
||||
|
@ -320,6 +320,79 @@ class CloudDriveDatabase:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# 新增统计方法
|
||||
def get_total_user_drives_count(self) -> int:
|
||||
"""获取用户网盘总数"""
|
||||
try:
|
||||
self.cursor.execute("SELECT COUNT(*) FROM user_drives")
|
||||
result = self.cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
except Exception as e:
|
||||
print(f"获取用户网盘总数错误: {e}")
|
||||
return 0
|
||||
|
||||
def get_active_external_links_count(self) -> int:
|
||||
"""获取活跃外链数量 (未过期且有剩余次数)"""
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
now_utc_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# 注意:SQLite 不直接支持 ISO 8601 比较,此查询可能需要调整或在 Python 中过滤
|
||||
# 简单起见,我们先只检查次数和时间是否存在
|
||||
# 更精确的查询可能需要 DATETIME 函数,或在 Python 中处理
|
||||
self.cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) FROM external_links
|
||||
WHERE (used_quota < total_quota)
|
||||
AND (expiry_time IS NOT NULL AND expiry_time > ?)
|
||||
""",
|
||||
(now_utc_iso,) # 这个比较可能不适用于所有 SQLite 版本/配置,后续可能需要调整
|
||||
)
|
||||
# 备选(更兼容但效率低):获取所有链接在 Python 中过滤
|
||||
# self.cursor.execute("SELECT link_uuid, expiry_time, used_quota, total_quota FROM external_links")
|
||||
# links = self.cursor.fetchall()
|
||||
# count = 0
|
||||
# for link in links:
|
||||
# is_active = False
|
||||
# if link['used_quota'] < link['total_quota']:
|
||||
# if link['expiry_time']:
|
||||
# try:
|
||||
# expiry_dt = datetime.fromisoformat(link['expiry_time'].replace('Z', '+00:00'))
|
||||
# if datetime.now(timezone.utc) < expiry_dt:
|
||||
# is_active = True
|
||||
# except: pass # Ignore parsing errors
|
||||
# else: # No expiry time means active if quota is available
|
||||
# is_active = True
|
||||
# if is_active:
|
||||
# count += 1
|
||||
# return count
|
||||
|
||||
result = self.cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
except Exception as e:
|
||||
print(f"获取活跃外链数量错误: {e}")
|
||||
return 0 # 返回 0 或其他错误指示
|
||||
|
||||
def get_total_external_links_count(self) -> int:
|
||||
"""获取外链总数"""
|
||||
try:
|
||||
self.cursor.execute("SELECT COUNT(*) FROM external_links")
|
||||
result = self.cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
except Exception as e:
|
||||
print(f"获取外链总数错误: {e}")
|
||||
return 0
|
||||
|
||||
def get_user_drives_count_by_provider(self) -> Dict[str, int]:
|
||||
"""按提供商统计用户网盘数量"""
|
||||
try:
|
||||
self.cursor.execute("SELECT provider_name, COUNT(*) as count FROM user_drives GROUP BY provider_name")
|
||||
results = self.cursor.fetchall()
|
||||
return {row['provider_name']: row['count'] for row in results}
|
||||
except Exception as e:
|
||||
print(f"按提供商统计用户网盘数量错误: {e}")
|
||||
return {}
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
if self.conn:
|
||||
|
Loading…
x
Reference in New Issue
Block a user