From 7f3f744af5157df45fb1ab512a08d22ea712b929 Mon Sep 17 00:00:00 2001 From: "Mr.Xia" <1424473282@qq.com> Date: Sat, 15 Aug 2026 20:50:38 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E7=A7=AF=E5=88=86=E5=95=86=E5=BA=97?= =?UTF-8?q?=E3=80=91onmyoji=5Fgacha=20=E6=96=B0=E5=A2=9E=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E5=95=86=E5=BA=97=E4=B8=8E=E5=85=91=E6=8D=A2=E6=8A=BD=E5=8D=A1?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLUGINS.md | 2 + danding_bot/plugins/onmyoji_gacha/__init__.py | 219 +++++++++++------- danding_bot/plugins/onmyoji_gacha/config.py | 2 + .../plugins/onmyoji_gacha/data_manager.py | 14 ++ danding_bot/plugins/onmyoji_gacha/gacha.py | 159 +++++++------ tests/test_onmyoji_gacha_http_api.py | 95 ++++++++ 6 files changed, 331 insertions(+), 160 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index c345dec..fa9bcf8 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -117,6 +117,8 @@ - `今日抽卡`: 查看今日抽卡统计。 - `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。 - `抽卡介绍`: 查看详细机制与奖励说明。 + - `积分商店`(`兑换商店`): 查看可兑换商品、余额与剩余抽卡次数。 + - `兑换抽卡`(`积分兑换`): 消耗 99 Bot 积分兑换 1 次当日抽卡机会。 - **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。 ### 10. damo_balance - 大漠账户查询 diff --git a/danding_bot/plugins/onmyoji_gacha/__init__.py b/danding_bot/plugins/onmyoji_gacha/__init__.py index 5eb627d..1415e8c 100644 --- a/danding_bot/plugins/onmyoji_gacha/__init__.py +++ b/danding_bot/plugins/onmyoji_gacha/__init__.py @@ -1,20 +1,20 @@ -import os -import logging -import random -from nonebot import get_driver, on_command, on_startswith +import os +import logging +import random +from nonebot import get_driver, on_command, on_startswith from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message from nonebot.adapters.onebot.v11.message import MessageSegment from nonebot.typing import T_State from nonebot.rule import Rule from pathlib import Path -from .config import Config -from .gacha import GachaSystem -from .utils import format_sign_in_message, format_user_mention, get_image_path -from .sign_in import award_daily_sign_in_points -from .api_utils import process_ssr_sp_reward, process_achievement_reward -from . import web_api -from danding_bot.plugins.danding_points import points_api +from .config import Config +from .gacha import GachaSystem +from .utils import format_sign_in_message, format_user_mention, get_image_path +from .sign_in import award_daily_sign_in_points +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() @@ -28,24 +28,26 @@ DAILY_STATS_COMMANDS = config.DAILY_STATS_COMMANDS TRIPLE_GACHA_COMMANDS = config.TRIPLE_GACHA_COMMANDS ACHIEVEMENT_COMMANDS = config.ACHIEVEMENT_COMMANDS INTRO_COMMANDS = config.INTRO_COMMANDS +SHOP_COMMANDS = config.SHOP_COMMANDS +EXCHANGE_COMMANDS = config.EXCHANGE_COMMANDS DAILY_LIMIT = config.DAILY_LIMIT -gacha_system = GachaSystem() -logger = logging.getLogger(__name__) +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 = "抽卡签到" - - -@get_driver().on_startup -async def load_gacha_shikigami_data() -> None: - """启动时从 xapi 拉取式神基础数据缓存。""" - - try: - await gacha_system.data_manager.refresh_shikigami_data() - except Exception: - logger.exception("启动拉取抽卡式神缓存失败") +SIGN_IN_REASON = "抽卡签到" + + +@get_driver().on_startup +async def load_gacha_shikigami_data() -> None: + """启动时从 xapi 拉取式神基础数据缓存。""" + + try: + await gacha_system.data_manager.refresh_shikigami_data() + except Exception: + logger.exception("启动拉取抽卡式神缓存失败") # 检查是否允许使用功能的规则 def check_permission() -> Rule: @@ -63,38 +65,38 @@ def check_permission() -> Rule: return Rule(_checker) -async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None: - """处理抽卡成功后的每日签到,不影响主流程""" - try: - points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) - success, new_balance, status = await award_daily_sign_in_points( - data_manager=gacha_system.data_manager, - points_api=points_api, - user_id=user_id, - points=points, - source=SIGN_IN_SOURCE, - reason=SIGN_IN_REASON, - ) - if not success: - if status == "points_failed": - logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points) - else: - logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status) - return - - await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) - except Exception: - logger.exception("处理抽卡签到失败 user_id=%s", user_id) - - -async def claim_achievement_after_reward(user_id: str, achievement_id: str, reward_success: bool) -> None: - """成就奖励发放成功后标记 xapi reward_claimed。""" - - if not reward_success: - return - claimed = await gacha_system.data_manager.claim_achievement_reward(user_id, achievement_id) - if not claimed: - logger.warning("成就奖励已发放但 claim 标记失败 user_id=%s achievement_id=%s", user_id, achievement_id) +async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None: + """处理抽卡成功后的每日签到,不影响主流程""" + try: + points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) + success, new_balance, status = await award_daily_sign_in_points( + data_manager=gacha_system.data_manager, + points_api=points_api, + user_id=user_id, + points=points, + source=SIGN_IN_SOURCE, + reason=SIGN_IN_REASON, + ) + if not success: + if status == "points_failed": + logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points) + else: + logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status) + return + + await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) + except Exception: + logger.exception("处理抽卡签到失败 user_id=%s", user_id) + + +async def claim_achievement_after_reward(user_id: str, achievement_id: str, reward_success: bool) -> None: + """成就奖励发放成功后标记 xapi reward_claimed。""" + + if not reward_success: + return + claimed = await gacha_system.data_manager.claim_achievement_reward(user_id, achievement_id) + if not claimed: + logger.warning("成就奖励已发放但 claim 标记失败 user_id=%s achievement_id=%s", user_id, achievement_id) # 注册抽卡命令,添加权限检查规则 gacha_matcher = on_command("抽卡", aliases=set(GACHA_COMMANDS), priority=10, rule=check_permission()) @@ -105,7 +107,7 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname # 执行抽卡 - result = await gacha_system.draw(user_id) + result = await gacha_system.draw(user_id) if not result["success"]: await gacha_matcher.finish(format_user_mention(user_id, user_name) + " ❌ " + result["message"]) @@ -146,9 +148,9 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): has_manual_rewards = False for achievement_id in unlocked_achievements: - # 尝试自动发放成就奖励 - auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) - await claim_achievement_after_reward(user_id, achievement_id, auto_success) + # 尝试自动发放成就奖励 + auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) + await claim_achievement_after_reward(user_id, achievement_id, auto_success) # 检查是否是重复奖励 if "_repeat_" in achievement_id: @@ -181,7 +183,7 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): msg.append("💰 未自动发放的奖励请联系管理员\n") # 添加成就进度提示 - achievement_data = await gacha_system.get_user_achievements(user_id) + achievement_data = await gacha_system.get_user_achievements(user_id) if achievement_data["success"]: progress = achievement_data["progress"] consecutive_days = progress.get("consecutive_days", 0) @@ -222,7 +224,7 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): msg.append(f"💔 已达月见黑级别:{no_ssr_streak}次 🌚\n") # 添加剩余次数和概率信息 - msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}/{DAILY_LIMIT}\n") + msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}次\n") msg.append(gacha_system.get_probability_text()) # 如果抽到了SSR或SP,处理奖励发放 @@ -246,13 +248,13 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State): await try_handle_daily_sign_in(gacha_matcher, user_id, user_name) return -async def notify_admin(bot: Bot, message: str): - """通知管理员""" - admin_id = 2185330092 - try: - await bot.send_private_msg(user_id=admin_id, message=message) - except Exception as e: - logger.debug("通知管理员失败: %s", e) +async def notify_admin(bot: Bot, message: str): + """通知管理员""" + admin_id = 2185330092 + try: + await bot.send_private_msg(user_id=admin_id, message=message) + except Exception as e: + logger.debug("通知管理员失败: %s", e) # 注册查询命令,添加权限检查规则 stats_matcher = on_command("我的抽卡", aliases=set(STATS_COMMANDS), priority=5, rule=check_permission()) @@ -272,7 +274,7 @@ async def handle_stats(bot: Bot, event: MessageEvent, state: T_State): user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname # 获取用户统计 - stats = await gacha_system.get_user_stats(user_id) + stats = await gacha_system.get_user_stats(user_id) if not stats["success"]: await stats_matcher.finish(format_user_mention(user_id, user_name) + " " + stats["message"]) @@ -314,7 +316,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State): user_name = event.sender.card or event.sender.nickname or "未知用户" # 执行三连抽 - result = await gacha_system.triple_draw(user_id) + result = await gacha_system.triple_draw(user_id) if not result["success"]: await triple_gacha_matcher.finish(f"❌ {result['message']}") @@ -358,9 +360,9 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State): has_manual_rewards = False for achievement_id in unlocked_achievements: - # 尝试自动发放成就奖励 - auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) - await claim_achievement_after_reward(user_id, achievement_id, auto_success) + # 尝试自动发放成就奖励 + auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) + await claim_achievement_after_reward(user_id, achievement_id, auto_success) # 检查是否是重复奖励 if "_repeat_" in achievement_id: @@ -393,7 +395,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State): msg.append("💰 未自动发放的奖励请联系管理员\n") # 添加成就进度提示 - achievement_data = await gacha_system.get_user_achievements(user_id) + achievement_data = await gacha_system.get_user_achievements(user_id) if achievement_data["success"]: progress = achievement_data["progress"] consecutive_days = progress.get("consecutive_days", 0) @@ -435,7 +437,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State): # 添加剩余次数 draws_left = result["draws_left"] - msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}/{DAILY_LIMIT}") + msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}次") # 如果抽到SSR/SP,处理奖励发放 if ssr_count > 0: @@ -463,7 +465,7 @@ async def handle_achievement(bot: Bot, event: MessageEvent, state: T_State): user_name = event.sender.card or event.sender.nickname or "未知用户" # 获取用户成就信息 - result = await gacha_system.get_user_achievements(user_id) + result = await gacha_system.get_user_achievements(user_id) if not result["success"]: await achievement_matcher.finish(f"❌ {result['message']}") @@ -547,6 +549,61 @@ async def handle_achievement(bot: Bot, event: MessageEvent, state: T_State): await achievement_matcher.finish(msg) +# 注册积分商店命令 +shop_matcher = on_command("积分商店", aliases=set(SHOP_COMMANDS), priority=5, rule=check_permission()) + +@shop_matcher.handle() +async def handle_shop(bot: Bot, event: MessageEvent, state: T_State): + """展示积分商店商品目录与该用户个人上下文""" + user_id = str(event.user_id) + user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname + + data = await gacha_system.data_manager.get_shop_products(user_id) + if data is None: + await shop_matcher.finish("❌ 积分商店暂时不可用,请稍后再试") + + products = data.get("products", []) + msg = Message() + msg.append(f"🛒 {format_user_mention(user_id, user_name)} 的积分商店:\n\n") + msg.append(f"💰 Bot积分余额:{data.get('balance', 0)}\n") + msg.append( + f"🎲 今日剩余抽卡:{data.get('draws_left', 0)}次" + f"(其中积分兑换 {data.get('extra_draws_today', 0)} 次)\n\n" + ) + msg.append("📦 在售商品:\n") + for product in products: + msg.append(f"• {product.get('name')}:{product.get('price_points', 0)} 积分\n") + msg.append(f" {product.get('description', '')}\n") + if products: + msg.append(f"\n发送「{EXCHANGE_COMMANDS[0]}」即可兑换") + await shop_matcher.finish(msg) + +# 注册兑换命令 +shop_exchange_matcher = on_command("兑换抽卡", aliases=set(EXCHANGE_COMMANDS), priority=5, rule=check_permission()) + +@shop_exchange_matcher.handle() +async def handle_shop_exchange(bot: Bot, event: MessageEvent, state: T_State): + """用 Bot 积分兑换一次当日抽卡机会(当前目录唯一商品 extra_draw)""" + user_id = str(event.user_id) + user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname + + result = await gacha_system.data_manager.exchange_shop_product(user_id, "extra_draw") + if result is None: + await shop_exchange_matcher.finish("❌ 兑换服务暂时不可用,请稍后再试") + + if not result.get("success"): + await shop_exchange_matcher.finish( + format_user_mention(user_id, user_name) + " ❌ " + str(result.get("message", "兑换失败")) + ) + + msg = Message() + msg.append(f"✅ {format_user_mention(user_id, user_name)} 兑换成功!\n\n") + msg.append("🎁 获得:抽卡机会 ×1(今日有效)\n") + msg.append(f"💰 积分余额:{result.get('balance', 0)}\n") + msg.append(f"📊 今日剩余抽卡:{result.get('draws_left', 0)}次\n") + msg.append(f"\n发送「{GACHA_COMMANDS[0]}」立即使用吧~") + await shop_exchange_matcher.finish(msg) + # 注册查询抽卡指令,支持@用户查询功能 query_matcher = on_command("查询抽卡", aliases={"查询抽奖"}, priority=5, rule=check_permission()) @@ -582,7 +639,7 @@ async def handle_query(bot: Bot, event: MessageEvent, state: T_State): target_user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname # 获取用户统计 - stats = await gacha_system.get_user_stats(target_user_id) + stats = await gacha_system.get_user_stats(target_user_id) # 构建响应消息 msg = Message() @@ -644,7 +701,7 @@ rank_matcher = on_startswith(("抽卡排行","抽卡榜"), priority=1, rule=chec @rank_matcher.handle() async def handle_rank(bot: Bot, event: MessageEvent, state: T_State): # 获取排行榜数据 - rank_data = await gacha_system.get_rank_list() + rank_data = await gacha_system.get_rank_list() if not rank_data: await rank_matcher.finish("暂无抽卡排行榜数据") @@ -689,7 +746,7 @@ async def handle_rank(bot: Bot, event: MessageEvent, state: T_State): @daily_stats_matcher.handle() async def handle_daily_stats(bot: Bot, event: MessageEvent, state: T_State): """处理今日抽卡统计命令""" - result = await gacha_system.get_daily_stats() + result = await gacha_system.get_daily_stats() if not result["success"]: await daily_stats_matcher.finish(f"❌ {result['message']}") @@ -768,6 +825,8 @@ async def handle_intro(bot: Bot, event: MessageEvent, state: T_State): msg += f"• 查询成就:{', '.join(ACHIEVEMENT_COMMANDS)}\n" msg += "• 查询抽卡/查询抽奖:查询自己的或@他人的抽卡数据\n" msg += "• 抽卡排行/抽卡榜:查看SSR/SP排行榜\n" + msg += f"• 积分商店:查看可兑换商品({'/'.join(SHOP_COMMANDS)})\n" + msg += f"• 兑换抽卡:用积分兑换额外抽卡机会({'/'.join(EXCHANGE_COMMANDS)})\n" msg += "\n" # 成就系统 diff --git a/danding_bot/plugins/onmyoji_gacha/config.py b/danding_bot/plugins/onmyoji_gacha/config.py index c382cb8..981e563 100644 --- a/danding_bot/plugins/onmyoji_gacha/config.py +++ b/danding_bot/plugins/onmyoji_gacha/config.py @@ -62,6 +62,8 @@ class Config(BaseSettings): TRIPLE_GACHA_COMMANDS: list = ["三连", "三连抽"] ACHIEVEMENT_COMMANDS: list = ["查询成就", "抽卡成就"] INTRO_COMMANDS: list = ["抽卡介绍", "抽卡说明", "抽卡帮助"] + SHOP_COMMANDS: list = ["积分商店", "兑换商店"] + EXCHANGE_COMMANDS: list = ["兑换抽卡", "积分兑换"] # 成就系统配置 ACHIEVEMENTS: dict = { diff --git a/danding_bot/plugins/onmyoji_gacha/data_manager.py b/danding_bot/plugins/onmyoji_gacha/data_manager.py index 8ddda01..db254d4 100644 --- a/danding_bot/plugins/onmyoji_gacha/data_manager.py +++ b/danding_bot/plugins/onmyoji_gacha/data_manager.py @@ -141,6 +141,20 @@ class DataManager: return await self.get_draws_left(user_id) > 0 + async def get_shop_products(self, user_id: str) -> Optional[Dict[str, Any]]: + """获取积分商店商品目录与该用户的个人上下文。""" + + return await self._request("GET", "shop/products", params={"user_id": user_id}) + + async def exchange_shop_product(self, user_id: str, product_id: str) -> Optional[Dict[str, Any]]: + """兑换积分商店商品;返回含 success/message 的业务结果。""" + + return await self._request( + "POST", + "shop/exchange", + payload={"user_id": user_id, "product_id": product_id}, + ) + async def record_draw_result(self, user_id: str, rarity: str, shikigami: Dict[str, Any]) -> Dict[str, Any]: """写入一次抽卡并返回 xapi 原始业务结果。""" diff --git a/danding_bot/plugins/onmyoji_gacha/gacha.py b/danding_bot/plugins/onmyoji_gacha/gacha.py index a99d709..9583532 100644 --- a/danding_bot/plugins/onmyoji_gacha/gacha.py +++ b/danding_bot/plugins/onmyoji_gacha/gacha.py @@ -24,47 +24,46 @@ class GachaSystem: def __init__(self): self.data_manager = data_manager - async def draw(self, user_id: str) -> Dict[str, Any]: - """执行一次抽卡""" - # 检查抽卡限制 - if not await self.data_manager.check_daily_limit(user_id): - draws_left = await self.data_manager.get_draws_left(user_id) - return { - "success": False, - "message": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!" - } + async def draw(self, user_id: str) -> Dict[str, Any]: + """执行一次抽卡""" + # 检查抽卡限制 + if not await self.data_manager.check_daily_limit(user_id): + return { + "success": False, + "message": "您今日的抽卡次数已用完,可在积分商店兑换额外机会,或明天再来吧!" + } # 抽取稀有度(传递用户ID) rarity = self._draw_rarity(user_id) # 从该稀有度中抽取式神 - await self.data_manager.ensure_shikigami_data() - shikigami_data = self.data_manager.shikigami_data.get(rarity, []) - if not shikigami_data: - return { - "success": False, - "message": f"系统错误:{rarity}稀有度下没有可用式神" - } + await self.data_manager.ensure_shikigami_data() + shikigami_data = self.data_manager.shikigami_data.get(rarity, []) + if not shikigami_data: + return { + "success": False, + "message": f"系统错误:{rarity}稀有度下没有可用式神" + } # 随机选择式神 shikigami = random.choice(shikigami_data) - # xapi 只负责写入抽卡数据;奖励副作用仍由 nonebot handler 编排。 - record_result = await self.data_manager.record_draw_result(user_id, rarity, shikigami) - if not record_result.get("success"): - return { - "success": False, - "message": record_result.get("message", "抽卡记录写入失败") - } + # xapi 只负责写入抽卡数据;奖励副作用仍由 nonebot handler 编排。 + record_result = await self.data_manager.record_draw_result(user_id, rarity, shikigami) + if not record_result.get("success"): + return { + "success": False, + "message": record_result.get("message", "抽卡记录写入失败") + } return { - "success": True, - "rarity": record_result.get("rarity", rarity), - "name": record_result.get("name", shikigami["name"]), - "image_url": record_result.get("image_url") or record_result.get("image_path") or shikigami["image_url"], - "draws_left": record_result.get("draws_left", 0), - "unlocked_achievements": record_result.get("unlocked_achievements", []) - } + "success": True, + "rarity": record_result.get("rarity", rarity), + "name": record_result.get("name", shikigami["name"]), + "image_url": record_result.get("image_url") or record_result.get("image_path") or shikigami["image_url"], + "draws_left": record_result.get("draws_left", 0), + "unlocked_achievements": record_result.get("unlocked_achievements", []) + } def _draw_rarity(self, user_id: str = None) -> str: """按概率抽取稀有度""" @@ -85,39 +84,39 @@ class GachaSystem: # 默认返回R,理论上不会执行到这里 return "R" - async def get_user_stats(self, user_id: str) -> Dict: - """获取用户抽卡统计""" - return await self.data_manager.get_user_stats(user_id) + async def get_user_stats(self, user_id: str) -> Dict: + """获取用户抽卡统计""" + return await self.data_manager.get_user_stats(user_id) def get_probability_text(self) -> str: """获取概率展示文本""" probs = config.RARITY_PROBABILITY return f"--- 系统概率 ---\nR: {probs['R']}% | SR: {probs['SR']}% | SSR: {probs['SSR']}% | SP: {probs['SP']}%" - async def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]: - """获取抽卡排行榜数据""" - items = await self.data_manager.get_rank(limit=10) - return [(item["user_id"], item) for item in items] - - async def get_daily_stats(self) -> Dict: - """获取今日抽卡统计""" - return await self.data_manager.get_daily_stats() - - async def triple_draw(self, user_id: str) -> Dict: - """执行三连抽""" - # 检查是否有足够的抽卡次数 - draws_left = await self.data_manager.get_draws_left(user_id) - if draws_left < 3: - return { - "success": False, + async def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]: + """获取抽卡排行榜数据""" + items = await self.data_manager.get_rank(limit=10) + return [(item["user_id"], item) for item in items] + + async def get_daily_stats(self) -> Dict: + """获取今日抽卡统计""" + return await self.data_manager.get_daily_stats() + + async def triple_draw(self, user_id: str) -> Dict: + """执行三连抽""" + # 检查是否有足够的抽卡次数 + draws_left = await self.data_manager.get_draws_left(user_id) + if draws_left < 3: + return { + "success": False, "message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会,三连抽需要3次机会" } results = [] - await self.data_manager.ensure_shikigami_data() - - # 执行三次本地概率抽取,统一提交 xapi 三连写入端点。 - for i in range(3): + await self.data_manager.ensure_shikigami_data() + + # 执行三次本地概率抽取,统一提交 xapi 三连写入端点。 + for i in range(3): # 抽取稀有度(传递用户ID) rarity = self._draw_rarity(user_id) @@ -132,30 +131,30 @@ class GachaSystem: # 随机选择式神 shikigami = random.choice(shikigami_data) - results.append({ - "id": shikigami["id"], - "rarity": rarity, - "name": shikigami["name"], - "image_url": shikigami["image_url"] - }) - - record_result = await self.data_manager.record_triple_draw_result(user_id, results) - if not record_result.get("success"): - return { - "success": False, - "message": record_result.get("message", "三连抽记录写入失败") - } - - return { - "success": True, - "results": record_result.get("results", results), - "draws_left": record_result.get("draws_left", 0), - "unlocked_achievements": record_result.get("unlocked_achievements", []) - } - - async def get_user_achievements(self, user_id: str) -> Dict: - """获取用户成就信息""" - achievement_data = await self.data_manager.get_user_achievements(user_id) + results.append({ + "id": shikigami["id"], + "rarity": rarity, + "name": shikigami["name"], + "image_url": shikigami["image_url"] + }) + + record_result = await self.data_manager.record_triple_draw_result(user_id, results) + if not record_result.get("success"): + return { + "success": False, + "message": record_result.get("message", "三连抽记录写入失败") + } + + return { + "success": True, + "results": record_result.get("results", results), + "draws_left": record_result.get("draws_left", 0), + "unlocked_achievements": record_result.get("unlocked_achievements", []) + } + + async def get_user_achievements(self, user_id: str) -> Dict: + """获取用户成就信息""" + achievement_data = await self.data_manager.get_user_achievements(user_id) if not achievement_data["unlocked"] and all(v == 0 for v in achievement_data["progress"].values()): return { @@ -169,6 +168,6 @@ class GachaSystem: "progress": achievement_data["progress"] } - async def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict: - """获取每日详细抽卡记录""" - return await self.data_manager.get_daily_records(date) + async def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict: + """获取每日详细抽卡记录""" + return await self.data_manager.get_daily_records(date) diff --git a/tests/test_onmyoji_gacha_http_api.py b/tests/test_onmyoji_gacha_http_api.py index 736318d..e655955 100644 --- a/tests/test_onmyoji_gacha_http_api.py +++ b/tests/test_onmyoji_gacha_http_api.py @@ -371,3 +371,98 @@ async def test_daily_sign_in_repeat_does_not_award_points(): assert (success, balance, status) == (False, 0, "signed_already_or_failed") assert calls == [("sign-in", "10001", 20)] + + +@pytest.mark.asyncio +async def test_shop_products_and_exchange_call_xapi_with_auth(fake_aiohttp): + responses, calls = fake_aiohttp + responses.extend( + [ + success_data( + { + "user_id": "10001", + "date": "2026-08-15", + "balance": 120, + "extra_draws_today": 0, + "daily_limit": 3, + "draws_left": 2, + "products": [ + { + "product_id": "extra_draw", + "name": "抽卡机会", + "description": "消耗 99 Bot 积分兑换 1 次抽卡机会,当日有效,次日清零", + "price_points": 99, + "effect": "extra_draw", + "valid_scope": "today", + } + ], + } + ), + success_data( + { + "success": True, + "product_id": "extra_draw", + "cost_points": 99, + "balance": 21, + "date": "2026-08-15", + "extra_draws_today": 1, + "daily_limit": 3, + "draws_left": 3, + } + ), + ] + ) + manager = DataManager() + + products = await manager.get_shop_products("10001") + exchanged = await manager.exchange_shop_product("10001", "extra_draw") + + assert products["products"][0]["price_points"] == 99 + assert products["balance"] == 120 + assert products["draws_left"] == 2 + assert exchanged["success"] is True + assert exchanged["balance"] == 21 + assert exchanged["draws_left"] == 3 + assert calls[0]["method"] == "GET" + assert calls[0]["url"] == "http://xapi.test/bot/gacha/shop/products" + assert calls[0]["params"] == {"user": "robot", "token": "secret", "user_id": "10001"} + assert calls[1]["method"] == "POST" + assert calls[1]["json"] == { + "user": "robot", + "token": "secret", + "user_id": "10001", + "product_id": "extra_draw", + } + + +@pytest.mark.asyncio +async def test_shop_exchange_rejection_keeps_message_from_xapi(fake_aiohttp): + """业务拒绝时插件拿到 data.success=false 与 message,不做本地判断。""" + + responses, calls = fake_aiohttp + responses.append( + success_data({"success": False, "message": "Bot积分不足,还需49积分", "balance": 50}) + ) + manager = DataManager() + + exchanged = await manager.exchange_shop_product("10001", "extra_draw") + + assert exchanged == {"success": False, "message": "Bot积分不足,还需49积分", "balance": 50} + + +@pytest.mark.asyncio +async def test_shop_methods_return_none_on_network_error(monkeypatch): + """网络异常时商店方法返回 None,与插件其他 API 方法一致。""" + + class FailingSession: + async def __aenter__(self): + raise aiohttp.ClientError("offline") + + async def __aexit__(self, exc_type, exc, tb): + return None + + monkeypatch.setattr(data_manager_module.aiohttp, "ClientSession", lambda: FailingSession()) + manager = DataManager() + + assert await manager.get_shop_products("10001") is None + assert await manager.exchange_shop_product("10001", "extra_draw") is None