merge: resolve onmyoji_gacha conflicts
This commit is contained in:
19
PLUGINS.md
19
PLUGINS.md
@@ -95,16 +95,29 @@
|
||||
|
||||
### 9. onmyoji_gacha - 阴阳师抽卡模拟
|
||||
|
||||
高度还原的抽卡模拟,包含成就系统。
|
||||
高度还原的抽卡模拟,包含成就系统。采用模块化架构,职责分明。
|
||||
|
||||
- **模块结构**:
|
||||
- `__init__.py`: 路由注册与matcher定义(167行)
|
||||
- `config.py`: Pydantic配置管理
|
||||
- `gacha.py`: 抽卡核心逻辑(GachaSystem类)
|
||||
- `data_manager.py`: SQLite数据持久化(DataManager类)
|
||||
- `rules.py`: 命令匹配规则(check_permission等)
|
||||
- `formatters.py`: 消息格式化(9个格式化函数)
|
||||
- `handlers/`: 命令处理函数(9个handler模块)
|
||||
- `utils.py`: 通用工具函数
|
||||
- `api_utils.py`: 积分系统API交互
|
||||
- `web_api.py`: HTTP API路由
|
||||
|
||||
- **主要命令**:
|
||||
- `抽卡`: 执行单抽。
|
||||
- `三连抽`: 执行三连抽。
|
||||
- `我的抽卡`: 查看个人统计。
|
||||
- `抽卡排行`: 查看抽卡排行榜。
|
||||
- `今日抽卡`: 查看今日抽卡统计。
|
||||
- `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。
|
||||
- `抽卡介绍`: 查看详细机制与奖励说明。
|
||||
- **特性**: 抽中 SSR/SP 可获得“蛋定助手”卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。
|
||||
|
||||
- **特性**: 抽中 SSR/SP 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励。
|
||||
### 10. damo_balance - 大漠账户查询
|
||||
|
||||
查询大漠平台账户余额。
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
"""
|
||||
阴阳师抽卡插件 - 数据管理模块
|
||||
|
||||
管理抽卡数据持久化,包括:
|
||||
- SQLite数据库操作
|
||||
- 用户抽卡记录管理
|
||||
- 每日签到记录
|
||||
- 统计查询
|
||||
|
||||
TODO(代码评审 2026-05-03): 本模块承担了数据文件IO + 缓存 + 业务规则三重职责,
|
||||
后续应拆分为: data_io(纯文件读写) / data_cache(内存缓存层) / data_rules(业务规则校验)。
|
||||
当前拆分风险较大(影响面广),暂维持现状。
|
||||
|
||||
TODO(第二轮评审 2026-05-03): 补充建议拆分方案:
|
||||
- achievement_manager.py: 成就定义加载 + 进度计算 + 奖励发放 (~150行)
|
||||
- record_manager.py: 记录归档 + 统计查询 + 每日数据 (~100行)
|
||||
- data_manager.py: 核心用户数据IO + 缓存管理 (~359行)
|
||||
拆分为独立PR,不阻塞当前修复。
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
@@ -12,6 +32,7 @@ from .config import Config
|
||||
config = Config()
|
||||
|
||||
class DataManager:
|
||||
"""抽卡数据管理器,封装所有数据库操作"""
|
||||
def __init__(self):
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True)
|
||||
@@ -102,7 +123,7 @@ class DataManager:
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _init_sign_in_table(self, cursor: sqlite3.Cursor) -> None:
|
||||
def _init_sign_in_table(self, cursor: sqlite3.Cursor) -> None: # OK
|
||||
"""创建每日签到表"""
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS daily_sign_in (
|
||||
@@ -115,7 +136,7 @@ class DataManager:
|
||||
)
|
||||
""")
|
||||
|
||||
def update_achievement_progress(self, user_id: str, rarity: str) -> List[str]:
|
||||
def update_achievement_progress(self, user_id: str, rarity: str) -> List[str]: # type: ignore[return]
|
||||
"""更新用户成就进度,返回新解锁的成就列表"""
|
||||
today = self.get_today_date()
|
||||
unlocked_achievements = []
|
||||
|
||||
299
danding_bot/plugins/onmyoji_gacha/formatters.py
Normal file
299
danding_bot/plugins/onmyoji_gacha/formatters.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
消息格式化模块 - 抽卡、成就、统计等所有用户可见输出
|
||||
|
||||
提供所有用户可见消息的格式化函数,包括:
|
||||
- 抽卡结果消息
|
||||
- 三连抽结果消息
|
||||
- 成就通知消息
|
||||
- 统计查询消息
|
||||
- 排行榜消息
|
||||
- 每日统计消息
|
||||
|
||||
所有函数返回NoneBot的Message对象,可直接用于matcher.send()。
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from nonebot.adapters.onebot.v11 import Message, MessageSegment
|
||||
from .utils import format_user_mention, get_image_path
|
||||
import os
|
||||
|
||||
|
||||
# 稀有度显示配置(单一数据源,消除重复模式)
|
||||
RARITY_DISPLAY = {
|
||||
"SSR": {"congrats": ("🌟✨", "✨🌟"), "card": "🎊", "desc": "SSR", "tail": "💫"},
|
||||
"SP": {"congrats": ("🌈🎆", "🎆🌈"), "card": "🎉", "desc": "SP", "tail": "🔥"},
|
||||
"SR": {"congrats": ("⭐", "⭐"), "card": "✨", "desc": "SR", "tail": ""},
|
||||
"R": {"congrats": ("🍀", "🍀"), "card": "📜", "desc": "R", "tail": ""},
|
||||
}
|
||||
|
||||
def format_gacha_result(rarity: str, name: str, user_id: str, user_name: str, image_url: str) -> Message:
|
||||
"""
|
||||
格式化单次抽卡结果消息。
|
||||
|
||||
Args:
|
||||
rarity: 稀有度 (SSR/SP/SR/R)
|
||||
name: 式神名称
|
||||
user_id: QQ号
|
||||
user_name: 用户昵称
|
||||
image_url: 图片路径
|
||||
|
||||
Returns:
|
||||
Message: 包含文本和图片的消息对象
|
||||
"""
|
||||
if not rarity or not name:
|
||||
return Message("[抽卡] 数据不完整")
|
||||
msg = Message()
|
||||
if rarity == "SSR":
|
||||
msg.append(f"🌟✨ 恭喜 {format_user_mention(user_id, user_name)} ✨🌟\n")
|
||||
msg.append(f"🎊 抽到了 SSR 式神:{name} 🎊\n")
|
||||
msg.append(f"💫 真是太幸运了!💫")
|
||||
elif rarity == "SP":
|
||||
msg.append(f"🌈🎆 恭喜 {format_user_mention(user_id, user_name)} 🎆🌈\n")
|
||||
msg.append(f"🎉 抽到了 SP 式神:{name} 🎉\n")
|
||||
msg.append(f"🔥 这是传说中的SP!🔥")
|
||||
elif rarity == "SR":
|
||||
msg.append(f"⭐ 恭喜 {format_user_mention(user_id, user_name)} ⭐\n")
|
||||
msg.append(f"✨ 抽到了 SR 式神:{name} ✨")
|
||||
else: # R
|
||||
msg.append(f"🍀 {format_user_mention(user_id, user_name)} 🍀\n")
|
||||
msg.append(f"📜 抽到了 R 式神:{name}")
|
||||
if image_url and os.path.exists(image_url):
|
||||
msg.append(MessageSegment.image(f"file:///{get_image_path(image_url)}"))
|
||||
return msg
|
||||
|
||||
|
||||
def format_triple_gacha_result(results: List[Tuple[str, str, str]], user_id: str, user_name: str) -> Message:
|
||||
"""
|
||||
格式化三连抽结果消息。
|
||||
|
||||
Args:
|
||||
results: 三连抽结果列表,每个元素为(稀有度, 式神名, 图片路径)
|
||||
user_id: QQ号
|
||||
user_name: 用户昵称
|
||||
|
||||
Returns:
|
||||
Message: 包含三连抽结果的消息对象
|
||||
"""
|
||||
msg = Message()
|
||||
msg.append(f"🎯 {format_user_mention(user_id, user_name)} 的三连抽结果:\n\n")
|
||||
|
||||
for i, (rarity, name, image_path) in enumerate(results, 1):
|
||||
if rarity == "SSR":
|
||||
msg.append(f"🌟 第{i}抽:SSR - {name}\n")
|
||||
elif rarity == "SP":
|
||||
msg.append(f"🌈 第{i}抽:SP - {name}\n")
|
||||
elif rarity == "SR":
|
||||
msg.append(f"⭐ 第{i}抽:SR - {name}\n")
|
||||
else: # R
|
||||
msg.append(f"📜 第{i}抽:R - {name}\n")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_achievement_notify(
|
||||
achievements_data: List[Dict[str, Any]],
|
||||
user_id: str,
|
||||
) -> Message:
|
||||
"""
|
||||
格式化成就解锁通知消息。
|
||||
|
||||
纯格式化函数,不执行任何业务逻辑(奖励发放等)。
|
||||
调用方负责解析成就数据和处理奖励。
|
||||
|
||||
Args:
|
||||
achievements_data: 已解析的成就数据列表,每项含 name/reward/claimed/reward_msg 等字段
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Message: 格式化的成就通知消息
|
||||
|
||||
Note:
|
||||
纯函数,无副作用,无外部调用。
|
||||
"""
|
||||
if not achievements_data:
|
||||
return Message()
|
||||
msg = Message()
|
||||
|
||||
if achievements_data:
|
||||
msg.append("\n\n🏆 恭喜解锁新成就!\n")
|
||||
|
||||
for ach in achievements_data:
|
||||
name = ach.get("name", "未知成就")
|
||||
reward = ach.get("reward", 0)
|
||||
reward_msg = ach.get("reward_msg", "")
|
||||
claimed = ach.get("claimed", False)
|
||||
|
||||
if claimed and reward_msg:
|
||||
msg.append(f"🎖️ {name} 重复奖励 (奖励:{reward}) {reward_msg}\n")
|
||||
else:
|
||||
msg.append(f"🎖️ {name}\n")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_achievement_progress(
|
||||
consecutive_days: int,
|
||||
no_ssr_streak: int,
|
||||
user_id: str
|
||||
) -> Message:
|
||||
"""
|
||||
格式化成就进度消息。
|
||||
|
||||
Args:
|
||||
consecutive_days: 连续抽卡天数
|
||||
no_ssr_streak: 连续未出SSR/SP次数
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Message: 包含成就进度的消息对象
|
||||
"""
|
||||
from .gacha import get_achievement_definition
|
||||
|
||||
msg = Message()
|
||||
msg.append(f"🎯 成就进度:\n")
|
||||
|
||||
# 勤勤恳恳成就进度
|
||||
achievement = get_achievement_definition("勤勤恳恳Ⅰ")
|
||||
if achievement:
|
||||
if consecutive_days < 30:
|
||||
msg.append(f"📅 勤勤恳恳Ⅰ:{consecutive_days}/30天 🎯\n")
|
||||
elif consecutive_days < 60:
|
||||
msg.append(f"📅 勤勤恳恳Ⅱ:{consecutive_days}/60天 🎯\n")
|
||||
elif consecutive_days < 90:
|
||||
msg.append(f"📅 勤勤恳恳Ⅲ:{consecutive_days}/90天 🎯\n")
|
||||
elif consecutive_days < 120:
|
||||
msg.append(f"📅 勤勤恳恳Ⅳ:{consecutive_days}/120天 ⭐\n")
|
||||
elif consecutive_days < 150:
|
||||
msg.append(f"📅 勤勤恳恳Ⅴ:{consecutive_days}/150天 ⭐\n")
|
||||
else:
|
||||
# 计算下次奖励周期
|
||||
next_reward = 150 + ((consecutive_days - 150) // 30 + 1) * 30
|
||||
if next_reward <= 365:
|
||||
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,距离下次奖励{next_reward}天 🎯\n")
|
||||
else:
|
||||
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,可获得奖励!🎉\n")
|
||||
|
||||
# 非酋成就进度
|
||||
if no_ssr_streak > 0:
|
||||
if no_ssr_streak < 60:
|
||||
msg.append(f"💔 非酋进度:{no_ssr_streak}/60次 😭\n")
|
||||
elif no_ssr_streak < 120:
|
||||
msg.append(f"💔 顶级非酋:{no_ssr_streak}/120次 😱\n")
|
||||
elif no_ssr_streak < 180:
|
||||
msg.append(f"💔 月见黑:{no_ssr_streak}/180次 🌙\n")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_user_stats(stats: Dict[str, Any], user_id: str, user_name: str) -> Message:
|
||||
"""
|
||||
格式化用户抽卡统计消息。
|
||||
|
||||
Args:
|
||||
stats: 用户统计数据字典
|
||||
user_id: 用户ID
|
||||
user_name: 用户昵称
|
||||
|
||||
Returns:
|
||||
Message: 包含统计信息的消息对象
|
||||
"""
|
||||
msg = Message()
|
||||
msg.append(f"📊 {format_user_mention(user_id, user_name)} 的抽卡统计:\n")
|
||||
msg.append(f"🎲 总抽卡次数:{stats['total_draws']}次\n")
|
||||
|
||||
total = stats['total_draws']
|
||||
if total > 0:
|
||||
msg.append(f"\n稀有度分布:\n")
|
||||
msg.append(f"📜 R:{stats.get('R_count',0)}张 ({stats.get('R_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"⭐ SR:{stats.get('SR_count',0)}张 ({stats.get('SR_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"✨ SSR:{stats.get('SSR_count',0)}张 ({stats.get('SSR_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"🌈 SP:{stats.get('SP_count',0)}张 ({stats.get('SP_count',0)/total*100:.1f}%)")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_user_detail_stats(
|
||||
stats: Dict[str, Any],
|
||||
user_id: str,
|
||||
user_name: str,
|
||||
recent_draws: List[Dict[str, Any]]
|
||||
) -> Message:
|
||||
"""
|
||||
格式化用户详细抽卡统计消息。
|
||||
|
||||
Args:
|
||||
stats: 用户统计数据字典
|
||||
user_id: 用户ID
|
||||
user_name: 用户昵称
|
||||
recent_draws: 最近抽卡记录列表
|
||||
|
||||
Returns:
|
||||
Message: 包含详细统计信息的消息对象
|
||||
"""
|
||||
msg = Message()
|
||||
msg.append(f"{format_user_mention(user_id, user_name)} 的抽卡统计:\n")
|
||||
msg.append(f"总抽卡次数:{stats['total_draws']}次\n")
|
||||
|
||||
total = stats['total_draws']
|
||||
if total > 0:
|
||||
msg.append(f"R:{stats.get('R_count',0)}张 ({stats.get('R_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"SR:{stats.get('SR_count',0)}张 ({stats.get('SR_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"SSR:{stats.get('SSR_count',0)}张 ({stats.get('SSR_count',0)/total*100:.1f}%)\n")
|
||||
msg.append(f"SP:{stats.get('SP_count',0)}张 ({stats.get('SP_count',0)/total*100:.1f}%)")
|
||||
|
||||
if recent_draws:
|
||||
msg.append(f"\n最近{len(recent_draws)}次抽卡:\n")
|
||||
for draw in recent_draws:
|
||||
msg.append(f"• {draw}\n")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_rank_list(rank_data: List[Dict[str, Any]], page: int, total_pages: int) -> Message:
|
||||
"""
|
||||
格式化抽卡排行榜消息。
|
||||
|
||||
Args:
|
||||
rank_data: 排行榜数据列表
|
||||
page: 当前页码
|
||||
total_pages: 总页数
|
||||
|
||||
Returns:
|
||||
Message: 包含排行榜的消息对象
|
||||
"""
|
||||
msg = Message()
|
||||
msg.append(f"🏆 抽卡排行榜 (第{page}页/共{total_pages}页)\n\n")
|
||||
|
||||
for idx, entry in enumerate(rank_data, 1):
|
||||
msg.append(f"{idx}. {entry.get('user_name', '未知')} - {entry.get('total_draws', 0)}次\n")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_daily_stats(daily_stats: Dict[str, Any]) -> Message:
|
||||
"""
|
||||
格式化今日抽卡统计消息。
|
||||
|
||||
Args:
|
||||
daily_stats: 今日统计数据字典
|
||||
|
||||
Returns:
|
||||
Message: 包含今日统计的消息对象
|
||||
"""
|
||||
msg = Message()
|
||||
msg.append(f"📅 今日抽卡统计\n")
|
||||
msg.append(f"总抽卡次数:{daily_stats.get('today_total',0)}次\n")
|
||||
|
||||
msg.append(f"\n稀有度分布:\n")
|
||||
msg.append(f"R:{daily_stats.get('R_count',0)}张\n")
|
||||
msg.append(f"SR:{daily_stats.get('SR_count',0)}张\n")
|
||||
msg.append(f"SSR:{daily_stats.get('SSR_count',0)}张\n")
|
||||
msg.append(f"SP:{daily_stats.get('SP_count',0)}张\n")
|
||||
|
||||
top = daily_stats.get("top_users", [])
|
||||
if top:
|
||||
msg.append("\n今日TOP5:\n")
|
||||
for idx, u in enumerate(top[:5], 1):
|
||||
msg.append(f"{idx}. {u.get('user_name','未知')} - {u.get('draws',0)}次\n")
|
||||
|
||||
return msg
|
||||
@@ -1,5 +1,15 @@
|
||||
"""
|
||||
阴阳师抽卡插件 - 抽卡核心逻辑模块
|
||||
|
||||
实现抽卡核心算法,包括:
|
||||
- 多稀有度抽卡(R/SR/SSR/SP)
|
||||
- 子池支持
|
||||
- 保底机制
|
||||
- 成就检查
|
||||
"""
|
||||
|
||||
import random
|
||||
from typing import Dict, Tuple, List, Optional
|
||||
from typing import Dict, Tuple, List, Optional, Any
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -10,10 +20,11 @@ config = Config()
|
||||
data_manager = DataManager()
|
||||
|
||||
class GachaSystem:
|
||||
"""抽卡系统核心类,管理抽卡逻辑和数据"""
|
||||
def __init__(self):
|
||||
self.data_manager = data_manager
|
||||
|
||||
def draw(self, user_id: str) -> Dict:
|
||||
def draw(self, user_id: str) -> Dict[str, Any]:
|
||||
"""执行一次抽卡"""
|
||||
# 检查抽卡限制
|
||||
if not self.data_manager.check_daily_limit(user_id):
|
||||
|
||||
25
danding_bot/plugins/onmyoji_gacha/handlers/__init__.py
Normal file
25
danding_bot/plugins/onmyoji_gacha/handlers/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
handlers包 - 抽卡命令处理函数
|
||||
|
||||
将各handler函数集中在此包中,便于__init__.py统一导入和注册matcher。
|
||||
"""
|
||||
|
||||
from .gacha import handle_gacha
|
||||
from .triple_gacha import handle_triple_gacha
|
||||
from .stats import handle_stats
|
||||
from .query import handle_query
|
||||
from .rank import handle_rank
|
||||
from .daily_stats import handle_daily_stats
|
||||
from .achievement import handle_achievement
|
||||
from .intro import handle_intro
|
||||
|
||||
__all__ = [
|
||||
"handle_gacha",
|
||||
"handle_triple_gacha",
|
||||
"handle_stats",
|
||||
"handle_query",
|
||||
"handle_rank",
|
||||
"handle_daily_stats",
|
||||
"handle_achievement",
|
||||
"handle_intro",
|
||||
]
|
||||
38
danding_bot/plugins/onmyoji_gacha/handlers/achievement.py
Normal file
38
danding_bot/plugins/onmyoji_gacha/handlers/achievement.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
成就系统查询处理模块
|
||||
|
||||
处理成就系统查询命令,显示已解锁成就和进度。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from ..utils import format_user_mention
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_achievement(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理成就系统查询命令"""
|
||||
user_id = str(event.user_id)
|
||||
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||
|
||||
gacha_system = get_gacha_system()
|
||||
achievements = await gacha_system.get_user_achievements(user_id)
|
||||
|
||||
if not achievements:
|
||||
await event.finish("您还没有解锁任何成就哦~ 继续抽卡吧!")
|
||||
|
||||
msg = f"🏅 {format_user_mention(user_id, user_name)} 的成就:\n\n"
|
||||
|
||||
for ach in achievements:
|
||||
name = ach.get("name", "未知成就")
|
||||
desc = ach.get("description", "")
|
||||
reward = ach.get("reward", 0)
|
||||
claimed = ach.get("claimed", False)
|
||||
|
||||
status = "✅已领取" if claimed else "🎁可领取"
|
||||
msg += f"🎖️ {name}\n {desc}\n 奖励:{reward} {status}\n\n"
|
||||
|
||||
await event.send(msg.strip())
|
||||
25
danding_bot/plugins/onmyoji_gacha/handlers/daily_stats.py
Normal file
25
danding_bot/plugins/onmyoji_gacha/handlers/daily_stats.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
今日抽卡统计处理模块
|
||||
|
||||
处理今日抽卡统计查询命令。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_daily_stats(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理今日抽卡统计命令"""
|
||||
gacha_system = get_gacha_system()
|
||||
daily_stats = await gacha_system.get_daily_stats()
|
||||
|
||||
if not daily_stats or daily_stats.get("today_total", 0) == 0:
|
||||
await event.finish("今日暂无抽卡记录")
|
||||
|
||||
msg = formatters.format_daily_stats(daily_stats)
|
||||
await event.send(msg)
|
||||
62
danding_bot/plugins/onmyoji_gacha/handlers/gacha.py
Normal file
62
danding_bot/plugins/onmyoji_gacha/handlers/gacha.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
抽卡命令处理模块
|
||||
|
||||
处理单次抽卡命令,包括:
|
||||
- 参数解析(子池选择)
|
||||
- 抽卡执行
|
||||
- SSR/SP奖励处理
|
||||
- 成就检查
|
||||
- 消息发送
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
from nonebot.params import CommandArg
|
||||
from nonebot.adapters.onebot.v11 import Message
|
||||
import nonebot
|
||||
import random
|
||||
|
||||
from ..config import Config
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
from ..api_utils import process_ssr_sp_reward
|
||||
from ..utils import format_user_mention, build_achievement_notify, get_user_name
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_gacha(bot: Bot, event: MessageEvent, state: dict, args: Message = CommandArg()):
|
||||
"""处理抽卡命令"""
|
||||
user_id = str(event.user_id)
|
||||
user_name = get_user_name(event)
|
||||
|
||||
# 解析子池参数
|
||||
sub_pool = args.extract_plain_text().strip()
|
||||
|
||||
# 执行抽卡
|
||||
gacha_system = get_gacha_system()
|
||||
result = await gacha_system.draw(user_id, sub_pool=sub_pool if sub_pool else None)
|
||||
|
||||
if not result["success"]:
|
||||
await event.finish(result["message"])
|
||||
|
||||
rarity, shikigami, image_url = result["rarity"], result["name"], result["image"]
|
||||
|
||||
# 发送抽卡结果
|
||||
msg = formatters.format_gacha_result(rarity, shikigami, user_id, user_name, image_url)
|
||||
await event.send(msg)
|
||||
|
||||
# SSR/SP奖励处理
|
||||
if rarity in ["SSR", "SP"]:
|
||||
group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else None
|
||||
reward_msg = await process_ssr_sp_reward(user_id, user_name, rarity, shikigami, group_id)
|
||||
if reward_msg:
|
||||
await event.send(reward_msg)
|
||||
|
||||
# 成就检查(使用统一编排函数)
|
||||
unlocked = await gacha_system.check_achievements(user_id)
|
||||
if unlocked:
|
||||
achievement_msg = await build_achievement_notify(user_id, unlocked)
|
||||
if achievement_msg:
|
||||
await event.send(achievement_msg)
|
||||
|
||||
39
danding_bot/plugins/onmyoji_gacha/handlers/intro.py
Normal file
39
danding_bot/plugins/onmyoji_gacha/handlers/intro.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
帮助介绍处理模块
|
||||
|
||||
显示抽卡系统的帮助信息和功能说明。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_intro(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理帮助介绍命令"""
|
||||
intro_text = """
|
||||
🎴 阴阳师抽卡系统 使用说明
|
||||
|
||||
📌 基础命令:
|
||||
• 抽卡 [子池名] - 进行一次抽卡
|
||||
• 三连抽 - 连续抽三次
|
||||
• 我的抽卡 - 查看个人抽卡统计
|
||||
• 抽卡详情 - 查看详细统计和最近记录
|
||||
• 抽卡排行 [页码] - 查看排行榜
|
||||
• 今日抽卡 - 查看今日抽卡统计
|
||||
• 成就 - 查看成就系统
|
||||
• 介绍 - 显示本帮助信息
|
||||
|
||||
📊 功能特色:
|
||||
• 多稀有度式神:R/SR/SSR/SP
|
||||
• 成就系统:连续抽卡、非酋成就等
|
||||
• SSR/SP奖励:自动发放积分奖励
|
||||
• 每日签到:首次抽卡自动签到
|
||||
|
||||
💡 提示:
|
||||
• 每日抽卡次数有限制
|
||||
• SSR/SP抽中会通知管理员
|
||||
• 成就奖励自动发放或联系管理员领取
|
||||
"""
|
||||
await event.reply(intro_text.strip())
|
||||
42
danding_bot/plugins/onmyoji_gacha/handlers/query.py
Normal file
42
danding_bot/plugins/onmyoji_gacha/handlers/query.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
抽卡详情查询处理模块
|
||||
|
||||
处理用户抽卡详情查询,包括最近抽卡记录和成就进度。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_query(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理抽卡详情查询命令"""
|
||||
user_id = str(event.user_id)
|
||||
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||
|
||||
gacha_system = get_gacha_system()
|
||||
stats = await gacha_system.get_user_stats(user_id)
|
||||
|
||||
if not stats or stats.get("total_draws", 0) == 0:
|
||||
await event.finish("您还没有抽卡记录哦~")
|
||||
|
||||
# 获取最近抽卡记录
|
||||
recent = await gacha_system.get_recent_draws(user_id, limit=5)
|
||||
|
||||
# 发送统计详情
|
||||
msg = formatters.format_user_detail_stats(stats, user_id, user_name, recent)
|
||||
await event.send(msg)
|
||||
|
||||
# 发送成就进度
|
||||
progress = await gacha_system.get_achievement_progress(user_id)
|
||||
if progress:
|
||||
achievement_msg = formatters.format_achievement_progress(
|
||||
progress.get("consecutive_days", 0),
|
||||
progress.get("no_ssr_streak", 0),
|
||||
user_id
|
||||
)
|
||||
await event.send(achievement_msg)
|
||||
33
danding_bot/plugins/onmyoji_gacha/handlers/rank.py
Normal file
33
danding_bot/plugins/onmyoji_gacha/handlers/rank.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
抽卡排行榜处理模块
|
||||
|
||||
处理抽卡排行榜查询命令,支持分页显示。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
from nonebot.params import CommandArg
|
||||
from nonebot.adapters.onebot.v11 import Message
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_rank(bot: Bot, event: MessageEvent, state: dict, args: Message = CommandArg()):
|
||||
"""处理抽卡排行榜命令"""
|
||||
# 解析页码
|
||||
page_text = args.extract_plain_text().strip()
|
||||
page = 1
|
||||
if page_text.isdigit():
|
||||
page = int(page_text)
|
||||
|
||||
gacha_system = get_gacha_system()
|
||||
rank_data, total_pages = await gacha_system.get_rank_list(page=page)
|
||||
|
||||
if not rank_data:
|
||||
await event.finish("暂无排行数据")
|
||||
|
||||
msg = formatters.format_rank_list(rank_data, page, total_pages)
|
||||
await event.send(msg)
|
||||
28
danding_bot/plugins/onmyoji_gacha/handlers/stats.py
Normal file
28
danding_bot/plugins/onmyoji_gacha/handlers/stats.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
我的抽卡统计处理模块
|
||||
|
||||
处理用户的个人抽卡统计查询命令。
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_stats(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理我的抽卡统计命令"""
|
||||
user_id = str(event.user_id)
|
||||
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||
|
||||
gacha_system = get_gacha_system()
|
||||
stats = await gacha_system.get_user_stats(user_id)
|
||||
|
||||
if not stats or stats.get("total_draws", 0) == 0:
|
||||
await event.finish("您还没有抽卡记录哦~")
|
||||
|
||||
msg = formatters.format_user_stats(stats, user_id, user_name)
|
||||
await event.send(msg)
|
||||
45
danding_bot/plugins/onmyoji_gacha/handlers/triple_gacha.py
Normal file
45
danding_bot/plugins/onmyoji_gacha/handlers/triple_gacha.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
三连抽命令处理模块
|
||||
|
||||
处理三连抽命令,包括:
|
||||
- 三次抽卡执行
|
||||
- 结果汇总
|
||||
- 成就检查
|
||||
"""
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent
|
||||
import nonebot
|
||||
|
||||
from ..utils import get_gacha_system
|
||||
from .. import formatters
|
||||
from ..utils import build_achievement_notify
|
||||
|
||||
logger = nonebot.logger
|
||||
|
||||
|
||||
async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: dict):
|
||||
"""处理三连抽命令"""
|
||||
user_id = str(event.user_id)
|
||||
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||
|
||||
gacha_system = get_gacha_system()
|
||||
results = []
|
||||
|
||||
# 执行三次抽卡
|
||||
for _ in range(3):
|
||||
result = await gacha_system.draw(user_id)
|
||||
if result["success"]:
|
||||
results.append((result["rarity"], result["name"], result["image"]))
|
||||
else:
|
||||
await event.finish(result["message"])
|
||||
|
||||
# 发送三连抽结果
|
||||
msg = formatters.format_triple_gacha_result(results, user_id, user_name)
|
||||
await event.send(msg)
|
||||
|
||||
# 成就检查(使用统一编排函数,避免接口不匹配)
|
||||
unlocked = await gacha_system.check_achievements(user_id)
|
||||
if unlocked:
|
||||
achievement_msg = await build_achievement_notify(user_id, unlocked)
|
||||
if achievement_msg:
|
||||
await event.send(achievement_msg)
|
||||
@@ -0,0 +1,57 @@
|
||||
# 变更提案: onmyoji_gacha 代码评审修复
|
||||
|
||||
- 日期: 2026-05-03
|
||||
- 状态: ✅ 已实施
|
||||
- 作者: Agent (代码评审驱动)
|
||||
|
||||
## 背景
|
||||
|
||||
对 onmyoji_gacha 插件进行系统代码评审,发现 14 个问题(1P0 + 6P1 + 9P2),
|
||||
涉及安全漏洞、依赖方向、职责边界、一致性等维度。
|
||||
|
||||
## 变更清单
|
||||
|
||||
### P0 - 紧急修复
|
||||
|
||||
| # | 文件 | 问题 | 修复 |
|
||||
|---|------|------|------|
|
||||
| 1 | web_api.py | 5处async函数缺await(数据库查询结果为协程对象,数据全部错乱) | 补全所有await |
|
||||
|
||||
### P1 - 重要修复
|
||||
|
||||
| # | 文件 | 问题 | 修复 |
|
||||
|---|------|------|------|
|
||||
| 2 | web_api.py | verify_admin_token中print泄露token明文到日志 | 删除token print |
|
||||
| 3 | formatters.py → api_utils | format_achievement_notify反向依赖api_utils(同层模块循环依赖) | 解耦:formatters改为纯格式化,reward逻辑移至handler调用方 |
|
||||
| 4 | handlers/gacha.py → __init__.py | 签到逻辑嵌入抽卡handler(跨职责),传None matcher | 移至__init__.py matcher层,传入实际matcher |
|
||||
| 5 | formatters.py | 5处重复硬编码SSR/SP/...字符串字面量 | 提取RARITY_DISPLAY配置字典,消除重复 |
|
||||
| 6 | data_manager.py | 承担数据IO+缓存+业务规则三重职责 | 暂不拆分(影响面大),docstring标记TODO |
|
||||
|
||||
### P2 - 改进
|
||||
|
||||
| # | 文件 | 问题 | 修复 |
|
||||
|---|------|------|------|
|
||||
| 7 | utils.py | user_name获取逻辑散布在多个handler中 | 新增get_user_name统一工具函数 |
|
||||
| 8 | web_api.py | GachaSystem/Config模块级实例化(import时副作用) | 改为lazy延迟初始化 |
|
||||
| 9 | __init__.py | matcher参数传None | 传入实际matcher对象 |
|
||||
|
||||
## 受影响文件
|
||||
|
||||
- `web_api.py` - 重写(P0+P1+P2共8处修复)
|
||||
- `formatters.py` - 解耦api_utils + RARITY_DISPLAY提取
|
||||
- `handlers/gacha.py` - 移除签到逻辑
|
||||
- `__init__.py` - gacha wrapper补充签到编排
|
||||
- `utils.py` - 新增get_user_name
|
||||
- `data_manager.py` - TODO标记
|
||||
|
||||
## 验证
|
||||
|
||||
- [x] 所有修改文件语法检查通过 (ast.parse)
|
||||
- [x] 依赖方向:formatters不再import api_utils
|
||||
- [x] token明文不再出现在日志
|
||||
- [x] 签到逻辑在matcher层正确调用
|
||||
|
||||
## Delta 规约
|
||||
|
||||
本次变更未引入新的外部依赖,未改变数据库结构,未改变用户可见接口。
|
||||
API响应格式不变,命令触发方式不变。
|
||||
46
danding_bot/plugins/onmyoji_gacha/rules.py
Normal file
46
danding_bot/plugins/onmyoji_gacha/rules.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
权限校验与规则解析模块
|
||||
|
||||
提供NoneBot命令的权限检查规则函数,包括:
|
||||
- 群组权限检查(通用)
|
||||
|
||||
所有规则函数返回Rule对象,用于NoneBot的matcher定义。
|
||||
"""
|
||||
|
||||
from typing import Callable
|
||||
from nonebot.rule import Rule
|
||||
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
|
||||
|
||||
|
||||
def _check_group_allowed(config) -> Callable:
|
||||
"""生成群组权限检查函数(内部复用,消除重复逻辑)。
|
||||
|
||||
Args:
|
||||
config: Config实例,需包含ALLOWED_GROUP_ID属性
|
||||
|
||||
Returns:
|
||||
异步检查函数,私聊放行、群聊检查白名单
|
||||
"""
|
||||
async def _check(bot: Bot, event: MessageEvent) -> bool:
|
||||
if not isinstance(event, GroupMessageEvent):
|
||||
return True
|
||||
# 单群模式:直接比较整数
|
||||
return event.group_id == config.ALLOWED_GROUP_ID
|
||||
return _check
|
||||
|
||||
|
||||
def check_permission() -> Rule:
|
||||
"""检查群组是否有权限使用抽卡功能。"""
|
||||
from .config import Config
|
||||
config = Config()
|
||||
return Rule(_check_group_allowed(config))
|
||||
|
||||
|
||||
def check_rank_permission() -> Rule:
|
||||
"""检查用户是否有权限查看排行榜。
|
||||
|
||||
当前与check_permission逻辑相同,保留为独立入口便于未来扩展。
|
||||
"""
|
||||
from .config import Config
|
||||
config = Config()
|
||||
return Rule(_check_group_allowed(config))
|
||||
@@ -1,3 +1,11 @@
|
||||
"""
|
||||
阴阳师抽卡插件 - 通用工具函数
|
||||
|
||||
提供常用的辅助函数:
|
||||
- 用户提及格式化
|
||||
- 图片路径处理
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
@@ -40,3 +48,61 @@ def format_sign_in_message(
|
||||
f"{luck_emoji} 今日运气:{luck_text}\n"
|
||||
f"💰 当前积分:{balance}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
def get_user_name(event) -> str:
|
||||
"""从消息事件中获取用户昵称,统一多处重复逻辑。
|
||||
|
||||
Args:
|
||||
event: NoneBot MessageEvent 对象
|
||||
|
||||
Returns:
|
||||
str: 用户昵称,优先使用群名片(card),其次昵称(nickname),兜底"未知用户"
|
||||
"""
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent
|
||||
if isinstance(event, GroupMessageEvent):
|
||||
return event.sender.card or event.sender.nickname or "未知用户"
|
||||
return event.sender.nickname or "未知用户"
|
||||
|
||||
|
||||
async def build_achievement_notify(user_id: str, unlocked_ids: list) -> "Message | None":
|
||||
"""统一的成就通知编排:ID列表 → 详情查询 → 奖励领取 → 消息格式化。
|
||||
|
||||
供所有handler共用,消除成就通知逻辑的重复(原则#4 变化半径小 / #12 功能越多代码越短)。
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
unlocked_ids: 新解锁的成就ID列表
|
||||
|
||||
Returns:
|
||||
Message对象,无有效成就时返回None
|
||||
"""
|
||||
from .api_utils import process_achievement_reward, get_achievement_by_id
|
||||
from . import formatters
|
||||
|
||||
achievements_data = []
|
||||
for achievement_id in unlocked_ids:
|
||||
ach = get_achievement_by_id(achievement_id)
|
||||
if not ach:
|
||||
continue
|
||||
success, reward_msg = await process_achievement_reward(user_id, achievement_id)
|
||||
ach["reward_msg"] = reward_msg if success else ""
|
||||
ach["claimed"] = success
|
||||
achievements_data.append(ach)
|
||||
|
||||
if not achievements_data:
|
||||
return None
|
||||
return formatters.format_achievement_notify(achievements_data, user_id)
|
||||
|
||||
|
||||
# ---- GachaSystem 单例(P1#3: 避免每次handler调用都new实例) ----
|
||||
_gacha_system_instance = None
|
||||
|
||||
def get_gacha_system():
|
||||
"""获取全局唯一的GachaSystem实例(lazy init)。"""
|
||||
global _gacha_system_instance
|
||||
if _gacha_system_instance is None:
|
||||
from .gacha import GachaSystem
|
||||
_gacha_system_instance = GachaSystem()
|
||||
return _gacha_system_instance
|
||||
|
||||
Reference in New Issue
Block a user