fix SiteStatistic

This commit is contained in:
jxxghp
2025-06-09 15:23:16 +08:00
parent b38ca9c653
commit b80666ebb1
2 changed files with 96 additions and 175 deletions

View File

@@ -3,11 +3,12 @@
"name": "站点数据统计",
"description": "站点统计数据图表。",
"labels": "站点,仪表板",
"version": "1.6",
"version": "1.7",
"icon": "statistic.png",
"author": "lightolly,jxxghp",
"level": 2,
"history": {
"v1.7": "优化内存占用",
"v1.6": "优化了站点数据获取失败时的回退逻辑",
"v1.5": "修复了发送增量通知失败等一些问题",
"v1.4.1": "支持数据刷新时发送消息通知",

View File

@@ -1,11 +1,10 @@
import gc
import warnings
from collections import defaultdict
from datetime import datetime, timedelta
from threading import Lock
from typing import Optional, Any, List, Dict, Tuple
import pytz
from app.helper.sites import SitesHelper
from apscheduler.schedulers.background import BackgroundScheduler
from app import schemas
@@ -14,6 +13,7 @@ from app.core.config import settings
from app.core.event import eventmanager, Event
from app.db.models.siteuserdata import SiteUserData
from app.db.site_oper import SiteOper
from app.helper.sites import SitesHelper
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import EventType, NotificationType
@@ -32,7 +32,7 @@ class SiteStatistic(_PluginBase):
# 插件图标
plugin_icon = "statistic.png"
# 插件版本
plugin_version = "1.6"
plugin_version = "1.7"
# 插件作者
plugin_author = "lightolly,jxxghp"
# 作者主页
@@ -263,63 +263,50 @@ class SiteStatistic(_PluginBase):
获取最近一次统计的日期、最近一次统计的站点数据、上一次的站点数据
如果上一次某个站点数据缺失,则 fallback 到该站点之前最近有数据的日期
"""
# 获取所有原始数据
raw_data_list: List[SiteUserData] = SiteOper().get_userdata()
# 优化:只获取最近的站点数据,而不是所有历史数据
raw_data_list: List[SiteUserData] = SiteOper().get_userdata_latest()
if not raw_data_list:
return "", [], []
# 每个日期、每个站点只保留最后一条数据
data_list = list({f"{data.updated_day}_{data.name}": data for data in raw_data_list}.values())
# 按日期倒序排序
data_list.sort(key=lambda x: x.updated_day, reverse=True)
# 按日期分组数据
data_by_day = defaultdict(list)
for data in data_list:
data_by_day[data.updated_day].append(data)
# 获取最近一次统计的日期
latest_day = data_list[0].updated_day
latest_day = raw_data_list[0].updated_day
# 最近一次统计数据按上传量降序排序
latest_data = [data for data in raw_data_list if data.updated_day == latest_day]
latest_data.sort(key=lambda x: x.upload or 0, reverse=True)
# 筛选最近一次统计的数据(可能为空)
latest_data = [data for data in data_list if data.updated_day == latest_day]
# 最近一次统计按上传量降序排序
latest_data.sort(key=lambda x: x.upload, reverse=True)
# 获取所有日期倒序排序后的列表
sorted_dates = sorted(data_by_day.keys(), reverse=True)
# 计算前一天的日期字符串(相对于最近一次日期)
# 计算前一天的日期字符串
previous_day_str = (datetime.strptime(latest_day, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
# 获取前一天的站点数据
previous_day_sites = data_by_day.get(previous_day_str, [])
# 构建前一天站点到数据的映射
previous_by_site = {data.name: data for data in previous_day_sites}
# 获取前一天的数据
previous_data_list = SiteOper().get_userdata_by_date(previous_day_str)
previous_by_site = {data.name: data for data in previous_data_list}
# 准备查找早于前一天的日期列表,用于 fallback
fallback_dates = [d for d in sorted_dates if d < previous_day_str]
# 按站点细化进行上一次数据的 fallback 处理
# 为当前站点查找对应的前一天数据
previous_data = []
for current_site in latest_data:
site_name = current_site.name
# 优先尝试获取前一天的同一站点数据
site_prev = previous_by_site.get(site_name)
# 如果前一天没有该站点数据,则进行逐日回退查找
if site_prev is None or site_prev.err_msg:
for d in fallback_dates:
# 在每个候选日期中查找对应站点数据
candidate = next((x for x in data_by_day[d] if x.name == site_name), None)
if candidate:
# 如果前一天没有该站点数据,尝试查找更早的数据
if not site_prev or site_prev.err_msg:
# 最多回溯7天避免查询过多历史数据
for i in range(2, 8):
fallback_date = (datetime.strptime(latest_day, "%Y-%m-%d") - timedelta(days=i)).strftime("%Y-%m-%d")
fallback_data_list = SiteOper().get_userdata_by_date(fallback_date)
fallback_by_site = {data.name: data for data in fallback_data_list}
candidate = fallback_by_site.get(site_name)
if candidate and not candidate.err_msg:
site_prev = candidate
break
# 如果找到了上一次的数据,加入结果列表
if site_prev:
previous_data.append(site_prev)
# 清理临时变量,帮助垃圾收集
del raw_data_list, previous_data_list, previous_by_site
gc.collect()
return latest_day, latest_data, previous_data
@staticmethod
@@ -846,152 +833,80 @@ class SiteStatistic(_PluginBase):
dashboard='all'
)
# 站点数据明细
site_trs = [
{
'component': 'tr',
'props': {
'class': 'text-sm'
},
'content': [
{
'component': 'td',
'props': {
'class': 'whitespace-nowrap break-keep text-high-emphasis'
},
'text': data.name
},
{
'component': 'td',
'text': data.username
},
{
'component': 'td',
'text': data.user_level
},
{
'component': 'td',
'props': {
'class': 'text-success'
},
'text': StringUtils.str_filesize(data.upload)
},
{
'component': 'td',
'props': {
'class': 'text-error'
},
'text': StringUtils.str_filesize(data.download)
},
{
'component': 'td',
'text': data.ratio
},
{
'component': 'td',
'text': format_bonus(data.bonus or 0)
},
{
'component': 'td',
'text': data.seeding
},
{
'component': 'td',
'text': StringUtils.str_filesize(data.seeding_size)
}
]
} for data in stattistic_data
# 优化:使用更轻量级的方式构建站点数据明细,避免创建过多嵌套对象
# 先准备表头
table_headers = [
{'text': '站点', 'class': 'text-start ps-4'},
{'text': '用户名', 'class': 'text-start ps-4'},
{'text': '用户等级', 'class': 'text-start ps-4'},
{'text': '上传量', 'class': 'text-start ps-4'},
{'text': '下载量', 'class': 'text-start ps-4'},
{'text': '分享率', 'class': 'text-start ps-4'},
{'text': '魔力值', 'class': 'text-start ps-4'},
{'text': '做种数', 'class': 'text-start ps-4'},
{'text': '做种体积', 'class': 'text-start ps-4'}
]
# 构建表头行
header_row = {
'component': 'thead',
'content': [
{
'component': 'th',
'props': {'class': header['class']},
'text': header['text']
} for header in table_headers
]
}
# 构建数据行,避免在列表推导式中创建复杂嵌套
table_rows = []
for data in stattistic_data:
# 预先计算所有需要的值
row_data = [
{'text': data.name, 'class': 'whitespace-nowrap break-keep text-high-emphasis'},
{'text': data.username, 'class': ''},
{'text': data.user_level, 'class': ''},
{'text': StringUtils.str_filesize(data.upload), 'class': 'text-success'},
{'text': StringUtils.str_filesize(data.download), 'class': 'text-error'},
{'text': data.ratio, 'class': ''},
{'text': format_bonus(data.bonus or 0), 'class': ''},
{'text': data.seeding, 'class': ''},
{'text': StringUtils.str_filesize(data.seeding_size), 'class': ''}
]
# 构建单行配置
row_content = []
for cell_data in row_data:
cell = {'component': 'td', 'text': cell_data['text']}
if cell_data['class']:
cell['props'] = {'class': cell_data['class']}
row_content.append(cell)
table_rows.append({
'component': 'tr',
'props': {'class': 'text-sm'},
'content': row_content
})
# 拼装页面
return [
page = [
{
'component': 'VRow',
'content': site_totals + [
# 各站点数据明细
{
'component': 'VCol',
'props': {
'cols': 12,
},
'props': {'cols': 12},
'content': [
{
'component': 'VTable',
'props': {
'hover': True
},
'props': {'hover': True},
'content': [
{
'component': 'thead',
'content': [
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '站点'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '用户名'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '用户等级'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '上传量'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '下载量'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '分享率'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '魔力值'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '做种数'
},
{
'component': 'th',
'props': {
'class': 'text-start ps-4'
},
'text': '做种体积'
}
]
},
header_row,
{
'component': 'tbody',
'content': site_trs
'content': table_rows
}
]
}
@@ -1001,6 +916,11 @@ class SiteStatistic(_PluginBase):
}
]
# 清理垃圾
gc.collect()
return page
def stop_service(self):
pass