加入了级别功能。
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
|
||||
|
||||
@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()
|
||||
|
||||
@@ -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 # 打劫最低积分要求(打劫者和目标都需满足)
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user