diff --git a/danding_bot/plugins/onmyoji_gacha/__init__.py b/danding_bot/plugins/onmyoji_gacha/__init__.py index b7d0d64..d5220e8 100644 --- a/danding_bot/plugins/onmyoji_gacha/__init__.py +++ b/danding_bot/plugins/onmyoji_gacha/__init__.py @@ -1,4 +1,6 @@ import os +import logging +import random from nonebot import on_command, on_startswith from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message from nonebot.adapters.onebot.v11.message import MessageSegment @@ -8,9 +10,10 @@ from pathlib import Path from .config import Config from .gacha import GachaSystem -from .utils import format_user_mention, get_image_path +from .utils import format_sign_in_message, format_user_mention, get_image_path from .api_utils import process_ssr_sp_reward, process_achievement_reward from . import web_api +from danding_bot.plugins.danding_points import points_api # 创建Config实例 config = Config() @@ -27,6 +30,11 @@ INTRO_COMMANDS = config.INTRO_COMMANDS DAILY_LIMIT = config.DAILY_LIMIT gacha_system = GachaSystem() +logger = logging.getLogger(__name__) +SIGN_IN_MIN_POINTS = 1 +SIGN_IN_MAX_POINTS = 100 +SIGN_IN_SOURCE = "gacha_sign" +SIGN_IN_REASON = "抽卡签到" # 检查是否允许使用功能的规则 def check_permission() -> Rule: @@ -43,6 +51,32 @@ def check_permission() -> Rule: return Rule(_checker) + +async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None: + """处理抽卡成功后的每日签到,不影响主流程""" + try: + if gacha_system.data_manager.has_signed_in_today(user_id): + return + + points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) + success, new_balance = await points_api.add_points( + user_id, + points, + SIGN_IN_SOURCE, + SIGN_IN_REASON, + ) + if not success: + logger.error("抽卡签到积分发放失败 user_id=%s points=%s", user_id, points) + return + + if not gacha_system.data_manager.record_sign_in(user_id, points): + logger.warning("抽卡签到落库冲突,积分已发放但签到记录重复 user_id=%s", user_id) + return + + await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) + except Exception: + logger.exception("处理抽卡签到失败 user_id=%s", user_id) + # 注册抽卡命令,添加权限检查规则 gacha_matcher = on_command("抽卡", aliases=set(GACHA_COMMANDS), priority=10, rule=check_permission()) @@ -188,7 +222,9 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): else: msg.append(f"\n\n抽中SSR或SP时,可获得蛋定助手天卡一张哦~~") - await gacha_matcher.finish(msg) + await gacha_matcher.send(msg) + await try_handle_daily_sign_in(gacha_matcher, user_id, user_name) + return async def notify_admin(bot: Bot, message: str): """通知管理员""" @@ -395,7 +431,9 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State): admin_msg += f" 需要手动发放 {ssr_count} 张奖励!" await notify_admin(bot, admin_msg) - await triple_gacha_matcher.finish(msg) + await triple_gacha_matcher.send(msg) + await try_handle_daily_sign_in(triple_gacha_matcher, user_id, user_name) + return @achievement_matcher.handle() async def handle_achievement(bot: Bot, event: MessageEvent, state: T_State): @@ -759,4 +797,4 @@ from . import web_api try: web_api.register_web_routes() except Exception as e: - print(f"❌ 注册 onmyoji_gacha Web 路由失败: {e}") \ No newline at end of file + print(f"❌ 注册 onmyoji_gacha Web 路由失败: {e}") diff --git a/danding_bot/plugins/onmyoji_gacha/data_manager.py b/danding_bot/plugins/onmyoji_gacha/data_manager.py index 361b69d..2a530fc 100644 --- a/danding_bot/plugins/onmyoji_gacha/data_manager.py +++ b/danding_bot/plugins/onmyoji_gacha/data_manager.py @@ -97,8 +97,23 @@ class DataManager: total_consecutive_days INTEGER DEFAULT 0 ) """) + + self._init_sign_in_table(cursor) conn.commit() + + def _init_sign_in_table(self, cursor: sqlite3.Cursor) -> None: + """创建每日签到表""" + cursor.execute(""" + CREATE TABLE IF NOT EXISTS daily_sign_in ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + sign_date TEXT NOT NULL, + points_awarded INTEGER NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(user_id, sign_date) + ) + """) def update_achievement_progress(self, user_id: str, rarity: str) -> List[str]: """更新用户成就进度,返回新解锁的成就列表""" @@ -321,6 +336,33 @@ class DataManager: def get_today_date(self) -> str: """获取当前日期字符串""" return datetime.datetime.now().strftime("%Y-%m-%d") + + def has_signed_in_today(self, user_id: str) -> bool: + """检查用户今天是否已签到""" + today = self.get_today_date() + with sqlite3.connect(config.DB_FILE) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT 1 FROM daily_sign_in WHERE user_id = ? AND sign_date = ? LIMIT 1", + (user_id, today), + ) + return cursor.fetchone() is not None + + def record_sign_in(self, user_id: str, points_awarded: int) -> bool: + """记录每日签到,重复签到返回False""" + today = self.get_today_date() + created_at = datetime.datetime.now().isoformat() + try: + with sqlite3.connect(config.DB_FILE) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO daily_sign_in (user_id, sign_date, points_awarded, created_at) + VALUES (?, ?, ?, ?) + """, (user_id, today, points_awarded, created_at)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False def get_current_time(self) -> str: """获取当前时间字符串""" @@ -549,4 +591,4 @@ class DataManager: # 更新成就进度 unlocked_achievements = self.update_achievement_progress(user_id, rarity) - return unlocked_achievements \ No newline at end of file + return unlocked_achievements diff --git a/danding_bot/plugins/onmyoji_gacha/utils.py b/danding_bot/plugins/onmyoji_gacha/utils.py index 8c5a5db..d8ccab5 100644 --- a/danding_bot/plugins/onmyoji_gacha/utils.py +++ b/danding_bot/plugins/onmyoji_gacha/utils.py @@ -1,5 +1,5 @@ import os -from typing import Union, Optional +from typing import Optional from pathlib import Path def get_image_path(file_path: str) -> str: @@ -9,4 +9,34 @@ def get_image_path(file_path: str) -> str: def format_user_mention(user_id: str, user_name: Optional[str] = None) -> str: """格式化用户@信息""" display_name = user_name if user_name else f"用户{user_id}" - return f"@{display_name}" \ No newline at end of file + return f"@{display_name}" + + +def get_luck_description(points: int) -> tuple[str, str]: + """根据积分返回运气描述与emoji""" + if points <= 10: + return "非酋", "😭" + if points <= 30: + return "一般", "😐" + if points <= 60: + return "小欧", "😊" + if points <= 90: + return "大欧", "🎉" + return "欧皇", "👑" + + +def format_sign_in_message( + user_id: str, + user_name: str, + points: int, + balance: int, +) -> str: + """格式化签到成功消息""" + luck_text, luck_emoji = get_luck_description(points) + mention = format_user_mention(user_id, user_name) + return ( + f"{mention} 📅 每日签到成功!\n" + f"🎁 获得积分:{points}\n" + f"{luck_emoji} 今日运气:{luck_text}\n" + f"💰 当前积分:{balance}" + ) diff --git a/test_onmyoji_gacha_sign_in.py b/test_onmyoji_gacha_sign_in.py new file mode 100644 index 0000000..0ab8be5 --- /dev/null +++ b/test_onmyoji_gacha_sign_in.py @@ -0,0 +1,95 @@ +import tempfile +import unittest +import importlib.util +import sys +import types +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent + + +def ensure_package(package_name: str, package_path: Path) -> None: + if package_name in sys.modules: + return + package = types.ModuleType(package_name) + package.__path__ = [str(package_path)] + sys.modules[package_name] = package + + +def load_module(module_name: str, relative_path: str): + module_path = PROJECT_ROOT / relative_path + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +ensure_package("danding_bot", PROJECT_ROOT / "danding_bot") +ensure_package("danding_bot.plugins", PROJECT_ROOT / "danding_bot/plugins") +ensure_package( + "danding_bot.plugins.onmyoji_gacha", + PROJECT_ROOT / "danding_bot/plugins/onmyoji_gacha", +) +load_module( + "danding_bot.plugins.onmyoji_gacha.config", + "danding_bot/plugins/onmyoji_gacha/config.py", +) +data_manager_module = load_module( + "danding_bot.plugins.onmyoji_gacha.data_manager", + "danding_bot/plugins/onmyoji_gacha/data_manager.py", +) +utils = load_module( + "danding_bot.plugins.onmyoji_gacha.utils", + "danding_bot/plugins/onmyoji_gacha/utils.py", +) + + +class DataManagerSignInTests(unittest.TestCase): + def setUp(self) -> None: + self.temp_dir = tempfile.TemporaryDirectory() + base_path = Path(self.temp_dir.name) + self.original_db_file = data_manager_module.config.DB_FILE + self.original_img_dir = data_manager_module.config.SHIKIGAMI_IMG_DIR + data_manager_module.config.DB_FILE = str(base_path / "gacha.db") + data_manager_module.config.SHIKIGAMI_IMG_DIR = str(base_path / "images") + Path(data_manager_module.config.SHIKIGAMI_IMG_DIR).mkdir(parents=True, exist_ok=True) + self.manager = data_manager_module.DataManager() + + def tearDown(self) -> None: + data_manager_module.config.DB_FILE = self.original_db_file + data_manager_module.config.SHIKIGAMI_IMG_DIR = self.original_img_dir + self.temp_dir.cleanup() + + def test_record_sign_in_is_idempotent_for_same_day(self) -> None: + user_id = "10001" + + self.assertFalse(self.manager.has_signed_in_today(user_id)) + self.assertTrue(self.manager.record_sign_in(user_id, 88)) + self.assertTrue(self.manager.has_signed_in_today(user_id)) + self.assertFalse(self.manager.record_sign_in(user_id, 99)) + + +class UtilsSignInTests(unittest.TestCase): + def test_get_luck_description_uses_document_ranges(self) -> None: + self.assertEqual(utils.get_luck_description(1), ("非酋", "😭")) + self.assertEqual(utils.get_luck_description(11), ("一般", "😐")) + self.assertEqual(utils.get_luck_description(31), ("小欧", "😊")) + self.assertEqual(utils.get_luck_description(61), ("大欧", "🎉")) + self.assertEqual(utils.get_luck_description(91), ("欧皇", "👑")) + + def test_format_sign_in_message_matches_required_format(self) -> None: + message = utils.format_sign_in_message("10001", "小夏", 87, 1247) + expected = ( + "@小夏 📅 每日签到成功!\n" + "🎁 获得积分:87\n" + "🎉 今日运气:大欧\n" + "💰 当前积分:1247" + ) + self.assertEqual(message, expected) + + +if __name__ == "__main__": + unittest.main()