增强社交统计并扩展 value_rank 社交榜单命令

- 在消息入库增量链路中回填 unique_interactors,实现去重互动人数实时更新

- 新增社交热度榜命令:社交热度榜 [名次],按互动分/被@/主动@展示

- 新增搭子榜命令:搭子榜 [名次],按无向关系边聚合展示成员组合

- 扩展 ValueRankDB 查询接口,支持社交热度与搭子关系窗口聚合

- 同步更新 value_rank 配置命令列表与帮助文案
This commit is contained in:
liuwei
2026-04-21 13:45:44 +08:00
parent d4d290fec8
commit 0dc55297bb
3 changed files with 151 additions and 4 deletions

View File

@@ -258,10 +258,63 @@ class MessageStorageDB(BaseDBOperator):
""",
receiver_social_rows,
)
# 4) 回填 unique_interactors针对本条消息受影响的用户实时重算“去重互动人数”。
affected_user_ids = [sender_id, *clean_mentioned_ids]
self._refresh_unique_interactors(stat_date, group_id, affected_user_ids)
except Exception as e:
# 社交图统计属于增强链路,不能反向影响主消息入库稳定性。
self.LOG.error(f"写入社交图增量数据失败: {e}")
def _refresh_unique_interactors(self, stat_date: str, group_id: str, user_ids: List[str]) -> None:
"""重算并回填用户在指定日期内的去重互动人数。
定义:
- 某用户当天主动@过的人 + 被谁@过(去重并集)
"""
if not user_ids:
return
deduped_user_ids = []
seen = set()
for uid in user_ids:
normalized_uid = str(uid or "").strip()
if not normalized_uid or normalized_uid in seen:
continue
seen.add(normalized_uid)
deduped_user_ids.append(normalized_uid)
for uid in deduped_user_ids:
try:
row = self.execute_query(
"""
SELECT COUNT(DISTINCT partner_id) AS partner_count
FROM (
SELECT mentioned_user_id AS partner_id
FROM t_message_mentions
WHERE stat_date = %s AND group_id = %s AND sender_id = %s
UNION
SELECT sender_id AS partner_id
FROM t_message_mentions
WHERE stat_date = %s AND group_id = %s AND mentioned_user_id = %s
) t
""",
(stat_date, group_id, uid, stat_date, group_id, uid),
fetch_one=True,
) or {}
partner_count = int(row.get("partner_count") or 0)
self.execute_update(
"""
UPDATE t_value_rank_social_daily
SET unique_interactors = %s, update_time = CURRENT_TIMESTAMP
WHERE stat_date = %s AND group_id = %s AND user_id = %s
""",
(partner_count, stat_date, group_id, uid),
)
except Exception as e:
self.LOG.error(f"回填 unique_interactors 失败: group={group_id}, user={uid}, err={e}")
def get_recent_messages(self, group_id: str, hours_ago: int = 8, min_content_length: int = 6) -> List[Dict]:
"""获取最近的消息"""
sql = """

View File

@@ -1,12 +1,14 @@
[ValueRank]
enable = true
command = ["我的身价", "身价排行", "身价说明", "重算身价"]
command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"]
command-format = """
📊 身价系统命令:
1. 我的身价
2. 身价排行 [名次]
3. 身价说明
4. 重算身价(管理员)
3. 社交热度榜 [名次]
4. 搭子榜 [名次]
5. 身价说明
6. 重算身价(管理员)
"""
# 统计窗口(天)

View File

@@ -216,6 +216,41 @@ class ValueRankDB(BaseDBOperator):
"""
return self.execute_query(sql, (stat_date, group_id, limit)) or []
def get_social_hot_rankings(self, group_id: str, social_window_days: int, limit: int) -> List[Dict[str, Any]]:
"""读取社交热度榜(窗口聚合)。"""
sql = """
SELECT
user_id,
SUM(mentioned_count) AS mentioned_count_window,
SUM(mention_others_count) AS mention_others_count_window,
SUM(unique_interactors) AS unique_interactors_window,
SUM(interaction_score) AS interaction_score_window
FROM t_value_rank_social_daily
WHERE group_id = %s
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
GROUP BY user_id
ORDER BY interaction_score_window DESC, mentioned_count_window DESC
LIMIT %s
"""
return self.execute_query(sql, (group_id, social_window_days, limit)) or []
def get_top_partner_pairs(self, group_id: str, social_window_days: int, limit: int) -> List[Dict[str, Any]]:
"""读取搭子榜(无向边聚合)。"""
sql = """
SELECT
LEAST(from_user_id, to_user_id) AS user_a,
GREATEST(from_user_id, to_user_id) AS user_b,
SUM(mention_count) AS mention_count_window,
SUM(interaction_score) AS interaction_score_window
FROM t_social_edges_daily
WHERE group_id = %s
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
GROUP BY LEAST(from_user_id, to_user_id), GREATEST(from_user_id, to_user_id)
ORDER BY interaction_score_window DESC, mention_count_window DESC
LIMIT %s
"""
return self.execute_query(sql, (group_id, social_window_days, limit)) or []
class ValueRankPlugin(MessagePluginInterface):
"""群成员身价排行插件。
@@ -268,7 +303,7 @@ class ValueRankPlugin(MessagePluginInterface):
# 配置默认值:即使未配置 config.toml也能以保守参数运行。
self.enable = True
self._commands = ["我的身价", "身价排行", "身价说明", "重算身价"]
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"]
self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价"
self.message_window_days = 7
@@ -369,6 +404,18 @@ class ValueRankPlugin(MessagePluginInterface):
await bot.send_text_message(roomid, text, sender)
return True, "查询成功"
if command == "社交热度榜":
limit = self._parse_rank_limit(content)
text = await self._build_social_hot_text(roomid, limit)
await bot.send_text_message(roomid, text, sender)
return True, "查询成功"
if command == "搭子榜":
limit = self._parse_rank_limit(content)
text = await self._build_partner_pairs_text(roomid, limit)
await bot.send_text_message(roomid, text, sender)
return True, "查询成功"
if command == "身价说明":
await bot.send_text_message(roomid, self._build_explain_text(), sender)
return True, "查询成功"
@@ -648,6 +695,51 @@ class ValueRankPlugin(MessagePluginInterface):
return "\n".join(lines)
async def _build_social_hot_text(self, group_id: str, limit: int) -> str:
"""构建“社交热度榜”输出文本。"""
if not self.db:
return "❌ 身价模块未初始化"
rows = self.db.get_social_hot_rankings(group_id, self.social_window_days, limit)
if not rows:
return "📊 近期暂无社交热度数据。"
cm = ContactManager.get_instance()
lines = [f"🔥 社交热度榜(近{self.social_window_days}天 Top{len(rows)}"]
for idx, row in enumerate(rows, start=1):
user_id = str(row.get("user_id") or "")
score = float(row.get("interaction_score_window") or 0.0)
mentioned_count = int(row.get("mentioned_count_window") or 0)
mention_others_count = int(row.get("mention_others_count_window") or 0)
nick = cm.get_group_name(group_id, user_id) or user_id
medal = "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"{idx}."
lines.append(
f"{medal} {nick} | 热度{score:.1f} | 被@{mentioned_count} | 主动@{mention_others_count}"
)
return "\n".join(lines)
async def _build_partner_pairs_text(self, group_id: str, limit: int) -> str:
"""构建“搭子榜”输出文本。"""
if not self.db:
return "❌ 身价模块未初始化"
rows = self.db.get_top_partner_pairs(group_id, self.social_window_days, limit)
if not rows:
return "📊 近期暂无搭子关系数据。"
cm = ContactManager.get_instance()
lines = [f"🤝 搭子榜(近{self.social_window_days}天 Top{len(rows)}"]
for idx, row in enumerate(rows, start=1):
user_a = str(row.get("user_a") or "")
user_b = str(row.get("user_b") or "")
nick_a = cm.get_group_name(group_id, user_a) or user_a
nick_b = cm.get_group_name(group_id, user_b) or user_b
mention_count = int(row.get("mention_count_window") or 0)
score = float(row.get("interaction_score_window") or 0.0)
medal = "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"{idx}."
lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}")
return "\n".join(lines)
def _build_explain_text(self) -> str:
"""输出算法说明文本。"""
return (