加入了级别功能。
This commit is contained in:
138
db/levels_db.py
Normal file
138
db/levels_db.py
Normal 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)}
|
||||||
@@ -111,7 +111,7 @@ class AIGenImagePlugin(MessagePluginInterface):
|
|||||||
return command in self._commands
|
return command in self._commands
|
||||||
|
|
||||||
@plugin_stats_decorator(plugin_name="AI绘图")
|
@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]]:
|
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
||||||
"""处理消息"""
|
"""处理消息"""
|
||||||
content = str(message.get("content", "")).strip()
|
content = str(message.get("content", "")).strip()
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ command-format = """
|
|||||||
# 打劫功能配置
|
# 打劫功能配置
|
||||||
rob-success-rate = 0.4 # 基础打劫成功率
|
rob-success-rate = 0.4 # 基础打劫成功率
|
||||||
rob-rate-decay = 0.1 # 积分差距成功率衰减系数
|
rob-rate-decay = 0.1 # 积分差距成功率衰减系数
|
||||||
rob-min-percent = 0.1 # 打劫成功时最小获取目标积分百分比
|
rob-min-percent = 0.2 # 打劫成功时最小获取目标积分百分比
|
||||||
rob-max-percent = 0.3 # 打劫成功时最大获取目标积分百分比
|
rob-max-percent = 0.4 # 打劫成功时最大获取目标积分百分比
|
||||||
rob-penalty-percent = 0.2 # 打劫失败时的惩罚百分比(扣除自身积分的比例)
|
rob-penalty-percent = 0.1 # 打劫失败时的惩罚百分比(扣除自身积分的比例)
|
||||||
rob-cooldown = 300 # 打劫冷却时间(秒),默认1小时
|
rob-cooldown = 300 # 打劫冷却时间(秒),默认1小时
|
||||||
rob-min-points = 30 # 打劫最低积分要求(打劫者和目标都需满足)
|
rob-min-points = 30 # 打劫最低积分要求(打劫者和目标都需满足)
|
||||||
@@ -8,6 +8,7 @@ from typing import Callable, Dict, Any, Tuple
|
|||||||
|
|
||||||
from db.stats_db import StatsDBOperator
|
from db.stats_db import StatsDBOperator
|
||||||
from db.connection import DBConnectionManager
|
from db.connection import DBConnectionManager
|
||||||
|
from db.levels_db import LevelsDBOperator
|
||||||
|
|
||||||
|
|
||||||
def plugin_stats_decorator(plugin_name: str) -> Callable:
|
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
|
process_time_ms=process_time_ms
|
||||||
)
|
)
|
||||||
logger.info(f"[{plugin_name}] 成功记录插件调用: {command}, 耗时: {process_time_ms:.2f}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 = {
|
normal_responses = {
|
||||||
@@ -95,44 +99,35 @@ def plugin_stats_decorator(plugin_name: str) -> Callable:
|
|||||||
|
|
||||||
return success, response
|
return success, response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 计算执行时间(毫秒)
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
process_time_ms = (end_time - start_time) * 1000
|
process_time_ms = (end_time - start_time) * 1000
|
||||||
|
|
||||||
# 记录错误
|
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
stack_trace = traceback.format_exc()
|
stack_trace = traceback.format_exc()
|
||||||
logger.error(f"[{plugin_name}] 执行出错: {error_message}")
|
logger.error(f"[{plugin_name}] 执行出错: {error_message}")
|
||||||
logger.debug(f"[{plugin_name}] 错误堆栈: {stack_trace}")
|
logger.debug(f"[{plugin_name}] 错误堆栈: {stack_trace}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 记录插件调用(失败)
|
|
||||||
logger.debug(f"[{plugin_name}] 记录插件调用失败统计")
|
logger.debug(f"[{plugin_name}] 记录插件调用失败统计")
|
||||||
stats_db.record_plugin_call(
|
stats_db.record_plugin_call(
|
||||||
plugin_name=plugin_name,
|
plugin_name=plugin_name,
|
||||||
command=command, # 使用提取的指令而不是完整内容
|
command=command,
|
||||||
user_id=sender,
|
user_id=sender,
|
||||||
group_id=roomid,
|
group_id=roomid,
|
||||||
success=False,
|
success=False,
|
||||||
process_time_ms=process_time_ms
|
process_time_ms=process_time_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
# 记录错误详情
|
|
||||||
logger.debug(f"[{plugin_name}] 记录错误详情")
|
logger.debug(f"[{plugin_name}] 记录错误详情")
|
||||||
stats_db.record_error(
|
stats_db.record_error(
|
||||||
plugin_name=plugin_name,
|
plugin_name=plugin_name,
|
||||||
command=command, # 使用提取的指令而不是完整内容
|
command=command,
|
||||||
user_id=sender,
|
user_id=sender,
|
||||||
group_id=roomid,
|
group_id=roomid,
|
||||||
error_message=error_message[:500] if error_message else "未知错误", # 限制长度并确保不为空
|
error_message=error_message[:500] if error_message else "未知错误",
|
||||||
stack_trace=stack_trace[:2000] if stack_trace else "无堆栈信息" # 限制长度并确保不为空
|
stack_trace=stack_trace[:2000] if stack_trace else "无堆栈信息"
|
||||||
)
|
)
|
||||||
logger.info(f"[{plugin_name}] 成功记录插件错误: {command}, 错误: {error_message}")
|
logger.info(f"[{plugin_name}] 成功记录插件错误: {command}, 错误: {error_message}")
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
logger.error(f"[{plugin_name}] 记录插件统计数据失败: {db_error}")
|
logger.error(f"[{plugin_name}] 记录插件统计数据失败: {db_error}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
# 重新抛出异常,让上层处理
|
|
||||||
raise
|
raise
|
||||||
except Exception as outer_error:
|
except Exception as outer_error:
|
||||||
logger.error(f"[{plugin_name}] 装饰器外层错误: {outer_error}")
|
logger.error(f"[{plugin_name}] 装饰器外层错误: {outer_error}")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Callable, Dict, Any, Tuple, Union
|
|||||||
|
|
||||||
from db.connection import DBConnectionManager
|
from db.connection import DBConnectionManager
|
||||||
from db.points_db import PointsDBOperator, PointSource
|
from db.points_db import PointsDBOperator, PointSource
|
||||||
|
from db.levels_db import LevelsDBOperator
|
||||||
from utils.revoke.message_auto_revoke import MessageAutoRevoke
|
from utils.revoke.message_auto_revoke import MessageAutoRevoke
|
||||||
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
|
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
|
||||||
from wechat_ipad import WechatAPIClient
|
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'}")
|
logger.info(f"PointsReward.{self.name if hasattr(self, 'name') else 'Unknown'}")
|
||||||
if reward_success:
|
if reward_success:
|
||||||
logger.info(f"用户 {sender} 获得 {points} 积分奖励")
|
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:
|
if "积分" not in response:
|
||||||
response += f"\n🎁 恭喜获得 {points} 积分奖励!"
|
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(
|
client_msg_id, create_time, new_msg_id = await bot.send_at_message(
|
||||||
(roomid if roomid else sender),
|
(roomid if roomid else sender),
|
||||||
response, [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'}")
|
logger.info(f"PointsReward.{self.name if hasattr(self, 'name') else 'Unknown'}")
|
||||||
if reward_success:
|
if reward_success:
|
||||||
logger.info(f"用户 {sender} 获得 {points} 积分奖励")
|
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:
|
if "积分" not in response:
|
||||||
response += f"\n🎁 恭喜获得 {points} 积分奖励!"
|
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:
|
else:
|
||||||
logger.warning(f"用户 {sender} 积分奖励失败: {reward_result}")
|
logger.warning(f"用户 {sender} 积分奖励失败: {reward_result}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import concurrent.futures # 添加线程池支持
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from db.connection import DBConnectionManager
|
from db.connection import DBConnectionManager
|
||||||
|
from db.levels_db import LevelsDBOperator
|
||||||
from db.message_storage import MessageStorageDB
|
from db.message_storage import MessageStorageDB
|
||||||
# 导入积分系统
|
# 导入积分系统
|
||||||
from db.points_db import PointsDBOperator, PointSource
|
from db.points_db import PointsDBOperator, PointSource
|
||||||
@@ -237,6 +238,13 @@ class MessageStorage:
|
|||||||
logging.info(f"成功写入发言统计: {group_id}, {wx_id}, {yesterday}, {count}")
|
logging.info(f"成功写入发言统计: {group_id}, {wx_id}, {yesterday}, {count}")
|
||||||
else:
|
else:
|
||||||
logging.error(f"写入发言统计失败: {group_id}, {wx_id}, {yesterday}, {count}")
|
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:
|
except Exception as e:
|
||||||
logging.error(f"写入发言统计出错: {e}")
|
logging.error(f"写入发言统计出错: {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user