merge: resolve onmyoji_gacha conflicts

This commit is contained in:
2026-05-11 22:32:13 +08:00
19 changed files with 1941 additions and 1091 deletions

View File

@@ -1,146 +1,159 @@
# 蛋定助手插件文档
## 项目概述
蛋定助手是一个基于 NoneBot2 框架开发的 QQ 机器人,提供多种功能插件,包括 AI 聊天、游戏辅助、积分系统、社群管理等。
## 插件总览
| 插件名称 | 描述 | 触发方式 | 权限要求 |
|---------|------|---------|---------|
| [chatai](#1-chatai---ai-聊天) | AI 聊天DeepSeek支持图文回复 | `*` 开头消息 | 所有用户 |
| [auto_recall](#2-auto_recall---自动撤回) | 自动撤回机器人发送的消息 | 自动执行 | 系统自动 |
| [auto_friend_accept](#3-auto_friend_accept---自动接受好友) | 自动同意好友请求并发送欢迎语 | 自动执行 | 系统自动 |
| [welcome_plugin](#4-welcome_plugin---入群欢迎) | 新成员入群欢迎并发送帮助菜单 | 自动执行 | 特定群 (621016172) |
| [danding_qqpush](#5-danding_qqpush---消息推送) | 通过 HTTP API 向指定群推送图文通知 | HTTP POST | 接口 Token 验证 |
| [danding_points](#6-danding_points---积分系统核心) | 积分系统数据库与 API 核心 | API 调用 | 系统内部 |
| [danding_points_query](#7-danding_points_query---积分查询) | 查询余额、排行及交易记录 | 命令触发 | 所有用户 |
| [group_horse_racing](#8-group_horse_racing---群赛马游戏) | 多人赛马游戏,支持下注与积分奖惩 | 命令触发 | 允许的群聊 |
| [onmyoji_gacha](#9-onmyoji_gacha---阴阳师抽卡模拟) | 阴阳师主题抽卡,包含成就与卡密奖励 | 命令触发 | 允许的群聊/用户 |
| [damo_balance](#10-damo_balance---大漠账户查询) | 查询大漠平台账户余额 | 命令触发 | 特定用户 |
| [danding_api](#11-danding_api---管理-api) | 管理员操作:卡密管理、加时、在线查询 | 命令触发 | 超级用户 |
| [danding_help](#12-danding_help---帮助菜单) | 提供教程、下载及功能指引 | 命令触发 | 特定群 (621016172) |
| [command_list](#13-command_list---指令列表) | 获取系统支持的所有指令列表 | 命令触发 | 所有用户 |
---
## 详细插件文档
### 1. chatai - AI 聊天
基于 DeepSeek AI 的聊天功能,支持将回复转换为图片形式,并在一定时间后自动撤回。
- **使用方法**: 发送以 `*` 开头的消息。
- **配置项**: `DEEPSEEK_TOKEN` (必填)。
- **特性**: AI 回复会自动转为图片显示,默认 120 秒后撤回。
### 2. auto_recall - 自动撤回
监控机器人发出的消息,并在指定时间后自动撤回,保持聊天环境整洁。
- **配置项**:
- `RECALL_DELAY`: 普通消息撤回延迟(默认 110s
- `QQPUSH_RECALL_DELAY`: 推送消息撤回延迟(默认 3600s
### 3. auto_friend_accept - 自动接受好友
自动处理好友请求,提升用户接入效率。
- **配置项**:
- `auto_accept_enabled`: 是否开启自动接受。
- `auto_reply_message`: 接受后的欢迎语。
### 4. welcome_plugin - 入群欢迎
针对特定群聊的新成员欢迎功能。
- **触发场景**: 新成员加入群 `621016172`
- **功能**: 随机发送欢迎语,并附带帮助菜单图片。
### 5. danding_qqpush - 消息推送
提供外部系统向 QQ 推送通知的 HTTP 接口。
- **接口**: `POST /danding/qqpush/{token}`
- **功能**: 自动将长文本转换为图片,支持 `@用户` 和换行符 `#`
- **配置**: `DANDING_QQPUSH_TOKEN`
### 6. danding_points - 积分系统核心
为其他插件提供积分存储与结算的基础设施。
- **功能**: 数据库管理SQLite、余额增减、排行榜计算、交易日志记录。
- **数据库路径**: `data/danding_points/points.db`
### 7. danding_points_query - 积分查询
用户通过命令与积分系统交互。
- **主要命令**:
- `我的积分`: 查看个人余额。
- `积分查询 @用户`: 查看他人余额。
- `积分排行`: 查看前 10 名。
- `积分历史查询`: 查看最近 5 条变动记录。
- `积分帮助`: 获取指令指引。
### 8. group_horse_racing - 群赛马游戏
集成积分系统的多人互动游戏。
- **主要命令**:
- `/赛马报名 [马名]`: 参加比赛。
- `/赛马下注 <序号> <金额>`: 对马匹下注。
- `/赛马开赛`: 开始比赛(至少 2 人)。
- **积分逻辑**: 参赛奖励 50 分,冠军奖励 200 分,支持下注赔率结算。
### 9. onmyoji_gacha - 阴阳师抽卡模拟
高度还原的抽卡模拟,包含成就系统。
- **主要命令**:
- `抽卡`: 执行单抽。
- `三连抽`: 执行三连抽。
- `我的抽卡`: 查看个人统计。
- `查询成就`: 查看已解锁成就及进度(非酋、勤勤恳恳系列)。
- `抽卡介绍`: 查看详细机制与奖励说明。
- **特性**: 抽中 SSR/SP 可获得“蛋定助手”卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。
### 10. damo_balance - 大漠账户查询
查询大漠平台账户余额。
- **命令**: `大漠余额``余额查询`
- **限制**: 仅特定用户可用,需输入验证码
### 11. danding_api - 管理 API
供超级用户使用的后台管理功能
- **主要命令**:
- `在线人数`: 查询当前活跃用户
- `生成卡密 <类型>`: 生成天/周/月卡。
- `用户加时 <用户名> <类型>`: 直接为特定用户增加时长。
### 12. danding_help - 帮助菜单
系统的官方指引手册
- **主要命令**: `帮助``下载``正式版如何运行` 等。
- **限制**: 仅在特定群 `621016172` 可用。
### 13. command_list - 指令列表
快速查阅所有可用指令
- **命令**: `指令列表`
---
## 常见问题 (FAQ)
- **Q: 为什么某些命令没反应?**
A: 部分插件(如 `danding_help`)限制了特定群聊使用;管理指令需要配置 `SUPERUSERS`
- **Q: 积分有什么用?**
A: 目前主要用于赛马下注及展示排名。
- **Q: 抽卡奖励如何领取?**
A: 抽中 SSR/SP 或解锁特定成就后,请截屏联系管理员。
# 蛋定助手插件文档
## 项目概述
蛋定助手是一个基于 NoneBot2 框架开发的 QQ 机器人,提供多种功能插件,包括 AI 聊天、游戏辅助、积分系统、社群管理等。
## 插件总览
| 插件名称 | 描述 | 触发方式 | 权限要求 |
|---------|------|---------|---------|
| [chatai](#1-chatai---ai-聊天) | AI 聊天DeepSeek支持图文回复 | `*` 开头消息 | 所有用户 |
| [auto_recall](#2-auto_recall---自动撤回) | 自动撤回机器人发送的消息 | 自动执行 | 系统自动 |
| [auto_friend_accept](#3-auto_friend_accept---自动接受好友) | 自动同意好友请求并发送欢迎语 | 自动执行 | 系统自动 |
| [welcome_plugin](#4-welcome_plugin---入群欢迎) | 新成员入群欢迎并发送帮助菜单 | 自动执行 | 特定群 (621016172) |
| [danding_qqpush](#5-danding_qqpush---消息推送) | 通过 HTTP API 向指定群推送图文通知 | HTTP POST | 接口 Token 验证 |
| [danding_points](#6-danding_points---积分系统核心) | 积分系统数据库与 API 核心 | API 调用 | 系统内部 |
| [danding_points_query](#7-danding_points_query---积分查询) | 查询余额、排行及交易记录 | 命令触发 | 所有用户 |
| [group_horse_racing](#8-group_horse_racing---群赛马游戏) | 多人赛马游戏,支持下注与积分奖惩 | 命令触发 | 允许的群聊 |
| [onmyoji_gacha](#9-onmyoji_gacha---阴阳师抽卡模拟) | 阴阳师主题抽卡,包含成就与卡密奖励 | 命令触发 | 允许的群聊/用户 |
| [damo_balance](#10-damo_balance---大漠账户查询) | 查询大漠平台账户余额 | 命令触发 | 特定用户 |
| [danding_api](#11-danding_api---管理-api) | 管理员操作:卡密管理、加时、在线查询 | 命令触发 | 超级用户 |
| [danding_help](#12-danding_help---帮助菜单) | 提供教程、下载及功能指引 | 命令触发 | 特定群 (621016172) |
| [command_list](#13-command_list---指令列表) | 获取系统支持的所有指令列表 | 命令触发 | 所有用户 |
---
## 详细插件文档
### 1. chatai - AI 聊天
基于 DeepSeek AI 的聊天功能,支持将回复转换为图片形式,并在一定时间后自动撤回。
- **使用方法**: 发送以 `*` 开头的消息。
- **配置项**: `DEEPSEEK_TOKEN` (必填)。
- **特性**: AI 回复会自动转为图片显示,默认 120 秒后撤回。
### 2. auto_recall - 自动撤回
监控机器人发出的消息,并在指定时间后自动撤回,保持聊天环境整洁。
- **配置项**:
- `RECALL_DELAY`: 普通消息撤回延迟(默认 110s
- `QQPUSH_RECALL_DELAY`: 推送消息撤回延迟(默认 3600s
### 3. auto_friend_accept - 自动接受好友
自动处理好友请求,提升用户接入效率。
- **配置项**:
- `auto_accept_enabled`: 是否开启自动接受。
- `auto_reply_message`: 接受后的欢迎语。
### 4. welcome_plugin - 入群欢迎
针对特定群聊的新成员欢迎功能。
- **触发场景**: 新成员加入群 `621016172`
- **功能**: 随机发送欢迎语,并附带帮助菜单图片。
### 5. danding_qqpush - 消息推送
提供外部系统向 QQ 推送通知的 HTTP 接口。
- **接口**: `POST /danding/qqpush/{token}`
- **功能**: 自动将长文本转换为图片,支持 `@用户` 和换行符 `#`
- **配置**: `DANDING_QQPUSH_TOKEN`
### 6. danding_points - 积分系统核心
为其他插件提供积分存储与结算的基础设施。
- **功能**: 数据库管理SQLite、余额增减、排行榜计算、交易日志记录。
- **数据库路径**: `data/danding_points/points.db`
### 7. danding_points_query - 积分查询
用户通过命令与积分系统交互。
- **主要命令**:
- `我的积分`: 查看个人余额。
- `积分查询 @用户`: 查看他人余额。
- `积分排行`: 查看前 10 名。
- `积分历史查询`: 查看最近 5 条变动记录。
- `积分帮助`: 获取指令指引。
### 8. group_horse_racing - 群赛马游戏
集成积分系统的多人互动游戏。
- **主要命令**:
- `/赛马报名 [马名]`: 参加比赛。
- `/赛马下注 <序号> <金额>`: 对马匹下注。
- `/赛马开赛`: 开始比赛(至少 2 人)。
- **积分逻辑**: 参赛奖励 50 分,冠军奖励 200 分,支持下注赔率结算。
### 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 可获得"蛋定助手"卡密奖励(需联系管理员领取)。包含每日抽卡签到积分奖励。成就系统自动发放积分奖励
### 10. damo_balance - 大漠账户查询
查询大漠平台账户余额。
- **命令**: `大漠余额``余额查询`
- **限制**: 仅特定用户可用,需输入验证码
### 11. danding_api - 管理 API
供超级用户使用的后台管理功能。
- **主要命令**:
- `在线人数`: 查询当前活跃用户
- `生成卡密 <类型>`: 生成天/周/月卡。
- `用户加时 <用户名> <类型>`: 直接为特定用户增加时长
### 12. danding_help - 帮助菜单
系统的官方指引手册。
- **主要命令**: `帮助``下载``正式版如何运行` 等。
- **限制**: 仅在特定群 `621016172` 可用
### 13. command_list - 指令列表
快速查阅所有可用指令。
- **命令**: `指令列表`
---
## 常见问题 (FAQ)
- **Q: 为什么某些命令没反应?**
A: 部分插件(如 `danding_help`)限制了特定群聊使用;管理指令需要配置 `SUPERUSERS`
- **Q: 积分有什么用?**
A: 目前主要用于赛马下注及展示排名。
- **Q: 抽卡奖励如何领取?**
A: 抽中 SSR/SP 或解锁特定成就后,请截屏联系管理员。

View File

@@ -249,4 +249,4 @@ async def process_achievement_reward(user_id: str, achievement_id: str) -> Tuple
f"获得奖励:蛋定助手{reward_type}一张\n"
f"⚠️自动加时失败: {message}\n"
f"请联系管理员手动领取奖励!")
return False, msg
return False, msg

View File

@@ -128,4 +128,4 @@ class Config(BaseSettings):
"""运行时警告如果使用默认admin token在生产环境可能被猜解"""
if self.WEB_ADMIN_TOKEN == "onmyoji_admin_token_2024":
logger.warning("⚠️ WEB_ADMIN_TOKEN 使用默认值,生产环境请务必通过环境变量覆盖!")
return self
return self

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -1,307 +1,318 @@
import random
from typing import Dict, Tuple, List, Optional
import os
from pathlib import Path
from .config import Config
from .data_manager import DataManager
config = Config()
data_manager = DataManager()
class GachaSystem:
def __init__(self):
self.data_manager = data_manager
def draw(self, user_id: str) -> Dict:
"""执行一次抽卡"""
# 检查抽卡限制
if not self.data_manager.check_daily_limit(user_id):
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": False,
"message": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!"
}
# 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id)
# 从该稀有度中抽取式神
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
if not shikigami_data:
return {
"success": False,
"message": f"系统错误:{rarity}稀有度下没有可用式神"
}
# 随机选择式神
shikigami = random.choice(shikigami_data)
# 记录抽卡
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
# 剩余次数
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": True,
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"],
"draws_left": draws_left,
"unlocked_achievements": unlocked_achievements
}
def _draw_rarity(self, user_id: str = None) -> str:
"""按概率抽取稀有度"""
# 检查是否是特殊概率用户
if user_id and user_id in config.SPECIAL_PROBABILITY_USERS:
# 100%概率抽到SSR或SP随机选择
return random.choice(["SSR", "SP"])
# 普通用户的概率逻辑
r = random.random() * 100 # 0-100的随机数
cumulative = 0
for rarity, prob in config.RARITY_PROBABILITY.items():
cumulative += prob
if r < cumulative:
return rarity
# 默认返回R理论上不会执行到这里
return "R"
def get_user_stats(self, user_id: str) -> Dict:
"""获取用户抽卡统计"""
user_stats = self.data_manager.get_user_stats()
if user_id not in user_stats:
return {
"success": False,
"message": "您还没有抽卡记录哦!"
}
stats = user_stats[user_id]
return {
"success": True,
"total_draws": stats["total_draws"],
"R_count": stats["R_count"],
"SR_count": stats["SR_count"],
"SSR_count": stats["SSR_count"],
"SP_count": stats["SP_count"],
"recent_draws": stats["draw_history"][-5:] if stats["draw_history"] else []
}
def get_probability_text(self) -> str:
"""获取概率展示文本"""
probs = config.RARITY_PROBABILITY
return f"--- 系统概率 ---\nR: {probs['R']}% | SR: {probs['SR']}% | SSR: {probs['SSR']}% | SP: {probs['SP']}%"
def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]:
"""获取抽卡排行榜数据"""
user_stats = self.data_manager.get_user_stats()
# 过滤有SSR/SP记录的用户
ranked_users = [
(user_id, stats)
for user_id, stats in user_stats.items()
if stats.get("SSR_count", 0) > 0 or stats.get("SP_count", 0) > 0
]
# 按SSR+SP总数降序排序
ranked_users.sort(
key=lambda x: (x[1].get("SSR_count", 0) + x[1].get("SP_count", 0)),
reverse=True
)
return ranked_users
def get_daily_stats(self) -> Dict:
"""获取今日抽卡统计"""
daily_draws = self.data_manager.get_daily_draws()
today = self.data_manager.get_today_date()
if not daily_draws or today not in daily_draws:
return {
"success": False,
"message": "今日还没有人抽卡哦!"
}
today_stats = daily_draws[today]
total_stats = {
"total_users": len(today_stats),
"total_draws": 0,
"R_count": 0,
"SR_count": 0,
"SSR_count": 0,
"SP_count": 0,
"user_stats": []
}
# 统计每个用户的抽卡情况
for user_id, draws in today_stats.items():
user_stats = {
"user_id": user_id,
"total_draws": len(draws),
"R_count": sum(1 for d in draws if d["rarity"] == "R"),
"SR_count": sum(1 for d in draws if d["rarity"] == "SR"),
"SSR_count": sum(1 for d in draws if d["rarity"] == "SSR"),
"SP_count": sum(1 for d in draws if d["rarity"] == "SP")
}
# 更新总统计
total_stats["total_draws"] += user_stats["total_draws"]
total_stats["R_count"] += user_stats["R_count"]
total_stats["SR_count"] += user_stats["SR_count"]
total_stats["SSR_count"] += user_stats["SSR_count"]
total_stats["SP_count"] += user_stats["SP_count"]
# 只记录抽到SSR或SP的用户
if user_stats["SSR_count"] > 0 or user_stats["SP_count"] > 0:
total_stats["user_stats"].append(user_stats)
# 按SSR+SP数量排序用户统计
total_stats["user_stats"].sort(
key=lambda x: (x["SSR_count"] + x["SP_count"]),
reverse=True
)
# 构建稀有度统计
rarity_stats = {
"R": total_stats["R_count"],
"SR": total_stats["SR_count"],
"SSR": total_stats["SSR_count"],
"SP": total_stats["SP_count"]
}
# 构建排行榜数据
top_users = []
for user_stat in total_stats["user_stats"]:
top_users.append({
"user_id": user_stat["user_id"],
"ssr_count": user_stat["SSR_count"] + user_stat["SP_count"]
})
final_stats = {
"total_users": total_stats["total_users"],
"total_draws": total_stats["total_draws"],
"rarity_stats": rarity_stats,
"top_users": top_users
}
return {
"success": True,
"date": today,
"stats": final_stats
}
def triple_draw(self, user_id: str) -> Dict:
"""执行三连抽"""
# 检查是否有足够的抽卡次数
draws_left = self.data_manager.get_draws_left(user_id)
if draws_left < 3:
return {
"success": False,
"message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会三连抽需要3次机会"
}
results = []
all_unlocked_achievements = []
# 执行三次抽卡
for i in range(3):
# 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id)
# 从该稀有度中抽取式神
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
if not shikigami_data:
return {
"success": False,
"message": f"系统错误:{rarity}稀有度下没有可用式神"
}
# 随机选择式神
shikigami = random.choice(shikigami_data)
# 记录抽卡
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
all_unlocked_achievements.extend(unlocked_achievements)
results.append({
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"]
})
# 剩余次数
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": True,
"results": results,
"draws_left": draws_left,
"unlocked_achievements": list(set(all_unlocked_achievements)) # 去重
}
def get_user_achievements(self, user_id: str) -> Dict:
"""获取用户成就信息"""
achievement_data = self.data_manager.get_user_achievements(user_id)
if not achievement_data["unlocked"] and all(v == 0 for v in achievement_data["progress"].values()):
return {
"success": False,
"message": "您还没有任何成就进度哦!快去抽卡吧!"
}
return {
"success": True,
"achievements": achievement_data["unlocked"],
"progress": achievement_data["progress"]
}
def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict:
"""获取每日详细抽卡记录"""
if not date:
date = self.data_manager.get_today_date()
daily_draws = self.data_manager.get_daily_draws()
if not daily_draws or date not in daily_draws:
return {
"success": False,
"message": f"{date} 没有抽卡记录"
}
records = []
for user_id, draws in daily_draws[date].items():
for draw in draws:
# 检查这次抽卡是否解锁了成就
unlocked_achievements = []
draw_time = draw.get("timestamp", "未知时间")
# 获取用户成就信息
achievement_data = self.data_manager.get_user_achievements(user_id)
if achievement_data["unlocked"]:
# 检查是否有在抽卡时间之后解锁的成就
for achievement_id, achievement_info in achievement_data["unlocked"].items():
if achievement_info["unlocked_date"] == f"{date} {draw_time}":
unlocked_achievements.append(achievement_id)
records.append({
"user_id": user_id,
"draw_time": draw_time,
"shikigami_name": draw["name"],
"rarity": draw["rarity"],
"unlocked_achievements": unlocked_achievements
})
# 按时间排序
records.sort(key=lambda x: x["draw_time"])
return {
"success": True,
"date": date,
"records": records,
"total_count": len(records)
"""
阴阳师抽卡插件 - 抽卡核心逻辑模块
实现抽卡核心算法,包括:
- 多稀有度抽卡R/SR/SSR/SP
- 子池支持
- 保底机制
- 成就检查
"""
import random
from typing import Dict, Tuple, List, Optional, Any
import os
from pathlib import Path
from .config import Config
from .data_manager import DataManager
config = Config()
data_manager = DataManager()
class GachaSystem:
"""抽卡系统核心类,管理抽卡逻辑和数据"""
def __init__(self):
self.data_manager = data_manager
def draw(self, user_id: str) -> Dict[str, Any]:
"""执行一次抽卡"""
# 检查抽卡限制
if not self.data_manager.check_daily_limit(user_id):
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": False,
"message": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!"
}
# 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id)
# 从该稀有度中抽取式神
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
if not shikigami_data:
return {
"success": False,
"message": f"系统错误:{rarity}稀有度下没有可用式神"
}
# 随机选择式神
shikigami = random.choice(shikigami_data)
# 记录抽卡
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
# 剩余次数
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": True,
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"],
"draws_left": draws_left,
"unlocked_achievements": unlocked_achievements
}
def _draw_rarity(self, user_id: str = None) -> str:
"""按概率抽取稀有度"""
# 检查是否是特殊概率用户
if user_id and user_id in config.SPECIAL_PROBABILITY_USERS:
# 100%概率抽到SSR或SP随机选择
return random.choice(["SSR", "SP"])
# 普通用户的概率逻辑
r = random.random() * 100 # 0-100的随机数
cumulative = 0
for rarity, prob in config.RARITY_PROBABILITY.items():
cumulative += prob
if r < cumulative:
return rarity
# 默认返回R理论上不会执行到这里
return "R"
def get_user_stats(self, user_id: str) -> Dict:
"""获取用户抽卡统计"""
user_stats = self.data_manager.get_user_stats()
if user_id not in user_stats:
return {
"success": False,
"message": "您还没有抽卡记录哦!"
}
stats = user_stats[user_id]
return {
"success": True,
"total_draws": stats["total_draws"],
"R_count": stats["R_count"],
"SR_count": stats["SR_count"],
"SSR_count": stats["SSR_count"],
"SP_count": stats["SP_count"],
"recent_draws": stats["draw_history"][-5:] if stats["draw_history"] else []
}
def get_probability_text(self) -> str:
"""获取概率展示文本"""
probs = config.RARITY_PROBABILITY
return f"--- 系统概率 ---\nR: {probs['R']}% | SR: {probs['SR']}% | SSR: {probs['SSR']}% | SP: {probs['SP']}%"
def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]:
"""获取抽卡排行榜数据"""
user_stats = self.data_manager.get_user_stats()
# 过滤有SSR/SP记录的用户
ranked_users = [
(user_id, stats)
for user_id, stats in user_stats.items()
if stats.get("SSR_count", 0) > 0 or stats.get("SP_count", 0) > 0
]
# 按SSR+SP总数降序排序
ranked_users.sort(
key=lambda x: (x[1].get("SSR_count", 0) + x[1].get("SP_count", 0)),
reverse=True
)
return ranked_users
def get_daily_stats(self) -> Dict:
"""获取今日抽卡统计"""
daily_draws = self.data_manager.get_daily_draws()
today = self.data_manager.get_today_date()
if not daily_draws or today not in daily_draws:
return {
"success": False,
"message": "今日还没有人抽卡哦!"
}
today_stats = daily_draws[today]
total_stats = {
"total_users": len(today_stats),
"total_draws": 0,
"R_count": 0,
"SR_count": 0,
"SSR_count": 0,
"SP_count": 0,
"user_stats": []
}
# 统计每个用户的抽卡情况
for user_id, draws in today_stats.items():
user_stats = {
"user_id": user_id,
"total_draws": len(draws),
"R_count": sum(1 for d in draws if d["rarity"] == "R"),
"SR_count": sum(1 for d in draws if d["rarity"] == "SR"),
"SSR_count": sum(1 for d in draws if d["rarity"] == "SSR"),
"SP_count": sum(1 for d in draws if d["rarity"] == "SP")
}
# 更新总统计
total_stats["total_draws"] += user_stats["total_draws"]
total_stats["R_count"] += user_stats["R_count"]
total_stats["SR_count"] += user_stats["SR_count"]
total_stats["SSR_count"] += user_stats["SSR_count"]
total_stats["SP_count"] += user_stats["SP_count"]
# 只记录抽到SSR或SP的用户
if user_stats["SSR_count"] > 0 or user_stats["SP_count"] > 0:
total_stats["user_stats"].append(user_stats)
# 按SSR+SP数量排序用户统计
total_stats["user_stats"].sort(
key=lambda x: (x["SSR_count"] + x["SP_count"]),
reverse=True
)
# 构建稀有度统计
rarity_stats = {
"R": total_stats["R_count"],
"SR": total_stats["SR_count"],
"SSR": total_stats["SSR_count"],
"SP": total_stats["SP_count"]
}
# 构建排行榜数据
top_users = []
for user_stat in total_stats["user_stats"]:
top_users.append({
"user_id": user_stat["user_id"],
"ssr_count": user_stat["SSR_count"] + user_stat["SP_count"]
})
final_stats = {
"total_users": total_stats["total_users"],
"total_draws": total_stats["total_draws"],
"rarity_stats": rarity_stats,
"top_users": top_users
}
return {
"success": True,
"date": today,
"stats": final_stats
}
def triple_draw(self, user_id: str) -> Dict:
"""执行三连抽"""
# 检查是否有足够的抽卡次数
draws_left = self.data_manager.get_draws_left(user_id)
if draws_left < 3:
return {
"success": False,
"message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会三连抽需要3次机会"
}
results = []
all_unlocked_achievements = []
# 执行三次抽卡
for i in range(3):
# 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id)
# 从该稀有度中抽取式神
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
if not shikigami_data:
return {
"success": False,
"message": f"系统错误:{rarity}稀有度下没有可用式神"
}
# 随机选择式神
shikigami = random.choice(shikigami_data)
# 记录抽卡
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
all_unlocked_achievements.extend(unlocked_achievements)
results.append({
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"]
})
# 剩余次数
draws_left = self.data_manager.get_draws_left(user_id)
return {
"success": True,
"results": results,
"draws_left": draws_left,
"unlocked_achievements": list(set(all_unlocked_achievements)) # 去重
}
def get_user_achievements(self, user_id: str) -> Dict:
"""获取用户成就信息"""
achievement_data = self.data_manager.get_user_achievements(user_id)
if not achievement_data["unlocked"] and all(v == 0 for v in achievement_data["progress"].values()):
return {
"success": False,
"message": "您还没有任何成就进度哦!快去抽卡吧!"
}
return {
"success": True,
"achievements": achievement_data["unlocked"],
"progress": achievement_data["progress"]
}
def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict:
"""获取每日详细抽卡记录"""
if not date:
date = self.data_manager.get_today_date()
daily_draws = self.data_manager.get_daily_draws()
if not daily_draws or date not in daily_draws:
return {
"success": False,
"message": f"{date} 没有抽卡记录"
}
records = []
for user_id, draws in daily_draws[date].items():
for draw in draws:
# 检查这次抽卡是否解锁了成就
unlocked_achievements = []
draw_time = draw.get("timestamp", "未知时间")
# 获取用户成就信息
achievement_data = self.data_manager.get_user_achievements(user_id)
if achievement_data["unlocked"]:
# 检查是否有在抽卡时间之后解锁的成就
for achievement_id, achievement_info in achievement_data["unlocked"].items():
if achievement_info["unlocked_date"] == f"{date} {draw_time}":
unlocked_achievements.append(achievement_id)
records.append({
"user_id": user_id,
"draw_time": draw_time,
"shikigami_name": draw["name"],
"rarity": draw["rarity"],
"unlocked_achievements": unlocked_achievements
})
# 按时间排序
records.sort(key=lambda x: x["draw_time"])
return {
"success": True,
"date": date,
"records": records,
"total_count": len(records)
}

View 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",
]

View 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())

View 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)

View 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)

View 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())

View 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)

View 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)

View 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)

View 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)

View File

@@ -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响应格式不变命令触发方式不变。

View 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))

View File

@@ -1,42 +1,108 @@
import os
from typing import Optional
from pathlib import Path
def get_image_path(file_path: str) -> str:
"""获取图片的绝对路径"""
return os.path.abspath(file_path)
def format_user_mention(user_id: str, user_name: Optional[str] = None) -> str:
"""格式化用户@信息"""
display_name = user_name if user_name else f"用户{user_id}"
return f"@{display_name}"
def get_luck_description(points: int) -> tuple[str, str]:
"""根据积分返回运气描述与emoji"""
if points <= 10:
return "非酋", "😭"
if points <= 30:
return "一般", "😐"
if points <= 60:
return "小欧", "😊"
if points <= 90:
return "大欧", "🎉"
return "欧皇", "👑"
def format_sign_in_message(
user_id: str,
user_name: str,
points: int,
balance: int,
) -> str:
"""格式化签到成功消息"""
luck_text, luck_emoji = get_luck_description(points)
mention = format_user_mention(user_id, user_name)
return (
f"{mention} 📅 每日签到成功!\n"
f"🎁 获得积分:{points}\n"
f"{luck_emoji} 今日运气:{luck_text}\n"
f"💰 当前积分:{balance}"
)
"""
阴阳师抽卡插件 - 通用工具函数
提供常用的辅助函数:
- 用户提及格式化
- 图片路径处理
"""
import os
from typing import Optional
from pathlib import Path
def get_image_path(file_path: str) -> str:
"""获取图片的绝对路径"""
return os.path.abspath(file_path)
def format_user_mention(user_id: str, user_name: Optional[str] = None) -> str:
"""格式化用户@信息"""
display_name = user_name if user_name else f"用户{user_id}"
return f"@{display_name}"
def get_luck_description(points: int) -> tuple[str, str]:
"""根据积分返回运气描述与emoji"""
if points <= 10:
return "非酋", "😭"
if points <= 30:
return "一般", "😐"
if points <= 60:
return "小欧", "😊"
if points <= 90:
return "大欧", "🎉"
return "欧皇", "👑"
def format_sign_in_message(
user_id: str,
user_name: str,
points: int,
balance: int,
) -> str:
"""格式化签到成功消息"""
luck_text, luck_emoji = get_luck_description(points)
mention = format_user_mention(user_id, user_name)
return (
f"{mention} 📅 每日签到成功!\n"
f"🎁 获得积分:{points}\n"
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

View File

@@ -197,4 +197,4 @@ def register_web_routes():
return True
except Exception as e:
logger.error(f"❌ 注册 Web 路由时出错: {e}")
return False
return False