diff --git a/admin/dashboard/templates/index.html b/admin/dashboard/templates/index.html index 4fdbcef..5ca4ea4 100644 --- a/admin/dashboard/templates/index.html +++ b/admin/dashboard/templates/index.html @@ -92,9 +92,23 @@ - 系统运行时间 - {% raw %}{{ formattedUptime }}{% endraw %} - 当前实例持续在线时长 + 新增用户数 + {% raw %}{{ newUsers }}{% endraw %} + 统计周期内首次触发用户 + + + + + 群渗透率 + {% raw %}{{ avgGroupPenetration.toFixed(2) }}{% endraw %}% + 活跃群触发人数占成员比例(均值) + + + + + 群健康分 + {% raw %}{{ groupHealthScore.toFixed(2) }}{% endraw %} + 综合成功率与响应时延评分 @@ -256,7 +270,10 @@ successRate: 0, activeUsers: 0, activeGroups: 0, + newUsers: 0, avgResponseTime: 0, + avgGroupPenetration: 0, + groupHealthScore: 0, topUsers: [], topGroups: [], topPlugins: [], @@ -425,7 +442,10 @@ this.successRate = parseFloat(data.success_rate) || 0; this.activeUsers = data.active_users || 0; this.activeGroups = data.active_groups || 0; + this.newUsers = data.new_users || 0; this.avgResponseTime = parseFloat(data.avg_response_time) || 0; + this.avgGroupPenetration = parseFloat(data.avg_group_penetration) || 0; + this.groupHealthScore = parseFloat(data.group_health_score) || 0; this.topUsers = data.top_users || []; this.topGroups = data.top_groups || []; this.topPlugins = data.top_plugins || []; @@ -791,8 +811,21 @@ margin-bottom: 16px; } + .hero-row, + .stats-highlight-row { + display: flex; + flex-wrap: wrap; + align-items: stretch; + } + + .hero-row > .el-col, + .stats-highlight-row > .el-col { + display: flex; + } + .hero-card { min-height: 228px; + height: 100%; } .hero-card--profile { @@ -894,10 +927,13 @@ .metric-grid .el-col { margin-bottom: 16px; + display: flex; } .metric-card { min-height: 106px; + width: 100%; + height: 100%; } .metric-card .el-card__body { @@ -942,6 +978,11 @@ padding-top: 12px !important; } + .insight-card { + width: 100%; + height: 100%; + } + .section-heading, .chart-card-header { display: flex; diff --git a/db/scripts/init.sql b/db/scripts/init.sql index 3f8c4df..ef58306 100644 --- a/db/scripts/init.sql +++ b/db/scripts/init.sql @@ -232,6 +232,27 @@ create or replace index idx_group_id create or replace index idx_last_used_at on message_archive.t_group_stats (last_used_at); +create or replace table message_archive.t_group_command_user_stats +( + id bigint auto_increment + primary key, + group_id varchar(50) not null comment '群组ID', + plugin_name varchar(50) not null comment '插件名称', + command varchar(50) not null comment '触发命令', + user_id varchar(50) not null comment '用户ID', + first_used_at datetime not null comment '首次触发时间', + last_used_at datetime not null comment '最近触发时间', + constraint uk_group_plugin_command_user + unique (group_id, plugin_name, command, user_id) +) + comment '群命令用户去重追踪表'; + +create or replace index idx_group_plugin_command + on message_archive.t_group_command_user_stats (group_id, plugin_name, command); + +create or replace index idx_group_command_user_last_used + on message_archive.t_group_command_user_stats (last_used_at); + create or replace table message_archive.t_plugin_point_config ( id int auto_increment diff --git a/db/stats_db.py b/db/stats_db.py index 191fea4..fea8d16 100644 --- a/db/stats_db.py +++ b/db/stats_db.py @@ -9,6 +9,31 @@ class StatsDBOperator(BaseDBOperator): def __init__(self, db_manager: DBConnectionManager): super().__init__(db_manager) + self._group_user_tracker_ready = False + + def _ensure_group_user_tracker_table(self) -> bool: + """确保群维度唯一用户追踪表存在""" + if self._group_user_tracker_ready: + return True + + sql = """ + CREATE TABLE IF NOT EXISTS t_group_command_user_stats ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + group_id VARCHAR(50) NOT NULL COMMENT '群组ID', + plugin_name VARCHAR(50) NOT NULL COMMENT '插件名称', + command VARCHAR(50) NOT NULL COMMENT '触发命令', + user_id VARCHAR(50) NOT NULL COMMENT '用户ID', + first_used_at DATETIME NOT NULL COMMENT '首次触发时间', + last_used_at DATETIME NOT NULL COMMENT '最近触发时间', + UNIQUE KEY uk_group_plugin_command_user (group_id, plugin_name, command, user_id), + INDEX idx_group_plugin_command (group_id, plugin_name, command), + INDEX idx_last_used_at (last_used_at) + ) COMMENT='群命令用户去重追踪表' + """ + ok = self.execute_update(sql) + if ok: + self._group_user_tracker_ready = True + return ok def record_plugin_call(self, plugin_name: str, command: str, user_id: str, group_id: Optional[str], success: bool, @@ -209,15 +234,31 @@ class StatsDBOperator(BaseDBOperator): 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 + # 记录“群 + 插件 + 命令 + 用户”去重关系,再计算该群该命令唯一用户数 + unique_users = 1 + if self._ensure_group_user_tracker_table(): + upsert_unique_user_sql = """ + INSERT INTO t_group_command_user_stats + (group_id, plugin_name, command, user_id, first_used_at, last_used_at) + VALUES (%s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + last_used_at = VALUES(last_used_at) + """ + self.execute_update( + upsert_unique_user_sql, + (group_id, plugin_name, command, user_id, now, now) + ) + + user_query_sql = """ + SELECT COUNT(*) as user_count + FROM t_group_command_user_stats + WHERE group_id = %s AND plugin_name = %s AND command = %s + """ + user_query_params = (group_id, 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 and user_result.get('user_count') is not None else 1 + elif result: + unique_users = max(result.get('unique_users') or 0, 1) if result: # 更新现有记录 @@ -413,6 +454,15 @@ class StatsDBOperator(BaseDBOperator): """ 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 + + # 3.1 新增用户(统计窗口内首次触发) + new_users_sql = """ + SELECT COUNT(DISTINCT user_id) as new_users + FROM t_user_stats + WHERE first_used_at >= %s + """ + new_users_result = self.execute_query(new_users_sql, (start_date_str,), fetch_one=True) + new_users = new_users_result['new_users'] if new_users_result else 0 # 4. 活跃群组数 active_groups_sql = """ @@ -440,6 +490,48 @@ class StatsDBOperator(BaseDBOperator): """ 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 + + # 6.1 群渗透率(活跃群内,触发人数 / 群成员数 的均值) + group_penetration_sql = """ + SELECT AVG(group_penetration) AS avg_group_penetration + FROM ( + SELECT + gs.group_id, + CASE + WHEN gm.member_count > 0 + THEN (LEAST(gs.unique_users, gm.member_count) / gm.member_count) * 100 + ELSE NULL + END AS group_penetration + FROM ( + SELECT group_id, MAX(unique_users) AS unique_users + FROM t_group_stats + WHERE last_used_at >= %s + GROUP BY group_id + ) gs + LEFT JOIN ( + SELECT chatroom_id AS group_id, COUNT(*) AS member_count + FROM t_chatroom_member + WHERE status = 1 OR status IS NULL + GROUP BY chatroom_id + ) gm ON gs.group_id = gm.group_id + ) t + WHERE group_penetration IS NOT NULL + """ + group_penetration_result = self.execute_query(group_penetration_sql, (start_date_str,), fetch_one=True) + avg_group_penetration = 0 + if group_penetration_result and group_penetration_result.get('avg_group_penetration') is not None: + avg_group_penetration = float(group_penetration_result['avg_group_penetration']) + + # 6.2 群健康分(成功率 + 响应速度融合评分) + # 响应时间评分: 500ms 及以内为满分, 5000ms 及以上趋近0分 + latency_score = 0 + if avg_response_time <= 500: + latency_score = 100 + elif avg_response_time >= 5000: + latency_score = 0 + else: + latency_score = max(0, min(100, ((5000 - avg_response_time) / 4500) * 100)) + group_health_score = (success_rate * 0.7) + (latency_score * 0.3) # 7. 最常用的插件 top_plugins_sql = """ @@ -479,9 +571,12 @@ class StatsDBOperator(BaseDBOperator): "total_calls": total_calls, "success_rate": success_rate, "active_users": active_users, + "new_users": new_users, "active_groups": active_groups, "error_count": error_count, "avg_response_time": avg_response_time, + "avg_group_penetration": round(avg_group_penetration, 2), + "group_health_score": round(group_health_score, 2), "top_plugins": top_plugins, "top_users": top_users, "top_groups": top_groups