Files
abot/db/stats_db.py
2025-12-17 14:48:40 +08:00

558 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union, Tuple
from db.base import BaseDBOperator
from db.connection import DBConnectionManager
class StatsDBOperator(BaseDBOperator):
"""统计数据库操作类"""
def __init__(self, db_manager: DBConnectionManager):
super().__init__(db_manager)
def record_plugin_call(self, plugin_name: str, command: str, user_id: str,
group_id: Optional[str], success: bool,
process_time_ms: float) -> bool:
"""记录插件调用信息
Args:
plugin_name: 插件名称
command: 触发的命令
user_id: 用户ID
group_id: 群组ID私聊为None
success: 是否调用成功
process_time_ms: 处理时间(毫秒)
Returns:
是否成功记录
"""
# 1. 更新插件统计汇总表
self._update_plugin_stats(plugin_name, command, success, process_time_ms,
True if group_id else False)
# 2. 更新用户使用统计表
self._update_user_stats(user_id, plugin_name, command, success)
# 3. 如果是群聊,更新群组使用统计表
if group_id:
self._update_group_stats(group_id, plugin_name, command, user_id, success)
return True
def record_error(self, plugin_name: str, command: str, user_id: str,
group_id: Optional[str], error_message: str,
stack_trace: Optional[str] = None) -> bool:
"""记录错误信息
Args:
plugin_name: 插件名称
command: 触发的命令
user_id: 用户ID
group_id: 群组ID私聊为None
error_message: 错误信息
stack_trace: 堆栈跟踪
Returns:
是否成功记录
"""
sql = """
INSERT INTO t_error_logs
(plugin_name, command, user_id, group_id, error_message, stack_trace)
VALUES (%s, %s, %s, %s, %s, %s)
"""
params = (plugin_name, command, user_id, group_id, error_message, stack_trace)
return self.execute_update(sql, params)
def _update_plugin_stats(self, plugin_name: str, command: str, success: bool,
process_time_ms: float, is_group: bool) -> bool:
"""更新插件统计汇总表
Args:
plugin_name: 插件名称
command: 触发的命令
success: 是否调用成功
process_time_ms: 处理时间(毫秒)
is_group: 是否群聊
Returns:
是否成功更新
"""
today = datetime.now().strftime("%Y-%m-%d")
# 先查询当前记录
query_sql = """
SELECT id, total_calls, success_calls, failed_calls, group_calls,
private_calls, avg_process_time
FROM t_plugin_stats
WHERE plugin_name = %s AND command = %s AND stat_date = %s
"""
query_params = (plugin_name, command, today)
result = self.execute_query(query_sql, query_params, fetch_one=True)
if result:
# 更新现有记录
total_calls = result['total_calls'] + 1
success_calls = result['success_calls'] + (1 if success else 0)
failed_calls = result['failed_calls'] + (0 if success else 1)
group_calls = result['group_calls'] + (1 if is_group else 0)
private_calls = result['private_calls'] + (0 if is_group else 1)
# 计算新的平均处理时间
old_avg = result['avg_process_time']
new_avg = ((old_avg * (total_calls - 1)) + process_time_ms) / total_calls
update_sql = """
UPDATE t_plugin_stats
SET total_calls = %s,
success_calls = %s,
failed_calls = %s,
group_calls = %s,
private_calls = %s,
avg_process_time = %s
WHERE id = %s
"""
update_params = (total_calls, success_calls, failed_calls, group_calls,
private_calls, new_avg, result['id'])
return self.execute_update(update_sql, update_params)
else:
# 插入新记录
insert_sql = """
INSERT INTO t_plugin_stats
(plugin_name, command, stat_date, total_calls, success_calls,
failed_calls, group_calls, private_calls, avg_process_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
insert_params = (
plugin_name, command, today, 1,
1 if success else 0,
0 if success else 1,
1 if is_group else 0,
0 if is_group else 1,
process_time_ms
)
return self.execute_update(insert_sql, insert_params)
def _update_user_stats(self, user_id: str, plugin_name: str,
command: str, success: bool) -> bool:
"""更新用户使用统计表
Args:
user_id: 用户ID
plugin_name: 插件名称
command: 触发的命令
success: 是否调用成功
Returns:
是否成功更新
"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 查询是否存在记录
query_sql = """
SELECT id, total_calls, success_calls, failed_calls, first_used_at
FROM t_user_stats
WHERE user_id = %s AND plugin_name = %s AND command = %s
"""
query_params = (user_id, plugin_name, command)
result = self.execute_query(query_sql, query_params, fetch_one=True)
if result:
# 更新现有记录
update_sql = """
UPDATE t_user_stats
SET total_calls = total_calls + 1,
success_calls = success_calls + %s,
failed_calls = failed_calls + %s,
last_used_at = %s
WHERE id = %s
"""
update_params = (1 if success else 0, 0 if success else 1, now, result['id'])
return self.execute_update(update_sql, update_params)
else:
# 插入新记录
insert_sql = """
INSERT INTO t_user_stats
(user_id, plugin_name, command, total_calls, success_calls,
failed_calls, first_used_at, last_used_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
"""
insert_params = (
user_id, plugin_name, command, 1,
1 if success else 0,
0 if success else 1,
now, now
)
return self.execute_update(insert_sql, insert_params)
def _update_group_stats(self, group_id: str, plugin_name: str,
command: str, user_id: str, success: bool) -> bool:
"""更新群组使用统计表
Args:
group_id: 群组ID
plugin_name: 插件名称
command: 触发的命令
user_id: 用户ID
success: 是否调用成功
Returns:
是否成功更新
"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 查询是否存在记录
query_sql = """
SELECT id, total_calls, success_calls, failed_calls, unique_users, first_used_at
FROM t_group_stats
WHERE group_id = %s AND plugin_name = %s AND command = %s
"""
query_params = (group_id, plugin_name, command)
result = self.execute_query(query_sql, query_params, fetch_one=True)
# 查询该命令的唯一用户
user_query_sql = """
SELECT COUNT(DISTINCT user_id) as user_count
FROM t_user_stats
WHERE plugin_name = %s AND command = %s
"""
user_query_params = (plugin_name, command)
user_result = self.execute_query(user_query_sql, user_query_params, fetch_one=True)
unique_users = user_result['user_count'] if user_result else 1
if result:
# 更新现有记录
update_sql = """
UPDATE t_group_stats
SET total_calls = total_calls + 1,
success_calls = success_calls + %s,
failed_calls = failed_calls + %s,
unique_users = %s,
last_used_at = %s
WHERE id = %s
"""
update_params = (1 if success else 0, 0 if success else 1,
unique_users, now, result['id'])
return self.execute_update(update_sql, update_params)
else:
# 插入新记录
insert_sql = """
INSERT INTO t_group_stats
(group_id, plugin_name, command, total_calls, success_calls,
failed_calls, unique_users, first_used_at, last_used_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
insert_params = (
group_id, plugin_name, command, 1,
1 if success else 0,
0 if success else 1,
unique_users, now, now
)
return self.execute_update(insert_sql, insert_params)
def get_plugin_stats(self, days: int = 7) -> List[Dict]:
"""获取插件使用统计
Args:
days: 统计天数
Returns:
插件统计列表
"""
sql = """
SELECT plugin_name, command,
SUM(total_calls) as total_calls,
SUM(success_calls) as success_calls,
SUM(failed_calls) as failed_calls,
AVG(avg_process_time) as avg_process_time
FROM t_plugin_stats
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
GROUP BY plugin_name, command
ORDER BY total_calls DESC
"""
return self.execute_query(sql, (days,)) or []
def get_user_stats(self, days: int = 7, limit: int = 20) -> List[Dict]:
"""获取用户使用统计
Args:
days: 统计天数
limit: 返回记录数量限制
Returns:
用户统计列表
"""
sql = """
SELECT user_id,
SUM(total_calls) as total_calls,
SUM(success_calls) as success_calls,
SUM(failed_calls) as failed_calls,
COUNT(DISTINCT plugin_name) as used_plugins
FROM t_user_stats
WHERE last_used_at >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
GROUP BY user_id
ORDER BY total_calls DESC
LIMIT %s
"""
return self.execute_query(sql, (days, limit)) or []
def get_group_stats(self, days: int = 7, limit: int = 20) -> List[Dict]:
"""获取群组使用统计
Args:
days: 统计天数
limit: 返回记录数量限制
Returns:
群组统计列表
"""
sql = """
SELECT group_id,
SUM(total_calls) as total_calls,
SUM(success_calls) as success_calls,
SUM(failed_calls) as failed_calls,
COUNT(DISTINCT plugin_name) as used_plugins,
MAX(unique_users) as unique_users
FROM t_group_stats
WHERE last_used_at >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
GROUP BY group_id
ORDER BY total_calls DESC
LIMIT %s
"""
return self.execute_query(sql, (days, limit)) or []
def get_error_logs(self, days: int = 7, page: int = 1, limit: int = 20) -> Tuple[List[Dict], int]:
"""获取错误日志
Args:
days: 统计天数
page: 页码
limit: 每页数量
Returns:
(日志列表, 总数)
"""
# 1. 获取总数
count_sql = """
SELECT COUNT(*) as total
FROM t_error_logs
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
"""
count_result = self.execute_query(count_sql, (days,), fetch_one=True)
total = count_result['total'] if count_result else 0
# 2. 获取分页数据
offset = (page - 1) * limit
sql = """
SELECT id, plugin_name, command, user_id, group_id,
error_message, stack_trace, created_at
FROM t_error_logs
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
ORDER BY created_at DESC
LIMIT %s OFFSET %s
"""
logs = self.execute_query(sql, (days, limit, offset)) or []
return logs, total
def get_error_detail(self, error_id: int) -> Optional[Dict]:
"""获取错误详情
Args:
error_id: 错误ID
Returns:
错误详情
"""
sql = """
SELECT id, plugin_name, command, user_id, group_id,
error_message, stack_trace, created_at
FROM t_error_logs
WHERE id = %s
"""
return self.execute_query(sql, (error_id,), fetch_one=True)
def get_dashboard_summary(self, days: int = 7) -> Dict:
"""获取仪表盘摘要数据
Args:
days: 统计天数
Returns:
仪表盘摘要数据
"""
# 获取时间范围
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
start_date_str = start_date.strftime("%Y-%m-%d")
# 1. 总调用次数
total_calls_sql = """
SELECT SUM(total_calls) as total_calls
FROM t_plugin_stats
WHERE stat_date >= %s
"""
total_calls_result = self.execute_query(total_calls_sql, (start_date_str,), fetch_one=True)
total_calls = total_calls_result['total_calls'] if total_calls_result and total_calls_result['total_calls'] else 0
# 2. 成功率
success_rate_sql = """
SELECT SUM(success_calls) as success_calls, SUM(total_calls) as total_calls
FROM t_plugin_stats
WHERE stat_date >= %s
"""
success_rate_result = self.execute_query(success_rate_sql, (start_date_str,), fetch_one=True)
success_rate = 0
if success_rate_result and success_rate_result['total_calls']:
success_rate = (success_rate_result['success_calls'] / success_rate_result['total_calls']) * 100
# 3. 活跃用户数
active_users_sql = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM t_user_stats
WHERE last_used_at >= %s
"""
active_users_result = self.execute_query(active_users_sql, (start_date_str,), fetch_one=True)
active_users = active_users_result['active_users'] if active_users_result else 0
# 4. 活跃群组数
active_groups_sql = """
SELECT COUNT(DISTINCT group_id) as active_groups
FROM t_group_stats
WHERE last_used_at >= %s
"""
active_groups_result = self.execute_query(active_groups_sql, (start_date_str,), fetch_one=True)
active_groups = active_groups_result['active_groups'] if active_groups_result else 0
# 5. 错误数量
error_count_sql = """
SELECT COUNT(*) as error_count
FROM t_error_logs
WHERE created_at >= %s
"""
error_count_result = self.execute_query(error_count_sql, (start_date_str,), fetch_one=True)
error_count = error_count_result['error_count'] if error_count_result else 0
# 6. 平均响应时间
avg_response_time_sql = """
SELECT AVG(avg_process_time) as avg_response_time
FROM t_plugin_stats
WHERE stat_date >= %s
"""
avg_response_time_result = self.execute_query(avg_response_time_sql, (start_date_str,), fetch_one=True)
avg_response_time = avg_response_time_result['avg_response_time'] if avg_response_time_result and avg_response_time_result['avg_response_time'] else 0
# 7. 最常用的插件
top_plugins_sql = """
SELECT plugin_name, SUM(total_calls) as total_calls
FROM t_plugin_stats
WHERE stat_date >= %s
GROUP BY plugin_name
ORDER BY total_calls DESC
LIMIT 5
"""
top_plugins = self.execute_query(top_plugins_sql, (start_date_str,)) or []
# 8. 最活跃的用户
top_users_sql = """
SELECT user_id, SUM(total_calls) as total_calls
FROM t_user_stats
WHERE last_used_at >= %s
GROUP BY user_id
ORDER BY total_calls DESC
LIMIT 5
"""
top_users = self.execute_query(top_users_sql, (start_date_str,)) or []
# 9. 最活跃的群组
top_groups_sql = """
SELECT group_id, SUM(total_calls) as total_calls
FROM t_group_stats
WHERE last_used_at >= %s
GROUP BY group_id
ORDER BY total_calls DESC
LIMIT 5
"""
top_groups = self.execute_query(top_groups_sql, (start_date_str,)) or []
# 返回汇总数据
return {
"total_calls": total_calls,
"success_rate": success_rate,
"active_users": active_users,
"active_groups": active_groups,
"error_count": error_count,
"avg_response_time": avg_response_time,
"top_plugins": top_plugins,
"top_users": top_users,
"top_groups": top_groups
}
def get_plugin_trend(self, plugin_name: str = "", days: int = 7) -> List[Dict]:
"""获取插件使用趋势数据
Args:
plugin_name: 插件名称,为空则获取所有插件
days: 统计天数
Returns:
插件使用趋势数据
"""
# 获取时间范围内的每一天
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days-1) # 包含今天所以减1
if plugin_name:
# 获取特定插件的趋势
sql = """
SELECT stat_date, SUM(total_calls) as total_calls,
SUM(success_calls) as success_calls,
SUM(failed_calls) as failed_calls
FROM t_plugin_stats
WHERE plugin_name = %s AND stat_date >= %s
GROUP BY stat_date
ORDER BY stat_date
"""
params = (plugin_name, start_date)
else:
# 获取所有插件的趋势
sql = """
SELECT stat_date, SUM(total_calls) as total_calls,
SUM(success_calls) as success_calls,
SUM(failed_calls) as failed_calls
FROM t_plugin_stats
WHERE stat_date >= %s
GROUP BY stat_date
ORDER BY stat_date
"""
params = (start_date,)
results = self.execute_query(sql, params) or []
# 将结果转换为按日期的字典
trend_by_date = {r['stat_date'].strftime('%Y-%m-%d'): r for r in results}
# 确保每一天都有数据
trend_data = []
current_date = start_date
while current_date <= end_date:
date_str = current_date.strftime('%Y-%m-%d')
if date_str in trend_by_date:
data = trend_by_date[date_str]
trend_data.append({
'date': date_str,
'total_calls': data['total_calls'],
'success_calls': data['success_calls'],
'failed_calls': data['failed_calls'],
'success_rate': (data['success_calls'] / data['total_calls'] * 100) if data['total_calls'] > 0 else 0
})
else:
trend_data.append({
'date': date_str,
'total_calls': 0,
'success_calls': 0,
'failed_calls': 0,
'success_rate': 0
})
current_date += timedelta(days=1)
return trend_data