扩展 value_rank 周报能力并新增周报命令

- 新增命令 身价周报,并接入配置与帮助文案

- 新增每周定时动作 value_rank_weekly_report_push(周一09:35)自动推送周报

- 周报内容包含:综合排行Top5、上升榜Top5、下行榜Top5(对比7天前)

- 扩展 ValueRankDB:新增按日期读取快照分数字典能力,支持周报对比计算

- 调度执行中支持周报推送并补充重算保障,确保周报数据为当天最新
This commit is contained in:
liuwei
2026-04-21 14:00:57 +08:00
parent 78adab65b2
commit d60d496bc3
2 changed files with 134 additions and 9 deletions

View File

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

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import math import math
from datetime import datetime from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from loguru import logger from loguru import logger
@@ -251,6 +251,23 @@ class ValueRankDB(BaseDBOperator):
""" """
return self.execute_query(sql, (group_id, social_window_days, limit)) or [] return self.execute_query(sql, (group_id, social_window_days, limit)) or []
def get_snapshot_score_map(self, stat_date: str, group_id: str) -> Dict[str, float]:
"""读取某天群内所有用户分数字典:{user_id: score}。"""
sql = """
SELECT user_id, score
FROM t_value_rank_snapshot
WHERE stat_date = %s
AND group_id = %s
"""
rows = self.execute_query(sql, (stat_date, group_id)) or []
result: Dict[str, float] = {}
for row in rows:
user_id = str(row.get("user_id") or "").strip()
if not user_id:
continue
result[user_id] = float(row.get("score") or 0.0)
return result
class ValueRankPlugin(MessagePluginInterface): class ValueRankPlugin(MessagePluginInterface):
"""群成员身价排行插件。 """群成员身价排行插件。
@@ -303,7 +320,7 @@ class ValueRankPlugin(MessagePluginInterface):
# 配置默认值:即使未配置 config.toml也能以保守参数运行。 # 配置默认值:即使未配置 config.toml也能以保守参数运行。
self.enable = True self.enable = True
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"] self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价周报", "身价说明", "重算身价"]
self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价" self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价"
self.message_window_days = 7 self.message_window_days = 7
@@ -416,6 +433,11 @@ class ValueRankPlugin(MessagePluginInterface):
await bot.send_text_message(roomid, text, sender) await bot.send_text_message(roomid, text, sender)
return True, "查询成功" return True, "查询成功"
if command == "身价周报":
text = await self._build_weekly_report_text(roomid)
await bot.send_text_message(roomid, text, sender)
return True, "查询成功"
if command == "身价说明": if command == "身价说明":
await bot.send_text_message(roomid, self._build_explain_text(), sender) await bot.send_text_message(roomid, self._build_explain_text(), sender)
return True, "查询成功" return True, "查询成功"
@@ -435,7 +457,7 @@ class ValueRankPlugin(MessagePluginInterface):
return True, "命令错误" return True, "命令错误"
def get_schedule_actions(self) -> List[Dict[str, Any]]: def get_schedule_actions(self) -> List[Dict[str, Any]]:
"""声明可调度动作:每日凌晨全量重算。""" """声明可调度动作:每日重算 + 每周周报"""
return [ return [
{ {
"action_key": "value_rank_daily_recompute", "action_key": "value_rank_daily_recompute",
@@ -447,15 +469,26 @@ class ValueRankPlugin(MessagePluginInterface):
"target_config": {}, "target_config": {},
"payload": {}, "payload": {},
"default_enabled": True, "default_enabled": True,
} },
{
"action_key": "value_rank_weekly_report_push",
"name": "身价周报推送",
"description": "每周推送身价周报(上升榜/下降榜/综合排行)",
"trigger_type": "every_week_time",
# weekday 取值与 datetime.weekday() 一致:周一=0 ... 周日=6。
"trigger_config": {"weekday": 0, "time_str": "09:35"},
"target_scope": "all_enabled_groups",
"target_config": {},
"payload": {},
"default_enabled": True,
},
] ]
async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]:
"""执行调度动作。""" """执行调度动作。"""
if action_key != "value_rank_daily_recompute": if action_key not in {"value_rank_daily_recompute", "value_rank_weekly_report_push"}:
return {"success": False, "summary": f"不支持动作: {action_key}", "detail": {}} return {"success": False, "summary": f"不支持动作: {action_key}", "detail": {}}
stat_date = datetime.now().strftime("%Y-%m-%d")
target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()]
if not target_groups: if not target_groups:
target_groups = [ target_groups = [
@@ -466,14 +499,39 @@ class ValueRankPlugin(MessagePluginInterface):
success_groups: List[str] = [] success_groups: List[str] = []
failed_groups: Dict[str, str] = {} failed_groups: Dict[str, str] = {}
updated_users = 0 updated_users = 0
pushed_groups: List[str] = []
stat_date = datetime.now().strftime("%Y-%m-%d")
bot = context.get("bot") or getattr(self, "bot", None)
# 周报任务先确保当日快照存在,再执行推送,避免“有报表命令但无数据”。
if action_key == "value_rank_weekly_report_push" and not bot:
return {"success": False, "summary": "周报推送失败bot 未注入", "detail": {}}
for gid in target_groups: for gid in target_groups:
try: try:
# 每个群都先重算一次,确保报告与排行数据是“当天最新口径”。
updated_users += self._recompute_group_snapshot(gid, stat_date) updated_users += self._recompute_group_snapshot(gid, stat_date)
if action_key == "value_rank_weekly_report_push":
weekly_text = await self._build_weekly_report_text(gid)
await bot.send_text_message(gid, weekly_text, "")
pushed_groups.append(gid)
success_groups.append(gid) success_groups.append(gid)
except Exception as e: except Exception as e:
failed_groups[gid] = str(e) failed_groups[gid] = str(e)
if action_key == "value_rank_weekly_report_push":
return {
"success": len(failed_groups) == 0,
"summary": f"身价周报完成:推送{len(pushed_groups)}群,失败{len(failed_groups)}",
"detail": {
"stat_date": stat_date,
"updated_users": updated_users,
"pushed_groups": pushed_groups,
"failed_groups": failed_groups,
},
}
return { return {
"success": len(failed_groups) == 0, "success": len(failed_groups) == 0,
"summary": f"身价重算完成:成功{len(success_groups)}群,失败{len(failed_groups)}", "summary": f"身价重算完成:成功{len(success_groups)}群,失败{len(failed_groups)}",
@@ -740,6 +798,72 @@ class ValueRankPlugin(MessagePluginInterface):
lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}") lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}")
return "\n".join(lines) return "\n".join(lines)
async def _build_weekly_report_text(self, group_id: str) -> str:
"""构建“身价周报”文本。
周报口径:
1. 综合排行:取今日 Top5
2. 上升榜/下降榜:对比“今日分数 - 7天前分数”
3. 若7天前无数据则按 0 处理,保证新群也可输出。
"""
if not self.db:
return "❌ 身价模块未初始化"
today = datetime.now().strftime("%Y-%m-%d")
# 使用 SQL DATE_SUB 也可计算,但这里使用 Python 日期便于阅读与调试。
base_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
# 确保今天有快照,避免周报命令调用时出现空数据。
today_rank_rows = self.db.get_today_rankings(today, group_id, 50)
if not today_rank_rows:
self._recompute_group_snapshot(group_id, today)
today_rank_rows = self.db.get_today_rankings(today, group_id, 50)
if not today_rank_rows:
return "📊 本周暂无可用身价数据。"
today_score_map = self.db.get_snapshot_score_map(today, group_id)
base_score_map = self.db.get_snapshot_score_map(base_date, group_id)
# 计算用户分数变化,并做升降序榜单。
delta_rows: List[Tuple[str, float, float, float]] = []
for user_id, today_score in today_score_map.items():
old_score = float(base_score_map.get(user_id, 0.0))
delta = round(float(today_score) - old_score, 2)
delta_rows.append((user_id, float(today_score), old_score, delta))
delta_rows_sorted_up = sorted(delta_rows, key=lambda x: x[3], reverse=True)
delta_rows_sorted_down = sorted(delta_rows, key=lambda x: x[3])
cm = ContactManager.get_instance()
lines: List[str] = [
f"📈 身价周报({today}",
f"对比基线:{base_date}",
"",
"🏆 本周综合排行 Top5",
]
for idx, row in enumerate(today_rank_rows[:5], start=1):
uid = str(row.get("user_id") or "")
nick = cm.get_group_name(group_id, uid) or uid
score = float(row.get("score") or 0.0)
title = str(row.get("title") or "")
lines.append(f"{idx}. {nick} | {score:.2f} | {title}")
lines.append("")
lines.append("🚀 本周上升榜 Top5")
for idx, (uid, today_score, old_score, delta) in enumerate(delta_rows_sorted_up[:5], start=1):
nick = cm.get_group_name(group_id, uid) or uid
lines.append(f"{idx}. {nick} | {old_score:.2f} -> {today_score:.2f} | {delta:+.2f}")
lines.append("")
lines.append("🧊 本周波动下行 Top5")
for idx, (uid, today_score, old_score, delta) in enumerate(delta_rows_sorted_down[:5], start=1):
nick = cm.get_group_name(group_id, uid) or uid
lines.append(f"{idx}. {nick} | {old_score:.2f} -> {today_score:.2f} | {delta:+.2f}")
lines.append("")
lines.append("提示:分数由积分/发言/活跃/社交影响力综合计算。")
return "\n".join(lines)
def _build_explain_text(self) -> str: def _build_explain_text(self) -> str:
"""输出算法说明文本。""" """输出算法说明文本。"""
return ( return (