diff --git a/db/message_storage.py b/db/message_storage.py index 4cd990e..bf3c12b 100644 --- a/db/message_storage.py +++ b/db/message_storage.py @@ -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 = """ diff --git a/plugins/value_rank/config.toml b/plugins/value_rank/config.toml index 8330fe8..9221c10 100644 --- a/plugins/value_rank/config.toml +++ b/plugins/value_rank/config.toml @@ -1,12 +1,14 @@ [ValueRank] enable = true -command = ["我的身价", "身价排行", "身价说明", "重算身价"] +command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"] command-format = """ 📊 身价系统命令: 1. 我的身价 2. 身价排行 [名次] -3. 身价说明 -4. 重算身价(管理员) +3. 社交热度榜 [名次] +4. 搭子榜 [名次] +5. 身价说明 +6. 重算身价(管理员) """ # 统计窗口(天) diff --git a/plugins/value_rank/main.py b/plugins/value_rank/main.py index 7af4c06..79aebee 100644 --- a/plugins/value_rank/main.py +++ b/plugins/value_rank/main.py @@ -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 (