diff --git a/package.v2.json b/package.v2.json index 68cf8c4..34f2149 100644 --- a/package.v2.json +++ b/package.v2.json @@ -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": "支持数据刷新时发送消息通知", diff --git a/plugins.v2/sitestatistic/__init__.py b/plugins.v2/sitestatistic/__init__.py index ed6cad0..3dcfeca 100644 --- a/plugins.v2/sitestatistic/__init__.py +++ b/plugins.v2/sitestatistic/__init__.py @@ -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