优化首页指标展示并修复群唯一用户统计
变更项: 1. 修复首页卡片等高问题,统一用户信息与右侧指标区高度,统一热门用户/群组/插件卡片高度。 2. 首页新增三个分析指标:新增用户数、群渗透率、群健康分,并完成前端数据绑定。 3. 优化仪表盘摘要接口,新增 new_users、avg_group_penetration、group_health_score 返回字段。 4. 修复 t_group_stats.unique_users 统计口径,改为按 group_id+plugin_name+command+user_id 去重统计,避免跨群串数据。 5. 新增 t_group_command_user_stats 表结构及索引,并补充到 init.sql。
This commit is contained in:
@@ -92,9 +92,23 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="metric-card metric-card--soft" shadow="hover">
|
||||
<div class="metric-label">系统运行时间</div>
|
||||
<div class="metric-value">{% raw %}{{ formattedUptime }}{% endraw %}</div>
|
||||
<div class="metric-footnote">当前实例持续在线时长</div>
|
||||
<div class="metric-label">新增用户数</div>
|
||||
<div class="metric-value">{% raw %}{{ newUsers }}{% endraw %}</div>
|
||||
<div class="metric-footnote">统计周期内首次触发用户</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="metric-card" shadow="hover">
|
||||
<div class="metric-label">群渗透率</div>
|
||||
<div class="metric-value">{% raw %}{{ avgGroupPenetration.toFixed(2) }}{% endraw %}%</div>
|
||||
<div class="metric-footnote">活跃群触发人数占成员比例(均值)</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="metric-card metric-card--soft" shadow="hover">
|
||||
<div class="metric-label">群健康分</div>
|
||||
<div class="metric-value">{% raw %}{{ groupHealthScore.toFixed(2) }}{% endraw %}</div>
|
||||
<div class="metric-footnote">综合成功率与响应时延评分</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
113
db/stats_db.py
113
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
|
||||
|
||||
Reference in New Issue
Block a user