【积分商店】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

@@ -28,6 +28,8 @@ 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()
@@ -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处理奖励发放
@@ -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:
@@ -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())
@@ -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

@@ -28,10 +28,9 @@ class GachaSystem:
"""执行一次抽卡""" """执行一次抽卡"""
# 检查抽卡限制 # 检查抽卡限制
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": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!" "message": "您今日的抽卡次数已用完,可在积分商店兑换额外机会,或明天再来吧!"
} }
# 抽取稀有度传递用户ID # 抽取稀有度传递用户ID

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