feat(bot): use runtime api for bot data

This commit is contained in:
2026-06-20 18:20:40 +08:00
parent f67f3ca1d6
commit 8d26c46323
16 changed files with 1803 additions and 1491 deletions

View File

@@ -1,7 +1,7 @@
import os
import logging
import random
from nonebot import on_command, on_startswith
import os
import logging
import random
from nonebot import get_driver, on_command, on_startswith
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message
from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.typing import T_State
@@ -29,12 +29,22 @@ ACHIEVEMENT_COMMANDS = config.ACHIEVEMENT_COMMANDS
INTRO_COMMANDS = config.INTRO_COMMANDS
DAILY_LIMIT = config.DAILY_LIMIT
gacha_system = GachaSystem()
logger = logging.getLogger(__name__)
gacha_system = GachaSystem()
logger = logging.getLogger(__name__)
SIGN_IN_MIN_POINTS = 1
SIGN_IN_MAX_POINTS = 100
SIGN_IN_SOURCE = "gacha_sign"
SIGN_IN_REASON = "抽卡签到"
SIGN_IN_REASON = "抽卡签到"
@get_driver().on_startup
async def load_gacha_shikigami_data() -> None:
"""启动时从 xapi 拉取式神基础数据缓存。"""
try:
await gacha_system.data_manager.refresh_shikigami_data()
except Exception:
logger.exception("启动拉取抽卡式神缓存失败")
# 检查是否允许使用功能的规则
def check_permission() -> Rule:
@@ -52,14 +62,11 @@ def check_permission() -> Rule:
return Rule(_checker)
async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None:
"""处理抽卡成功后的每日签到,不影响主流程"""
try:
if gacha_system.data_manager.has_signed_in_today(user_id):
return
points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS)
success, new_balance = await points_api.add_points(
async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> None:
"""处理抽卡成功后的每日签到,不影响主流程"""
try:
points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS)
success, new_balance = await points_api.add_points(
user_id,
points,
SIGN_IN_SOURCE,
@@ -69,13 +76,23 @@ async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> Non
logger.error("抽卡签到积分发放失败 user_id=%s points=%s", user_id, points)
return
if not gacha_system.data_manager.record_sign_in(user_id, points):
logger.warning("抽卡签到落库冲突,积分已发放但签到记录重复 user_id=%s", user_id)
return
await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance))
except Exception:
logger.exception("处理抽卡签到失败 user_id=%s", user_id)
if not await gacha_system.data_manager.record_sign_in(user_id, points):
logger.warning("抽卡签到落库冲突,积分已发放但签到记录重复 user_id=%s", user_id)
return
await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance))
except Exception:
logger.exception("处理抽卡签到失败 user_id=%s", user_id)
async def claim_achievement_after_reward(user_id: str, achievement_id: str, reward_success: bool) -> None:
"""成就奖励发放成功后标记 xapi reward_claimed。"""
if not reward_success:
return
claimed = await gacha_system.data_manager.claim_achievement_reward(user_id, achievement_id)
if not claimed:
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())
@@ -86,7 +103,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
# 执行抽卡
result = gacha_system.draw(user_id)
result = await gacha_system.draw(user_id)
if not result["success"]:
await gacha_matcher.finish(format_user_mention(user_id, user_name) + "" + result["message"])
@@ -127,8 +144,9 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State):
has_manual_rewards = False
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)
# 检查是否是重复奖励
if "_repeat_" in achievement_id:
@@ -161,7 +179,7 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State):
msg.append("💰 未自动发放的奖励请联系管理员\n")
# 添加成就进度提示
achievement_data = gacha_system.get_user_achievements(user_id)
achievement_data = await gacha_system.get_user_achievements(user_id)
if achievement_data["success"]:
progress = achievement_data["progress"]
consecutive_days = progress.get("consecutive_days", 0)
@@ -226,13 +244,13 @@ async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State):
await try_handle_daily_sign_in(gacha_matcher, user_id, user_name)
return
async def notify_admin(bot: Bot, message: str):
"""通知管理员"""
admin_id = 2185330092
try:
await bot.send_private_msg(user_id=admin_id, message=message)
except Exception as e:
pass # 忽略通知失败的错误
async def notify_admin(bot: Bot, message: str):
"""通知管理员"""
admin_id = 2185330092
try:
await bot.send_private_msg(user_id=admin_id, message=message)
except Exception as e:
logger.debug("通知管理员失败: %s", e)
# 注册查询命令,添加权限检查规则
stats_matcher = on_command("我的抽卡", aliases=set(STATS_COMMANDS), priority=5, rule=check_permission())
@@ -252,7 +270,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
# 获取用户统计
stats = gacha_system.get_user_stats(user_id)
stats = await gacha_system.get_user_stats(user_id)
if not stats["success"]:
await stats_matcher.finish(format_user_mention(user_id, user_name) + " " + stats["message"])
@@ -294,7 +312,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State):
user_name = event.sender.card or event.sender.nickname or "未知用户"
# 执行三连抽
result = gacha_system.triple_draw(user_id)
result = await gacha_system.triple_draw(user_id)
if not result["success"]:
await triple_gacha_matcher.finish(f"{result['message']}")
@@ -338,8 +356,9 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State):
has_manual_rewards = False
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)
# 检查是否是重复奖励
if "_repeat_" in achievement_id:
@@ -372,7 +391,7 @@ async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State):
msg.append("💰 未自动发放的奖励请联系管理员\n")
# 添加成就进度提示
achievement_data = gacha_system.get_user_achievements(user_id)
achievement_data = await gacha_system.get_user_achievements(user_id)
if achievement_data["success"]:
progress = achievement_data["progress"]
consecutive_days = progress.get("consecutive_days", 0)
@@ -442,7 +461,7 @@ async def handle_achievement(bot: Bot, event: MessageEvent, state: T_State):
user_name = event.sender.card or event.sender.nickname or "未知用户"
# 获取用户成就信息
result = gacha_system.get_user_achievements(user_id)
result = await gacha_system.get_user_achievements(user_id)
if not result["success"]:
await achievement_matcher.finish(f"{result['message']}")
@@ -561,7 +580,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
# 获取用户统计
stats = gacha_system.get_user_stats(target_user_id)
stats = await gacha_system.get_user_stats(target_user_id)
# 构建响应消息
msg = Message()
@@ -623,7 +642,7 @@ rank_matcher = on_startswith(("抽卡排行","抽卡榜"), priority=1, rule=chec
@rank_matcher.handle()
async def handle_rank(bot: Bot, event: MessageEvent, state: T_State):
# 获取排行榜数据
rank_data = gacha_system.get_rank_list()
rank_data = await gacha_system.get_rank_list()
if not rank_data:
await rank_matcher.finish("暂无抽卡排行榜数据")
@@ -668,7 +687,7 @@ async def handle_rank(bot: Bot, event: MessageEvent, state: T_State):
@daily_stats_matcher.handle()
async def handle_daily_stats(bot: Bot, event: MessageEvent, state: T_State):
"""处理今日抽卡统计命令"""
result = gacha_system.get_daily_stats()
result = await gacha_system.get_daily_stats()
if not result["success"]:
await daily_stats_matcher.finish(f"{result['message']}")

View File

@@ -1,7 +1,7 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import model_validator
import os
import logging
from pydantic import field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
import os
import logging
logger = logging.getLogger("onmyoji_gacha")
@@ -115,13 +115,24 @@ class Config(BaseSettings):
WEB_ADMIN_TOKEN: str = os.getenv("WEB_ADMIN_TOKEN", "onmyoji_admin_token_2024")
WEB_ADMIN_PORT: int = int(os.getenv("WEB_ADMIN_PORT", "8080"))
# 蛋定服务器对接配置
DD_API_HOST: str = "https://api.danding.vip/DD/"
BOT_TOKEN: str = os.getenv("ONMYOJI_BOT_TOKEN", os.getenv("BOT_TOKEN", "")) # 必须设置
BOT_USER_ID: str = "1424473282"
# 蛋定服务器对接配置
DD_API_HOST: str = "https://api.danding.vip/DD/"
GACHA_API_HOST: str = os.getenv("DANDING_GACHA_API_HOST", "https://api.danding.vip/bot/gacha")
BOT_TOKEN: str = os.getenv(
"DANDING_BOT_TOKEN",
os.getenv("ONMYOJI_BOT_TOKEN", os.getenv("DANDING_API_TOKEN", os.getenv("BOT_TOKEN", ""))),
)
BOT_USER_ID: str = os.getenv("DANDING_BOT_USER", "1424473282")
# 时区
TIMEZONE: str = "Asia/Shanghai"
TIMEZONE: str = "Asia/Shanghai"
@field_validator("GACHA_API_HOST")
@classmethod
def validate_gacha_api_host(cls, value):
if not value:
raise ValueError("GACHA_API_HOST cannot be empty")
return value.rstrip("/")
@model_validator(mode="after")
def _warn_default_token(self):

View File

@@ -1,615 +1,291 @@
"""
阴阳师抽卡插件 - 数据管理模块
管理抽卡数据持久化,包括:
- 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
import datetime
from typing import Dict, List, Any, Optional
import logging
from pathlib import Path
from .config import Config
# 创建Config实例
config = Config()
class DataManager:
"""抽卡数据管理器,封装所有数据库操作"""
def __init__(self):
# 确保目录存在
os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True)
# 初始化数据库
self._init_db()
# 加载式神数据
self.shikigami_data = self._load_shikigami_data()
def _init_db(self):
"""初始化数据库"""
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
# 创建式神表
cursor.execute("""
CREATE TABLE IF NOT EXISTS shikigami (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
rarity TEXT NOT NULL,
image_path TEXT NOT NULL
)
""")
# 创建每日抽卡记录表
cursor.execute("""
CREATE TABLE IF NOT EXISTS daily_draws (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
user_id TEXT NOT NULL,
rarity TEXT NOT NULL,
shikigami_id INTEGER NOT NULL,
timestamp TEXT NOT NULL,
FOREIGN KEY (shikigami_id) REFERENCES shikigami(id)
)
""")
# 创建用户统计表
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
total_draws INTEGER DEFAULT 0,
R_count INTEGER DEFAULT 0,
SR_count INTEGER DEFAULT 0,
SSR_count INTEGER DEFAULT 0,
SP_count INTEGER DEFAULT 0
)
""")
# 创建抽卡历史表
cursor.execute("""
CREATE TABLE IF NOT EXISTS draw_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
date TEXT NOT NULL,
rarity TEXT NOT NULL,
shikigami_id INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES user_stats(user_id),
FOREIGN KEY (shikigami_id) REFERENCES shikigami(id)
)
""")
# 创建成就表
cursor.execute("""
CREATE TABLE IF NOT EXISTS achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
achievement_id TEXT NOT NULL,
unlocked_date TEXT NOT NULL,
reward_claimed INTEGER DEFAULT 0,
UNIQUE(user_id, achievement_id)
)
""")
# 创建用户成就进度表
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_achievement_progress (
user_id TEXT PRIMARY KEY,
consecutive_days INTEGER DEFAULT 0,
last_draw_date TEXT DEFAULT '',
no_ssr_streak INTEGER DEFAULT 0,
total_consecutive_days INTEGER DEFAULT 0
)
""")
self._init_sign_in_table(cursor)
conn.commit()
def _init_sign_in_table(self, cursor: sqlite3.Cursor) -> None: # OK
"""创建每日签到表"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS daily_sign_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
sign_date TEXT NOT NULL,
points_awarded INTEGER NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(user_id, sign_date)
)
""")
def update_achievement_progress(self, user_id: str, rarity: str) -> List[str]: # type: ignore[return]
"""更新用户成就进度,返回新解锁的成就列表"""
today = self.get_today_date()
unlocked_achievements = []
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
# 获取或创建用户成就进度
cursor.execute(
"SELECT * FROM user_achievement_progress WHERE user_id = ?",
(user_id,)
)
progress = cursor.fetchone()
if not progress:
cursor.execute(
"INSERT INTO user_achievement_progress (user_id, last_draw_date) VALUES (?, ?)",
(user_id, today)
)
consecutive_days = 1
no_ssr_streak = 1 if rarity not in ["SSR", "SP"] else 0
total_consecutive_days = 1
else:
last_draw_date = progress[2]
consecutive_days = progress[1]
no_ssr_streak = progress[3]
total_consecutive_days = progress[4]
# 更新连续抽卡天数
if last_draw_date != today:
# 检查是否是连续的一天
last_date = datetime.datetime.strptime(last_draw_date, "%Y-%m-%d")
current_date = datetime.datetime.strptime(today, "%Y-%m-%d")
days_diff = (current_date - last_date).days
if days_diff == 1:
consecutive_days += 1
total_consecutive_days += 1
elif days_diff > 1:
consecutive_days = 1
total_consecutive_days += 1
# days_diff == 0 表示今天已经抽过卡了,不更新连续天数
# 更新无SSR连击数
if rarity in ["SSR", "SP"]:
no_ssr_streak = 0
else:
no_ssr_streak += 1
# 更新进度
cursor.execute("""
INSERT OR REPLACE INTO user_achievement_progress
(user_id, consecutive_days, last_draw_date, no_ssr_streak, total_consecutive_days)
VALUES (?, ?, ?, ?, ?)
""", (user_id, consecutive_days, today, no_ssr_streak, total_consecutive_days))
# 检查是否解锁新成就
for achievement_id, achievement_config in config.ACHIEVEMENTS.items():
# 对于可重复获得的成就(勤勤恳恳系列),需要特殊处理
if achievement_config.get("repeatable", False) and achievement_config["type"] == "consecutive_days":
# 检查连续抽卡成就的升级逻辑
if consecutive_days >= achievement_config["threshold"]:
# 检查是否已经解锁过这个等级
cursor.execute(
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
(user_id, achievement_id)
)
if not cursor.fetchone():
# 解锁新等级的成就
cursor.execute("""
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
VALUES (?, ?, ?)
""", (user_id, achievement_id, today))
unlocked_achievements.append(achievement_id)
# 如果是最高等级(Ⅴ),检查是否需要给重复奖励
elif achievement_config["level"] == 5 and consecutive_days >= 150:
# 每30天给一次重复奖励
days_over_150 = consecutive_days - 150
if days_over_150 > 0 and days_over_150 % 30 == 0:
# 检查这个重复奖励是否已经给过
repeat_id = f"{achievement_id}_repeat_{days_over_150//30}"
cursor.execute(
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
(user_id, repeat_id)
)
if not cursor.fetchone():
cursor.execute("""
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
VALUES (?, ?, ?)
""", (user_id, repeat_id, today))
unlocked_achievements.append(achievement_id)
else:
# 非重复成就的原有逻辑
# 检查是否已经解锁
cursor.execute(
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
(user_id, achievement_id)
)
if cursor.fetchone():
continue
# 检查成就条件
unlocked = False
if achievement_config["type"] == "consecutive_days":
if consecutive_days >= achievement_config["threshold"]:
unlocked = True
elif achievement_config["type"] == "no_ssr_streak":
if no_ssr_streak >= achievement_config["threshold"]:
unlocked = True
if unlocked:
cursor.execute("""
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
VALUES (?, ?, ?)
""", (user_id, achievement_id, today))
unlocked_achievements.append(achievement_id)
conn.commit()
return unlocked_achievements
def get_user_achievements(self, user_id: str) -> Dict[str, Any]:
"""获取用户成就信息"""
with sqlite3.connect(config.DB_FILE) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 获取已解锁的成就
cursor.execute(
"SELECT achievement_id, unlocked_date, reward_claimed FROM achievements WHERE user_id = ?",
(user_id,)
)
unlocked = {row["achievement_id"]: {
"unlocked_date": row["unlocked_date"],
"reward_claimed": bool(row["reward_claimed"])
} for row in cursor.fetchall()}
# 获取进度
cursor.execute(
"SELECT * FROM user_achievement_progress WHERE user_id = ?",
(user_id,)
)
progress_row = cursor.fetchone()
if not progress_row:
progress = {
"consecutive_days": 0,
"no_ssr_streak": 0,
"total_consecutive_days": 0
}
else:
progress = {
"consecutive_days": progress_row["consecutive_days"],
"no_ssr_streak": progress_row["no_ssr_streak"],
"total_consecutive_days": progress_row["total_consecutive_days"]
}
return {
"unlocked": unlocked,
"progress": progress
}
def claim_achievement_reward(self, user_id: str, achievement_id: str) -> bool:
"""领取成就奖励"""
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE achievements
SET reward_claimed = 1
WHERE user_id = ? AND achievement_id = ? AND reward_claimed = 0
""", (user_id, achievement_id))
conn.commit()
return cursor.rowcount > 0
def _load_shikigami_data(self) -> Dict[str, List[Dict[str, str]]]:
"""加载式神数据到数据库"""
result = {"R": [], "SR": [], "SSR": [], "SP": []}
rarity_dirs = {
"R": "r",
"SR": "sr",
"SSR": "ssr",
"SP": "sp"
}
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
# 清空现有式神数据
cursor.execute("DELETE FROM shikigami")
for rarity, dir_name in rarity_dirs.items():
dir_path = os.path.join(config.SHIKIGAMI_IMG_DIR, dir_name)
if os.path.exists(dir_path):
for file_name in os.listdir(dir_path):
if file_name.endswith(('.png', '.jpg', '.jpeg')):
name = os.path.splitext(file_name)[0]
image_path = os.path.join(dir_path, file_name)
# 插入式神数据
cursor.execute(
"INSERT INTO shikigami (name, rarity, image_path) VALUES (?, ?, ?)",
(name, rarity, image_path)
)
result[rarity].append({
"name": name,
"image_url": image_path
})
conn.commit()
return result
def get_today_date(self) -> str:
"""获取当前日期字符串"""
return datetime.datetime.now().strftime("%Y-%m-%d")
def has_signed_in_today(self, user_id: str) -> bool:
"""检查用户今天是否已签到"""
today = self.get_today_date()
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT 1 FROM daily_sign_in WHERE user_id = ? AND sign_date = ? LIMIT 1",
(user_id, today),
)
return cursor.fetchone() is not None
def record_sign_in(self, user_id: str, points_awarded: int) -> bool:
"""记录每日签到重复签到返回False"""
today = self.get_today_date()
created_at = datetime.datetime.now().isoformat()
try:
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO daily_sign_in (user_id, sign_date, points_awarded, created_at)
VALUES (?, ?, ?, ?)
""", (user_id, today, points_awarded, created_at))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
def get_current_time(self) -> str:
"""获取当前时间字符串"""
return datetime.datetime.now().strftime("%H:%M:%S")
def get_daily_draws(self) -> Dict[str, Dict[str, List[Dict[str, str]]]]:
"""获取每日抽卡记录"""
result = {}
today = self.get_today_date()
with sqlite3.connect(config.DB_FILE) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 先查询今日的抽卡记录
cursor.execute("""
SELECT date, user_id, rarity, shikigami_id, timestamp
FROM daily_draws
WHERE date = ?
ORDER BY timestamp
""", (today,))
rows = cursor.fetchall()
# 获取所有涉及的式神ID
shikigami_ids = list(set(row["shikigami_id"] for row in rows))
# 查询式神信息
shikigami_info = {}
if shikigami_ids:
placeholders = ','.join('?' * len(shikigami_ids))
cursor.execute(f"""
SELECT id, name, rarity
FROM shikigami
WHERE id IN ({placeholders})
""", shikigami_ids)
for shikigami_row in cursor.fetchall():
shikigami_info[shikigami_row["id"]] = {
"name": shikigami_row["name"],
"rarity": shikigami_row["rarity"]
}
# 构建结果
for row in rows:
date = row["date"]
user_id = row["user_id"]
shikigami_id = row["shikigami_id"]
if date not in result:
result[date] = {}
if user_id not in result[date]:
result[date][user_id] = []
# 如果找不到式神信息使用daily_draws表中的稀有度和默认名称
if shikigami_id in shikigami_info:
name = shikigami_info[shikigami_id]["name"]
rarity = shikigami_info[shikigami_id]["rarity"]
else:
name = f"式神{shikigami_id}"
rarity = row["rarity"]
result[date][user_id].append({
"rarity": rarity,
"name": name,
"timestamp": row["timestamp"]
})
return result
def save_daily_draws(self, data: Dict[str, Dict[str, List[Dict[str, str]]]]):
"""保存每日抽卡记录"""
# SQLite实现中此方法为空因为记录时直接插入数据库
pass
def get_user_stats(self) -> Dict[str, Dict[str, Any]]:
"""获取用户统计数据"""
result = {}
with sqlite3.connect(config.DB_FILE) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 获取基础统计
cursor.execute("SELECT * FROM user_stats")
user_stats = cursor.fetchall()
for stat in user_stats:
user_id = stat["user_id"]
result[user_id] = {
"total_draws": stat["total_draws"],
"R_count": stat["R_count"],
"SR_count": stat["SR_count"],
"SSR_count": stat["SSR_count"],
"SP_count": stat["SP_count"],
"draw_history": []
}
# 获取抽卡历史
cursor.execute("""
SELECT draw_history.date, draw_history.rarity, shikigami.name
FROM draw_history
JOIN shikigami ON draw_history.shikigami_id = shikigami.id
WHERE draw_history.user_id = ?
ORDER BY draw_history.date DESC
LIMIT 100
""", (user_id,))
history = cursor.fetchall()
result[user_id]["draw_history"] = [
{
"date": row["date"],
"rarity": row["rarity"],
"name": row["name"]
} for row in history
]
return result
def save_user_stats(self, data: Dict[str, Dict[str, Any]]):
"""保存用户统计数据"""
# SQLite实现中此方法为空因为统计时直接更新数据库
pass
def check_daily_limit(self, user_id: str) -> bool:
"""检查用户是否达到每日抽卡限制"""
today = self.get_today_date()
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT COUNT(*)
FROM daily_draws
WHERE date = ? AND user_id = ?
""", (today, user_id))
count = cursor.fetchone()[0]
return count < config.DAILY_LIMIT
def get_draws_left(self, user_id: str) -> int:
"""获取用户今日剩余抽卡次数"""
today = self.get_today_date()
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT COUNT(*)
FROM daily_draws
WHERE date = ? AND user_id = ?
""", (today, user_id))
count = cursor.fetchone()[0]
return max(0, config.DAILY_LIMIT - count)
def record_draw(self, user_id: str, rarity: str, shikigami_name: str) -> List[str]:
"""记录一次抽卡,返回新解锁的成就列表"""
today = self.get_today_date()
current_time = self.get_current_time()
with sqlite3.connect(config.DB_FILE) as conn:
cursor = conn.cursor()
# 获取式神ID
cursor.execute(
"SELECT id FROM shikigami WHERE name = ? AND rarity = ?",
(shikigami_name, rarity)
)
shikigami_id = cursor.fetchone()
if not shikigami_id:
logging.error(f"找不到式神: {shikigami_name} ({rarity})")
return []
shikigami_id = shikigami_id[0]
# 记录每日抽卡
cursor.execute("""
INSERT INTO daily_draws (date, user_id, rarity, shikigami_id, timestamp)
VALUES (?, ?, ?, ?, ?)
""", (today, user_id, rarity, shikigami_id, current_time))
# 更新用户统计
cursor.execute("""
INSERT OR IGNORE INTO user_stats (user_id) VALUES (?)
""", (user_id,))
cursor.execute("""
UPDATE user_stats
SET total_draws = total_draws + 1,
R_count = R_count + ?,
SR_count = SR_count + ?,
SSR_count = SSR_count + ?,
SP_count = SP_count + ?
WHERE user_id = ?
""", (
1 if rarity == "R" else 0,
1 if rarity == "SR" else 0,
1 if rarity == "SSR" else 0,
1 if rarity == "SP" else 0,
user_id
))
# 添加抽卡历史
cursor.execute("""
INSERT INTO draw_history (user_id, date, rarity, shikigami_id)
VALUES (?, ?, ?, ?)
""", (user_id, today, rarity, shikigami_id))
# 保持历史记录不超过100条
cursor.execute("""
DELETE FROM draw_history
WHERE user_id = ? AND id NOT IN (
SELECT id FROM draw_history
WHERE user_id = ?
ORDER BY date DESC
LIMIT 100
)
""", (user_id, user_id))
conn.commit()
# 更新成就进度
unlocked_achievements = self.update_achievement_progress(user_id, rarity)
return unlocked_achievements
"""
阴阳师抽卡插件 - xapi 数据管理模块
本模块只负责调用 xapi /bot/gacha 运行时 API。抽卡概率、奖励发放和 QQ 消息编排
仍由 nonebot 插件本地负责。
"""
from __future__ import annotations
import asyncio
import datetime
import logging
from typing import Any, Dict, List, Optional
import aiohttp
from .config import Config
logger = logging.getLogger(__name__)
config = Config()
class DataManager:
"""抽卡数据管理器,封装 /bot/gacha HTTP 调用。"""
def __init__(self):
self.shikigami_data: Dict[str, List[Dict[str, Any]]] = {"R": [], "SR": [], "SSR": [], "SP": []}
def _url(self, path: str) -> str:
"""拼接 /bot/gacha 端点地址。"""
return f"{config.GACHA_API_HOST}/{path.lstrip('/')}"
def _auth(self) -> Dict[str, str]:
"""生成 xapi Bot 鉴权参数。"""
return {
"user": config.BOT_USER_ID,
"token": config.BOT_TOKEN,
}
async def _request(
self,
method: str,
path: str,
*,
payload: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""调用 xapi /bot/gacha并只向上层暴露 data。"""
request_url = self._url(path)
timeout = aiohttp.ClientTimeout(total=10)
try:
async with aiohttp.ClientSession() as session:
if method == "GET":
request_params = {**self._auth(), **(params or {})}
async with session.get(request_url, params=request_params, timeout=timeout) as resp:
return await self._parse_response(resp, path)
request_payload = {**self._auth(), **(payload or {})}
async with session.post(request_url, json=request_payload, timeout=timeout) as resp:
return await self._parse_response(resp, path)
except aiohttp.ClientError as exc:
logger.error("gacha api request failed path=%s error=%s", path, exc)
return None
except asyncio.TimeoutError as exc:
logger.error("gacha api request timeout path=%s error=%s", path, exc)
return None
async def _parse_response(self, resp: aiohttp.ClientResponse, path: str) -> Optional[Dict[str, Any]]:
"""解析 xapi 统一响应,失败时返回 None 维持旧调用方失败语义。"""
if resp.status != 200:
logger.error("gacha api bad status path=%s status=%s", path, resp.status)
return None
body = await resp.json()
if body.get("code") != 200:
logger.error("gacha api fail path=%s code=%s message=%s", path, body.get("code"), body.get("message"))
return None
data = body.get("data")
return data if isinstance(data, dict) else None
async def refresh_shikigami_data(self) -> Dict[str, List[Dict[str, Any]]]:
"""从 xapi 拉取式神基础数据并按稀有度缓存。"""
data = await self._request("GET", "shikigami")
items = data.get("items", []) if data else []
grouped: Dict[str, List[Dict[str, Any]]] = {"R": [], "SR": [], "SSR": [], "SP": []}
for item in items:
rarity = item.get("rarity")
if rarity not in grouped:
continue
image_path = item.get("image_path") or item.get("image_url") or ""
grouped[rarity].append(
{
"id": item.get("id"),
"name": item.get("name"),
"rarity": rarity,
"image_path": image_path,
"image_url": image_path,
}
)
self.shikigami_data = grouped
return self.shikigami_data
async def ensure_shikigami_data(self) -> Dict[str, List[Dict[str, Any]]]:
"""确保式神缓存已加载。"""
if not any(self.shikigami_data.values()):
await self.refresh_shikigami_data()
return self.shikigami_data
def get_today_date(self) -> str:
"""获取当前日期字符串。"""
return datetime.datetime.now().strftime("%Y-%m-%d")
def get_current_time(self) -> str:
"""获取当前时间字符串。"""
return datetime.datetime.now().strftime("%H:%M:%S")
def _find_shikigami(self, rarity: str, shikigami_name: str) -> Optional[Dict[str, Any]]:
"""从本地缓存查找 xapi 托管式神。"""
for item in self.shikigami_data.get(rarity, []):
if item.get("name") == shikigami_name:
return item
return None
async def get_draws_left(self, user_id: str) -> int:
"""获取用户今日剩余抽卡次数。"""
data = await self._request("GET", "draws-left", params={"user_id": user_id})
if data is None:
return 0
return int(data.get("draws_left", 0) or 0)
async def check_daily_limit(self, user_id: str) -> bool:
"""检查用户是否还有抽卡次数。"""
return await self.get_draws_left(user_id) > 0
async def record_draw_result(self, user_id: str, rarity: str, shikigami: Dict[str, Any]) -> Dict[str, Any]:
"""写入一次抽卡并返回 xapi 原始业务结果。"""
data = await self._request(
"POST",
"draw",
payload={
"user_id": user_id,
"shikigami_id": int(shikigami["id"]),
"rarity": rarity,
"name": shikigami["name"],
},
)
if data is None:
return {"success": False, "message": "抽卡记录写入失败"}
return data
async def record_triple_draw_result(self, user_id: str, draws: List[Dict[str, Any]]) -> Dict[str, Any]:
"""写入三连抽并返回 xapi 原始业务结果。"""
payload_draws = [
{
"shikigami_id": int(item["id"]),
"rarity": item["rarity"],
"name": item["name"],
}
for item in draws
]
data = await self._request("POST", "draw/triple", payload={"user_id": user_id, "draws": payload_draws})
if data is None:
return {"success": False, "message": "三连抽记录写入失败"}
return data
async def record_draw(self, user_id: str, rarity: str, shikigami_name: str) -> List[str]:
"""记录一次抽卡,返回新解锁的成就列表。"""
await self.ensure_shikigami_data()
shikigami = self._find_shikigami(rarity, shikigami_name)
if not shikigami:
logger.error("找不到式神: %s (%s)", shikigami_name, rarity)
return []
result = await self.record_draw_result(user_id, rarity, shikigami)
if not result.get("success"):
logger.error("抽卡记录写入失败 user_id=%s message=%s", user_id, result.get("message"))
return []
return result.get("unlocked_achievements", [])
async def record_sign_in(self, user_id: str, points_awarded: int) -> bool:
"""记录每日签到,重复签到返回 False。"""
data = await self._request(
"POST",
"sign-in",
payload={"user_id": user_id, "points_awarded": points_awarded},
)
if data is None:
return False
return bool(data.get("success")) and not bool(data.get("signed_already"))
async def get_user_stats(self, user_id: str) -> Dict[str, Any]:
"""获取用户抽卡统计。"""
data = await self._request("GET", "user-stats", params={"user_id": user_id})
return data or {"success": False, "message": "您还没有抽卡记录哦!"}
async def get_daily_stats(self, date: Optional[str] = None) -> Dict[str, Any]:
"""获取指定日期抽卡统计。"""
params = {"date": date} if date else {}
data = await self._request("GET", "daily-stats", params=params)
return data or {"success": False, "message": "今日还没有人抽卡哦!"}
async def get_rank(self, limit: int = 10) -> List[Dict[str, Any]]:
"""获取抽卡排行榜。"""
data = await self._request("GET", "rank", params={"limit": max(1, min(100, limit))})
if data is None:
return []
items = data.get("items", [])
return items if isinstance(items, list) else []
async def get_user_achievements(self, user_id: str) -> Dict[str, Any]:
"""获取用户成就信息。"""
data = await self._request("GET", f"achievements/{user_id}")
if data is None:
return {
"unlocked": {},
"progress": {
"consecutive_days": 0,
"no_ssr_streak": 0,
"total_consecutive_days": 0,
},
}
return {
"unlocked": data.get("achievements", {}),
"progress": data.get("progress", {}),
}
async def claim_achievement_reward(self, user_id: str, achievement_id: str) -> bool:
"""标记成就奖励已领取。"""
data = await self._request("POST", f"achievements/{user_id}/claim", payload={"achievement_id": achievement_id})
return bool(data and data.get("success"))
async def get_daily_records(self, date: Optional[str] = None) -> Dict[str, Any]:
"""获取每日详细抽卡记录。"""
params = {"date": date} if date else {}
data = await self._request("GET", "records/daily", params=params)
return data or {"success": False, "date": date or self.get_today_date(), "records": [], "total_count": 0}
async def get_daily_draws(self, date: Optional[str] = None) -> Dict[str, Dict[str, List[Dict[str, str]]]]:
"""按旧结构返回每日抽卡记录。"""
data = await self.get_daily_records(date)
result: Dict[str, Dict[str, List[Dict[str, str]]]] = {}
if not data.get("success"):
return result
target_date = data.get("date") or date or self.get_today_date()
result[target_date] = {}
for record in data.get("records", []):
user_id = record.get("user_id")
if not user_id:
continue
result[target_date].setdefault(user_id, []).append(
{
"rarity": record.get("rarity", ""),
"name": record.get("shikigami_name", ""),
"timestamp": record.get("draw_time", ""),
}
)
return result
async def has_signed_in_today(self, user_id: str) -> bool:
"""保留旧方法名;当前无独立查询端点,签到去重由 xapi sign-in 写接口处理。"""
return False
def save_daily_draws(self, data: Dict[str, Dict[str, List[Dict[str, str]]]]) -> None:
"""兼容旧空方法,运行时不写本地文件。"""
return None
def save_user_stats(self, data: Dict[str, Dict[str, Any]]) -> None:
"""兼容旧空方法,运行时不写本地文件。"""
return None

View File

@@ -24,44 +24,47 @@ 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}次,明天再来吧!"
}
async def draw(self, user_id: str) -> Dict[str, Any]:
"""执行一次抽卡"""
# 检查抽卡限制
if not await self.data_manager.check_daily_limit(user_id):
draws_left = await 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}稀有度下没有可用式神"
}
await self.data_manager.ensure_shikigami_data()
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)
# xapi 只负责写入抽卡数据;奖励副作用仍由 nonebot handler 编排。
record_result = await self.data_manager.record_draw_result(user_id, rarity, shikigami)
if not record_result.get("success"):
return {
"success": False,
"message": record_result.get("message", "抽卡记录写入失败")
}
return {
"success": True,
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"],
"draws_left": draws_left,
"unlocked_achievements": unlocked_achievements
}
"success": True,
"rarity": record_result.get("rarity", rarity),
"name": record_result.get("name", shikigami["name"]),
"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),
"unlocked_achievements": record_result.get("unlocked_achievements", [])
}
def _draw_rarity(self, user_id: str = None) -> str:
"""按概率抽取稀有度"""
@@ -82,145 +85,39 @@ class GachaSystem:
# 默认返回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 []
}
async def get_user_stats(self, user_id: str) -> Dict:
"""获取用户抽卡统计"""
return await self.data_manager.get_user_stats(user_id)
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,
async def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]:
"""获取抽卡排行榜数据"""
items = await self.data_manager.get_rank(limit=10)
return [(item["user_id"], item) for item in items]
async def get_daily_stats(self) -> Dict:
"""获取今日抽卡统计"""
return await self.data_manager.get_daily_stats()
async def triple_draw(self, user_id: str) -> Dict:
"""执行三连抽"""
# 检查是否有足够的抽卡次数
draws_left = await 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):
await self.data_manager.ensure_shikigami_data()
# 执行三次本地概率抽取,统一提交 xapi 三连写入端点。
for i in range(3):
# 抽取稀有度传递用户ID
rarity = self._draw_rarity(user_id)
@@ -235,29 +132,30 @@ class GachaSystem:
# 随机选择式神
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)
results.append({
"id": shikigami["id"],
"rarity": rarity,
"name": shikigami["name"],
"image_url": shikigami["image_url"]
})
record_result = await self.data_manager.record_triple_draw_result(user_id, results)
if not record_result.get("success"):
return {
"success": False,
"message": record_result.get("message", "三连抽记录写入失败")
}
return {
"success": True,
"results": record_result.get("results", results),
"draws_left": record_result.get("draws_left", 0),
"unlocked_achievements": record_result.get("unlocked_achievements", [])
}
async def get_user_achievements(self, user_id: str) -> Dict:
"""获取用户成就信息"""
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()):
return {
@@ -271,48 +169,6 @@ class GachaSystem:
"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)
}
async def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict:
"""获取每日详细抽卡记录"""
return await self.data_manager.get_daily_records(date)

View File

@@ -82,9 +82,9 @@ async def admin_page(request: Request):
# API 端点
@router.get("/api/stats/daily", response_model=DailyStatsResponse, dependencies=[Depends(verify_admin_token)])
async def get_daily_stats():
"""获取今日抽卡统计"""
result = gacha_system.get_daily_stats()
async def get_daily_stats():
"""获取今日抽卡统计"""
result = await gacha_system.get_daily_stats()
if not result["success"]:
return result
@@ -95,9 +95,9 @@ async def get_daily_stats():
}
@router.get("/api/stats/user/{user_id}", response_model=UserStatsResponse, dependencies=[Depends(verify_admin_token)])
async def get_user_stats(user_id: str):
"""获取用户抽卡统计"""
result = gacha_system.get_user_stats(user_id)
async def get_user_stats(user_id: str):
"""获取用户抽卡统计"""
result = await gacha_system.get_user_stats(user_id)
if not result["success"]:
return {
"success": False,
@@ -122,9 +122,9 @@ async def get_user_stats(user_id: str):
}
@router.get("/api/stats/rank", response_model=RankListResponse, dependencies=[Depends(verify_admin_token)])
async def get_rank_list():
"""获取抽卡排行榜"""
rank_data = gacha_system.get_rank_list()
async def get_rank_list():
"""获取抽卡排行榜"""
rank_data = await gacha_system.get_rank_list()
# 转换数据格式
formatted_data = []
@@ -145,9 +145,9 @@ async def get_rank_list():
}
@router.get("/api/achievements/{user_id}", response_model=AchievementResponse, dependencies=[Depends(verify_admin_token)])
async def get_user_achievements(user_id: str):
"""获取用户成就信息"""
result = gacha_system.get_user_achievements(user_id)
async def get_user_achievements(user_id: str):
"""获取用户成就信息"""
result = await gacha_system.get_user_achievements(user_id)
if not result["success"]:
return {
"success": False,
@@ -164,9 +164,9 @@ async def get_user_achievements(user_id: str):
}
@router.get("/api/records/daily", response_model=DailyDetailedRecordsResponse, dependencies=[Depends(verify_admin_token)])
async def get_daily_detailed_records(date: Optional[str] = None):
"""获取每日详细抽卡记录"""
result = gacha_system.get_daily_detailed_records(date)
async def get_daily_detailed_records(date: Optional[str] = None):
"""获取每日详细抽卡记录"""
result = await gacha_system.get_daily_detailed_records(date)
if not result["success"]:
return {
"success": False,