【积分商店】onmyoji_gacha 新增积分商店与兑换抽卡命令

This commit is contained in:
2026-08-15 20:50:38 +08:00
parent cb94794e38
commit 7f3f744af5
6 changed files with 331 additions and 160 deletions

View File

@@ -117,6 +117,8 @@
- `今日抽卡`: 查看今日抽卡统计。 - `今日抽卡`: 查看今日抽卡统计。
- `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。 - `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。
- `抽卡介绍`: 查看详细机制与奖励说明。 - `抽卡介绍`: 查看详细机制与奖励说明。
- `积分商店``兑换商店`: 查看可兑换商品、余额与剩余抽卡次数。
- `兑换抽卡``积分兑换`: 消耗 99 Bot 积分兑换 1 次当日抽卡机会。
- **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。 - **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。
### 10. damo_balance - 大漠账户查询 ### 10. damo_balance - 大漠账户查询

View File

@@ -1,20 +1,20 @@
import os import os
import logging import logging
import random import random
from nonebot import get_driver, on_command, on_startswith from nonebot import get_driver, on_command, on_startswith
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message
from nonebot.adapters.onebot.v11.message import MessageSegment from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.rule import Rule from nonebot.rule import Rule
from pathlib import Path from pathlib import Path
from .config import Config from .config import Config
from .gacha import GachaSystem from .gacha import GachaSystem
from .utils import format_sign_in_message, format_user_mention, get_image_path from .utils import format_sign_in_message, format_user_mention, get_image_path
from .sign_in import award_daily_sign_in_points from .sign_in import award_daily_sign_in_points
from .api_utils import process_ssr_sp_reward, process_achievement_reward from .api_utils import process_ssr_sp_reward, process_achievement_reward
from . import web_api from . import web_api
from danding_bot.plugins.danding_points import points_api from danding_bot.plugins.danding_points import points_api
# 创建Config实例 # 创建Config实例
config = Config() config = Config()
@@ -28,24 +28,26 @@ DAILY_STATS_COMMANDS = config.DAILY_STATS_COMMANDS
TRIPLE_GACHA_COMMANDS = config.TRIPLE_GACHA_COMMANDS TRIPLE_GACHA_COMMANDS = config.TRIPLE_GACHA_COMMANDS
ACHIEVEMENT_COMMANDS = config.ACHIEVEMENT_COMMANDS ACHIEVEMENT_COMMANDS = config.ACHIEVEMENT_COMMANDS
INTRO_COMMANDS = config.INTRO_COMMANDS INTRO_COMMANDS = config.INTRO_COMMANDS
SHOP_COMMANDS = config.SHOP_COMMANDS
EXCHANGE_COMMANDS = config.EXCHANGE_COMMANDS
DAILY_LIMIT = config.DAILY_LIMIT DAILY_LIMIT = config.DAILY_LIMIT
gacha_system = GachaSystem() gacha_system = GachaSystem()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SIGN_IN_MIN_POINTS = 1 SIGN_IN_MIN_POINTS = 1
SIGN_IN_MAX_POINTS = 100 SIGN_IN_MAX_POINTS = 100
SIGN_IN_SOURCE = "gacha_sign" SIGN_IN_SOURCE = "gacha_sign"
SIGN_IN_REASON = "抽卡签到" SIGN_IN_REASON = "抽卡签到"
@get_driver().on_startup @get_driver().on_startup
async def load_gacha_shikigami_data() -> None: async def load_gacha_shikigami_data() -> None:
"""启动时从 xapi 拉取式神基础数据缓存。""" """启动时从 xapi 拉取式神基础数据缓存。"""
try: try:
await gacha_system.data_manager.refresh_shikigami_data() await gacha_system.data_manager.refresh_shikigami_data()
except Exception: except Exception:
logger.exception("启动拉取抽卡式神缓存失败") logger.exception("启动拉取抽卡式神缓存失败")
# 检查是否允许使用功能的规则 # 检查是否允许使用功能的规则
def check_permission() -> Rule: def check_permission() -> Rule:
@@ -63,38 +65,38 @@ def check_permission() -> Rule:
return Rule(_checker) return Rule(_checker)
async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None: async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None:
"""处理抽卡成功后的每日签到,不影响主流程""" """处理抽卡成功后的每日签到,不影响主流程"""
try: try:
points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS)
success, new_balance, status = await award_daily_sign_in_points( success, new_balance, status = await award_daily_sign_in_points(
data_manager=gacha_system.data_manager, data_manager=gacha_system.data_manager,
points_api=points_api, points_api=points_api,
user_id=user_id, user_id=user_id,
points=points, points=points,
source=SIGN_IN_SOURCE, source=SIGN_IN_SOURCE,
reason=SIGN_IN_REASON, reason=SIGN_IN_REASON,
) )
if not success: if not success:
if status == "points_failed": if status == "points_failed":
logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points) logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points)
else: else:
logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status) logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status)
return return
await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance))
except Exception: except Exception:
logger.exception("处理抽卡签到失败 user_id=%s", user_id) logger.exception("处理抽卡签到失败 user_id=%s", user_id)
async def claim_achievement_after_reward(user_id: str, achievement_id: str, reward_success: bool) -> None: async def claim_achievement_after_reward(user_id: str, achievement_id: str, reward_success: bool) -> None:
"""成就奖励发放成功后标记 xapi reward_claimed。""" """成就奖励发放成功后标记 xapi reward_claimed。"""
if not reward_success: if not reward_success:
return return
claimed = await gacha_system.data_manager.claim_achievement_reward(user_id, achievement_id) claimed = await gacha_system.data_manager.claim_achievement_reward(user_id, achievement_id)
if not claimed: if not claimed:
logger.warning("成就奖励已发放但 claim 标记失败 user_id=%s achievement_id=%s", user_id, achievement_id) 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()) 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 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"]: if not result["success"]:
await gacha_matcher.finish(format_user_mention(user_id, user_name) + "" + result["message"]) 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 has_manual_rewards = False
for achievement_id in unlocked_achievements: for achievement_id in unlocked_achievements:
# 尝试自动发放成就奖励 # 尝试自动发放成就奖励
auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id)
await claim_achievement_after_reward(user_id, achievement_id, auto_success) await claim_achievement_after_reward(user_id, achievement_id, auto_success)
# 检查是否是重复奖励 # 检查是否是重复奖励
if "_repeat_" in achievement_id: if "_repeat_" in achievement_id:
@@ -181,7 +183,7 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State):
msg.append("💰 未自动发放的奖励请联系管理员\n") 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"]: if achievement_data["success"]:
progress = achievement_data["progress"] progress = achievement_data["progress"]
consecutive_days = progress.get("consecutive_days", 0) 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"💔 已达月见黑级别:{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()) msg.append(gacha_system.get_probability_text())
# 如果抽到了SSR或SP处理奖励发放 # 如果抽到了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) await try_handle_daily_sign_in(gacha_matcher, user_id, user_name)
return return
async def notify_admin(bot: Bot, message: str): async def notify_admin(bot: Bot, message: str):
"""通知管理员""" """通知管理员"""
admin_id = 2185330092 admin_id = 2185330092
try: try:
await bot.send_private_msg(user_id=admin_id, message=message) await bot.send_private_msg(user_id=admin_id, message=message)
except Exception as e: except Exception as e:
logger.debug("通知管理员失败: %s", e) logger.debug("通知管理员失败: %s", e)
# 注册查询命令,添加权限检查规则 # 注册查询命令,添加权限检查规则
stats_matcher = on_command("我的抽卡", aliases=set(STATS_COMMANDS), priority=5, rule=check_permission()) 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 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"]: if not stats["success"]:
await stats_matcher.finish(format_user_mention(user_id, user_name) + " " + stats["message"]) 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 "未知用户" 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"]: if not result["success"]:
await triple_gacha_matcher.finish(f"{result['message']}") 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 has_manual_rewards = False
for achievement_id in unlocked_achievements: for achievement_id in unlocked_achievements:
# 尝试自动发放成就奖励 # 尝试自动发放成就奖励
auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id) auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id)
await claim_achievement_after_reward(user_id, achievement_id, auto_success) await claim_achievement_after_reward(user_id, achievement_id, auto_success)
# 检查是否是重复奖励 # 检查是否是重复奖励
if "_repeat_" in achievement_id: if "_repeat_" in achievement_id:
@@ -393,7 +395,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State):
msg.append("💰 未自动发放的奖励请联系管理员\n") 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"]: if achievement_data["success"]:
progress = achievement_data["progress"] progress = achievement_data["progress"]
consecutive_days = progress.get("consecutive_days", 0) 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"] draws_left = result["draws_left"]
msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}/{DAILY_LIMIT}") msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}")
# 如果抽到SSR/SP处理奖励发放 # 如果抽到SSR/SP处理奖励发放
if ssr_count > 0: 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 "未知用户" 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"]: if not result["success"]:
await achievement_matcher.finish(f"{result['message']}") 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) 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()) 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 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() msg = Message()
@@ -644,7 +701,7 @@ rank_matcher = on_startswith(("抽卡排行","抽卡榜"), priority=1, rule=chec
@rank_matcher.handle() @rank_matcher.handle()
async def handle_rank(bot: Bot, event: MessageEvent, state: T_State): 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: if not rank_data:
await rank_matcher.finish("暂无抽卡排行榜数据") await rank_matcher.finish("暂无抽卡排行榜数据")
@@ -689,7 +746,7 @@ async def handle_rank(bot: Bot, event: MessageEvent, state: T_State):
@daily_stats_matcher.handle() @daily_stats_matcher.handle()
async def handle_daily_stats(bot: Bot, event: MessageEvent, state: T_State): 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"]: if not result["success"]:
await daily_stats_matcher.finish(f"{result['message']}") 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 += f"• 查询成就:{', '.join(ACHIEVEMENT_COMMANDS)}\n"
msg += "• 查询抽卡/查询抽奖:查询自己的或@他人的抽卡数据\n" msg += "• 查询抽卡/查询抽奖:查询自己的或@他人的抽卡数据\n"
msg += "• 抽卡排行/抽卡榜查看SSR/SP排行榜\n" msg += "• 抽卡排行/抽卡榜查看SSR/SP排行榜\n"
msg += f"• 积分商店:查看可兑换商品({'/'.join(SHOP_COMMANDS)}\n"
msg += f"• 兑换抽卡:用积分兑换额外抽卡机会({'/'.join(EXCHANGE_COMMANDS)}\n"
msg += "\n" msg += "\n"
# 成就系统 # 成就系统

View File

@@ -62,6 +62,8 @@ class Config(BaseSettings):
TRIPLE_GACHA_COMMANDS: list = ["三连", "三连抽"] TRIPLE_GACHA_COMMANDS: list = ["三连", "三连抽"]
ACHIEVEMENT_COMMANDS: list = ["查询成就", "抽卡成就"] ACHIEVEMENT_COMMANDS: list = ["查询成就", "抽卡成就"]
INTRO_COMMANDS: list = ["抽卡介绍", "抽卡说明", "抽卡帮助"] INTRO_COMMANDS: list = ["抽卡介绍", "抽卡说明", "抽卡帮助"]
SHOP_COMMANDS: list = ["积分商店", "兑换商店"]
EXCHANGE_COMMANDS: list = ["兑换抽卡", "积分兑换"]
# 成就系统配置 # 成就系统配置
ACHIEVEMENTS: dict = { ACHIEVEMENTS: dict = {

View File

@@ -141,6 +141,20 @@ class DataManager:
return await self.get_draws_left(user_id) > 0 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]: async def record_draw_result(self, user_id: str, rarity: str, shikigami: Dict[str, Any]) -> Dict[str, Any]:
"""写入一次抽卡并返回 xapi 原始业务结果。""" """写入一次抽卡并返回 xapi 原始业务结果。"""

View File

@@ -24,47 +24,46 @@ class GachaSystem:
def __init__(self): def __init__(self):
self.data_manager = data_manager self.data_manager = data_manager
async def draw(self, user_id: str) -> Dict[str, Any]: async def draw(self, user_id: str) -> Dict[str, Any]:
"""执行一次抽卡""" """执行一次抽卡"""
# 检查抽卡限制 # 检查抽卡限制
if not await self.data_manager.check_daily_limit(user_id): if not await self.data_manager.check_daily_limit(user_id):
draws_left = await self.data_manager.get_draws_left(user_id) return {
return { "success": False,
"success": False, "message": "您今日的抽卡次数已用完,可在积分商店兑换额外机会,或明天再来吧!"
"message": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!" }
}
# 抽取稀有度传递用户ID # 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id) rarity = self._draw_rarity(user_id)
# 从该稀有度中抽取式神 # 从该稀有度中抽取式神
await self.data_manager.ensure_shikigami_data() await self.data_manager.ensure_shikigami_data()
shikigami_data = self.data_manager.shikigami_data.get(rarity, []) shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
if not shikigami_data: if not shikigami_data:
return { return {
"success": False, "success": False,
"message": f"系统错误:{rarity}稀有度下没有可用式神" "message": f"系统错误:{rarity}稀有度下没有可用式神"
} }
# 随机选择式神 # 随机选择式神
shikigami = random.choice(shikigami_data) shikigami = random.choice(shikigami_data)
# xapi 只负责写入抽卡数据;奖励副作用仍由 nonebot handler 编排。 # xapi 只负责写入抽卡数据;奖励副作用仍由 nonebot handler 编排。
record_result = await self.data_manager.record_draw_result(user_id, rarity, shikigami) record_result = await self.data_manager.record_draw_result(user_id, rarity, shikigami)
if not record_result.get("success"): if not record_result.get("success"):
return { return {
"success": False, "success": False,
"message": record_result.get("message", "抽卡记录写入失败") "message": record_result.get("message", "抽卡记录写入失败")
} }
return { return {
"success": True, "success": True,
"rarity": record_result.get("rarity", rarity), "rarity": record_result.get("rarity", rarity),
"name": record_result.get("name", shikigami["name"]), "name": record_result.get("name", shikigami["name"]),
"image_url": record_result.get("image_url") or record_result.get("image_path") or shikigami["image_url"], "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), "draws_left": record_result.get("draws_left", 0),
"unlocked_achievements": record_result.get("unlocked_achievements", []) "unlocked_achievements": record_result.get("unlocked_achievements", [])
} }
def _draw_rarity(self, user_id: str = None) -> str: def _draw_rarity(self, user_id: str = None) -> str:
"""按概率抽取稀有度""" """按概率抽取稀有度"""
@@ -85,39 +84,39 @@ class GachaSystem:
# 默认返回R理论上不会执行到这里 # 默认返回R理论上不会执行到这里
return "R" return "R"
async def get_user_stats(self, user_id: str) -> Dict: async def get_user_stats(self, user_id: str) -> Dict:
"""获取用户抽卡统计""" """获取用户抽卡统计"""
return await self.data_manager.get_user_stats(user_id) return await self.data_manager.get_user_stats(user_id)
def get_probability_text(self) -> str: def get_probability_text(self) -> str:
"""获取概率展示文本""" """获取概率展示文本"""
probs = config.RARITY_PROBABILITY probs = config.RARITY_PROBABILITY
return f"--- 系统概率 ---\nR: {probs['R']}% | SR: {probs['SR']}% | SSR: {probs['SSR']}% | SP: {probs['SP']}%" 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]]]: async def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]:
"""获取抽卡排行榜数据""" """获取抽卡排行榜数据"""
items = await self.data_manager.get_rank(limit=10) items = await self.data_manager.get_rank(limit=10)
return [(item["user_id"], item) for item in items] return [(item["user_id"], item) for item in items]
async def get_daily_stats(self) -> Dict: async def get_daily_stats(self) -> Dict:
"""获取今日抽卡统计""" """获取今日抽卡统计"""
return await self.data_manager.get_daily_stats() return await self.data_manager.get_daily_stats()
async def triple_draw(self, user_id: str) -> Dict: async def triple_draw(self, user_id: str) -> Dict:
"""执行三连抽""" """执行三连抽"""
# 检查是否有足够的抽卡次数 # 检查是否有足够的抽卡次数
draws_left = await self.data_manager.get_draws_left(user_id) draws_left = await self.data_manager.get_draws_left(user_id)
if draws_left < 3: if draws_left < 3:
return { return {
"success": False, "success": False,
"message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会三连抽需要3次机会" "message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会三连抽需要3次机会"
} }
results = [] results = []
await self.data_manager.ensure_shikigami_data() await self.data_manager.ensure_shikigami_data()
# 执行三次本地概率抽取,统一提交 xapi 三连写入端点。 # 执行三次本地概率抽取,统一提交 xapi 三连写入端点。
for i in range(3): for i in range(3):
# 抽取稀有度传递用户ID # 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id) rarity = self._draw_rarity(user_id)
@@ -132,30 +131,30 @@ class GachaSystem:
# 随机选择式神 # 随机选择式神
shikigami = random.choice(shikigami_data) shikigami = random.choice(shikigami_data)
results.append({ results.append({
"id": shikigami["id"], "id": shikigami["id"],
"rarity": rarity, "rarity": rarity,
"name": shikigami["name"], "name": shikigami["name"],
"image_url": shikigami["image_url"] "image_url": shikigami["image_url"]
}) })
record_result = await self.data_manager.record_triple_draw_result(user_id, results) record_result = await self.data_manager.record_triple_draw_result(user_id, results)
if not record_result.get("success"): if not record_result.get("success"):
return { return {
"success": False, "success": False,
"message": record_result.get("message", "三连抽记录写入失败") "message": record_result.get("message", "三连抽记录写入失败")
} }
return { return {
"success": True, "success": True,
"results": record_result.get("results", results), "results": record_result.get("results", results),
"draws_left": record_result.get("draws_left", 0), "draws_left": record_result.get("draws_left", 0),
"unlocked_achievements": record_result.get("unlocked_achievements", []) "unlocked_achievements": record_result.get("unlocked_achievements", [])
} }
async def get_user_achievements(self, user_id: str) -> Dict: async def get_user_achievements(self, user_id: str) -> Dict:
"""获取用户成就信息""" """获取用户成就信息"""
achievement_data = await self.data_manager.get_user_achievements(user_id) 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()): if not achievement_data["unlocked"] and all(v == 0 for v in achievement_data["progress"].values()):
return { return {
@@ -169,6 +168,6 @@ class GachaSystem:
"progress": achievement_data["progress"] "progress": achievement_data["progress"]
} }
async def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict: async def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict:
"""获取每日详细抽卡记录""" """获取每日详细抽卡记录"""
return await self.data_manager.get_daily_records(date) return await self.data_manager.get_daily_records(date)

View File

@@ -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 (success, balance, status) == (False, 0, "signed_already_or_failed")
assert calls == [("sign-in", "10001", 20)] 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