加入了级别功能。

This commit is contained in:
liuwei
2025-11-14 15:16:56 +08:00
parent ceadaa03f1
commit 57c6a0d234
6 changed files with 169 additions and 17 deletions

138
db/levels_db.py Normal file
View File

@@ -0,0 +1,138 @@
from datetime import datetime
from typing import Dict, Optional, Tuple
import math
from loguru import logger
from db.base import BaseDBOperator
from db.connection import DBConnectionManager
class LevelsDBOperator(BaseDBOperator):
def __init__(self, db_manager: DBConnectionManager = None):
super().__init__(db_manager or DBConnectionManager.get_instance())
self.LOG = logger
self._ensure_table()
def _ensure_table(self) -> bool:
sql = (
"""
CREATE TABLE IF NOT EXISTS t_user_levels (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(100) NOT NULL,
group_id VARCHAR(100) NOT NULL,
exp BIGINT DEFAULT 0,
level INT DEFAULT 1,
last_calc DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_user_group (user_id, group_id)
) ENGINE=InnoDB CHARACTER SET utf8mb4;
"""
)
return self.execute_update(sql)
def get_user_level(self, user_id: str, group_id: str) -> Dict:
sql = (
"SELECT user_id, group_id, exp, level, last_calc, last_active_at "
"FROM t_user_levels WHERE user_id = %s AND group_id = %s"
)
result = self.execute_query(sql, (user_id, group_id), fetch_one=True)
if result:
return result
now = datetime.now()
insert_sql = (
"INSERT INTO t_user_levels (user_id, group_id, exp, level, last_calc, last_active_at) "
"VALUES (%s, %s, %s, %s, %s, %s)"
)
self.execute_update(insert_sql, (user_id, group_id, 0, 1, now, now))
return {
"user_id": user_id,
"group_id": group_id,
"exp": 0,
"level": 1,
"last_calc": now,
"last_active_at": now,
}
def _apply_decay(self, cur: Dict, now: datetime) -> Tuple[int, bool]:
exp = int(cur.get("exp", 0))
last_active = cur.get("last_active_at")
if not last_active:
return exp, False
try:
inactive_days = (now - last_active).days
except Exception:
return exp, False
if inactive_days < 7:
return exp, False
weeks = inactive_days // 7
if weeks <= 0:
return exp, False
rate = 0.95
decayed = int(exp * (rate ** weeks))
update_sql = (
"UPDATE t_user_levels SET exp = %s, last_calc = %s WHERE user_id = %s AND group_id = %s"
)
self.execute_update(update_sql, (decayed, now, cur.get("user_id"), cur.get("group_id")))
return decayed, True
def _compute_level(self, exp: int) -> int:
thresholds = [0, 500, 1000, 1200, 1500, 1800, 4000, 8000, 9000, 20000]
lvl = 1
for t in thresholds:
if exp >= t:
lvl += 1
else:
break
return max(1, lvl - 1)
def level_title(self, level: int) -> str:
titles = [
"凡人",
"炼体期",
"筑基期",
"结丹期",
"元婴期",
"化神期",
"合体期",
"大乘期",
"渡劫期",
"真仙",
]
if level <= 0:
return titles[0]
idx = min(level - 1, len(titles) - 1)
return titles[idx]
def add_exp(self, user_id: str, group_id: str, delta: int, reason: Optional[str] = None) -> Tuple[bool, Dict]:
if not user_id or not group_id:
return False, {"error": "invalid_identity"}
try:
cur = self.get_user_level(user_id, group_id)
now = datetime.now()
base_exp, _ = self._apply_decay(cur, now)
new_exp = max(0, int(base_exp) + int(delta))
new_level = self._compute_level(new_exp)
update_sql = (
"UPDATE t_user_levels SET exp = %s, level = %s, last_calc = %s, last_active_at = %s "
"WHERE user_id = %s AND group_id = %s"
)
self.execute_update(update_sql, (new_exp, new_level, now, now, user_id, group_id))
return True, {"user_id": user_id, "group_id": group_id, "exp": new_exp, "level": new_level}
except Exception as e:
self.LOG.error(f"add_exp error: {e}")
return False, {"error": str(e)}
def recalc_level(self, user_id: str, group_id: str) -> Tuple[bool, Dict]:
try:
cur = self.get_user_level(user_id, group_id)
new_level = self._compute_level(int(cur.get("exp", 0)))
now = datetime.now()
update_sql = (
"UPDATE t_user_levels SET level = %s, last_calc = %s WHERE user_id = %s AND group_id = %s"
)
self.execute_update(update_sql, (new_level, now, user_id, group_id))
return True, {"user_id": user_id, "group_id": group_id, "exp": cur.get("exp", 0), "level": new_level}
except Exception as e:
self.LOG.error(f"recalc_level error: {e}")
return False, {"error": str(e)}

View File

@@ -111,7 +111,7 @@ class AIGenImagePlugin(MessagePluginInterface):
return command in self._commands
@plugin_stats_decorator(plugin_name="AI绘图")
@plugin_points_cost(5, "AI绘图消耗积分", FEATURE_KEY)
@plugin_points_cost(20, "AI绘图消耗积分", FEATURE_KEY)
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
"""处理消息"""
content = str(message.get("content", "")).strip()

View File

@@ -13,8 +13,8 @@ command-format = """
# 打劫功能配置
rob-success-rate = 0.4 # 基础打劫成功率
rob-rate-decay = 0.1 # 积分差距成功率衰减系数
rob-min-percent = 0.1 # 打劫成功时最小获取目标积分百分比
rob-max-percent = 0.3 # 打劫成功时最大获取目标积分百分比
rob-penalty-percent = 0.2 # 打劫失败时的惩罚百分比(扣除自身积分的比例)
rob-min-percent = 0.2 # 打劫成功时最小获取目标积分百分比
rob-max-percent = 0.4 # 打劫成功时最大获取目标积分百分比
rob-penalty-percent = 0.1 # 打劫失败时的惩罚百分比(扣除自身积分的比例)
rob-cooldown = 300 # 打劫冷却时间(秒)默认1小时
rob-min-points = 30 # 打劫最低积分要求(打劫者和目标都需满足)

View File

@@ -8,6 +8,7 @@ from typing import Callable, Dict, Any, Tuple
from db.stats_db import StatsDBOperator
from db.connection import DBConnectionManager
from db.levels_db import LevelsDBOperator
def plugin_stats_decorator(plugin_name: str) -> Callable:
@@ -68,6 +69,9 @@ def plugin_stats_decorator(plugin_name: str) -> Callable:
process_time_ms=process_time_ms
)
logger.info(f"[{plugin_name}] 成功记录插件调用: {command}, 耗时: {process_time_ms:.2f}ms")
if success and sender:
levels_db = LevelsDBOperator(db_manager)
levels_db.add_exp(sender, roomid or sender, 2, "plugin_call")
# 定义不需要记录错误的正常业务状态
normal_responses = {
@@ -95,44 +99,35 @@ def plugin_stats_decorator(plugin_name: str) -> Callable:
return success, response
except Exception as e:
# 计算执行时间(毫秒)
end_time = time.time()
process_time_ms = (end_time - start_time) * 1000
# 记录错误
error_message = str(e)
stack_trace = traceback.format_exc()
logger.error(f"[{plugin_name}] 执行出错: {error_message}")
logger.debug(f"[{plugin_name}] 错误堆栈: {stack_trace}")
try:
# 记录插件调用(失败)
logger.debug(f"[{plugin_name}] 记录插件调用失败统计")
stats_db.record_plugin_call(
plugin_name=plugin_name,
command=command, # 使用提取的指令而不是完整内容
command=command,
user_id=sender,
group_id=roomid,
success=False,
process_time_ms=process_time_ms
)
# 记录错误详情
logger.debug(f"[{plugin_name}] 记录错误详情")
stats_db.record_error(
plugin_name=plugin_name,
command=command, # 使用提取的指令而不是完整内容
command=command,
user_id=sender,
group_id=roomid,
error_message=error_message[:500] if error_message else "未知错误", # 限制长度并确保不为空
stack_trace=stack_trace[:2000] if stack_trace else "无堆栈信息" # 限制长度并确保不为空
error_message=error_message[:500] if error_message else "未知错误",
stack_trace=stack_trace[:2000] if stack_trace else "无堆栈信息"
)
logger.info(f"[{plugin_name}] 成功记录插件错误: {command}, 错误: {error_message}")
except Exception as db_error:
logger.error(f"[{plugin_name}] 记录插件统计数据失败: {db_error}")
logger.error(traceback.format_exc())
# 重新抛出异常,让上层处理
raise
except Exception as outer_error:
logger.error(f"[{plugin_name}] 装饰器外层错误: {outer_error}")

View File

@@ -7,6 +7,7 @@ from typing import Callable, Dict, Any, Tuple, Union
from db.connection import DBConnectionManager
from db.points_db import PointsDBOperator, PointSource
from db.levels_db import LevelsDBOperator
from utils.revoke.message_auto_revoke import MessageAutoRevoke
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
from wechat_ipad import WechatAPIClient
@@ -86,10 +87,15 @@ def points_reward_decorator(points_calculator: Union[int, Callable], source_type
logger.info(f"PointsReward.{self.name if hasattr(self, 'name') else 'Unknown'}")
if reward_success:
logger.info(f"用户 {sender} 获得 {points} 积分奖励")
levels_db = LevelsDBOperator(db_manager)
ok, lvl = levels_db.add_exp(sender, roomid, points, source.value)
# 如果响应中没有提到积分,添加积分信息
if "积分" not in response:
response += f"\n🎁 恭喜获得 {points} 积分奖励!"
if ok and isinstance(lvl, dict) and "level" in lvl and "exp" in lvl:
title = levels_db.level_title(int(lvl['level']))
response += f"\n🔰 当前等级: {lvl['level']}{title} 经验: {lvl['exp']}"
client_msg_id, create_time, new_msg_id = await bot.send_at_message(
(roomid if roomid else sender),
response, [sender]
@@ -160,10 +166,15 @@ def points_reward_decorator(points_calculator: Union[int, Callable], source_type
logger.info(f"PointsReward.{self.name if hasattr(self, 'name') else 'Unknown'}")
if reward_success:
logger.info(f"用户 {sender} 获得 {points} 积分奖励")
levels_db = LevelsDBOperator(db_manager)
ok, lvl = levels_db.add_exp(sender, roomid, points, source.value)
# 如果响应中没有提到积分,添加积分信息
if "积分" not in response:
response += f"\n🎁 恭喜获得 {points} 积分奖励!"
if ok and isinstance(lvl, dict) and "level" in lvl and "exp" in lvl:
title = levels_db.level_title(int(lvl['level']))
response += f"\n🔰 当前等级: {lvl['level']}{title} 经验: {lvl['exp']}"
else:
logger.warning(f"用户 {sender} 积分奖励失败: {reward_result}")
except Exception as e:

View File

@@ -6,6 +6,7 @@ import concurrent.futures # 添加线程池支持
import os
from db.connection import DBConnectionManager
from db.levels_db import LevelsDBOperator
from db.message_storage import MessageStorageDB
# 导入积分系统
from db.points_db import PointsDBOperator, PointSource
@@ -237,6 +238,13 @@ class MessageStorage:
logging.info(f"成功写入发言统计: {group_id}, {wx_id}, {yesterday}, {count}")
else:
logging.error(f"写入发言统计失败: {group_id}, {wx_id}, {yesterday}, {count}")
try:
levels_db = LevelsDBOperator(self.db_manager)
delta = int(0.5 * min(count, 10))
if delta > 0:
levels_db.add_exp(wx_id, group_id, delta, "speech_count")
except Exception as e2:
logging.error(f"写入等级经验失败: {group_id}, {wx_id}, {yesterday}, {count} - {e2}")
except Exception as e:
logging.error(f"写入发言统计出错: {e}")