diff --git a/db/levels_db.py b/db/levels_db.py new file mode 100644 index 0000000..8e8b1ea --- /dev/null +++ b/db/levels_db.py @@ -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)} \ No newline at end of file diff --git a/plugins/ai_gen_image/main.py b/plugins/ai_gen_image/main.py index 95763a5..849b1b5 100644 --- a/plugins/ai_gen_image/main.py +++ b/plugins/ai_gen_image/main.py @@ -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() diff --git a/plugins/point_trade/config.toml b/plugins/point_trade/config.toml index 347eb58..41e5225 100644 --- a/plugins/point_trade/config.toml +++ b/plugins/point_trade/config.toml @@ -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 # 打劫最低积分要求(打劫者和目标都需满足) \ No newline at end of file diff --git a/utils/decorator/plugin_decorators.py b/utils/decorator/plugin_decorators.py index 5ef6a84..255f876 100644 --- a/utils/decorator/plugin_decorators.py +++ b/utils/decorator/plugin_decorators.py @@ -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}") diff --git a/utils/decorator/points_decorator.py b/utils/decorator/points_decorator.py index 1707ea1..3fa8fdb 100644 --- a/utils/decorator/points_decorator.py +++ b/utils/decorator/points_decorator.py @@ -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: diff --git a/utils/wechat/message_to_db.py b/utils/wechat/message_to_db.py index f2f3bce..ba73e96 100644 --- a/utils/wechat/message_to_db.py +++ b/utils/wechat/message_to_db.py @@ -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}")