【积分商店】onmyoji_gacha 新增积分商店与兑换抽卡命令
This commit is contained in:
@@ -117,6 +117,8 @@
|
|||||||
- `今日抽卡`: 查看今日抽卡统计。
|
- `今日抽卡`: 查看今日抽卡统计。
|
||||||
- `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。
|
- `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。
|
||||||
- `抽卡介绍`: 查看详细机制与奖励说明。
|
- `抽卡介绍`: 查看详细机制与奖励说明。
|
||||||
|
- `积分商店`(`兑换商店`): 查看可兑换商品、余额与剩余抽卡次数。
|
||||||
|
- `兑换抽卡`(`积分兑换`): 消耗 99 Bot 积分兑换 1 次当日抽卡机会。
|
||||||
- **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。
|
- **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。
|
||||||
### 10. damo_balance - 大漠账户查询
|
### 10. damo_balance - 大漠账户查询
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
# 成就系统
|
# 成就系统
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 原始业务结果。"""
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user