feat: add activity based group lottery
This commit is contained in:
@@ -14,4 +14,6 @@ main.py.bak
|
|||||||
backups/
|
backups/
|
||||||
cache/
|
cache/
|
||||||
botpy.log
|
botpy.log
|
||||||
|
front/node_modules/
|
||||||
|
front/dist/
|
||||||
.patch_autodelete.py
|
.patch_autodelete.py
|
||||||
|
|||||||
@@ -33,6 +33,35 @@ async def ensure_runtime_schema():
|
|||||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS delete_reason VARCHAR(255) NULL;
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS delete_reason VARCHAR(255) NULL;
|
||||||
ALTER TABLE user_bindings ADD COLUMN IF NOT EXISTS auth_email VARCHAR(255) NULL;
|
ALTER TABLE user_bindings ADD COLUMN IF NOT EXISTS auth_email VARCHAR(255) NULL;
|
||||||
ALTER TABLE user_bindings ADD COLUMN IF NOT EXISTS time_expire VARCHAR(100) NULL;
|
ALTER TABLE user_bindings ADD COLUMN IF NOT EXISTS time_expire VARCHAR(100) NULL;
|
||||||
|
CREATE TABLE IF NOT EXISTS lottery_events (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
chat_id BIGINT NOT NULL,
|
||||||
|
message_id BIGINT NULL,
|
||||||
|
creator_id BIGINT NOT NULL,
|
||||||
|
creator_name VARCHAR(255) NULL,
|
||||||
|
prize VARCHAR(500) NOT NULL,
|
||||||
|
condition_type VARCHAR(50) NOT NULL DEFAULT 'all',
|
||||||
|
condition_value INT NOT NULL DEFAULT 0,
|
||||||
|
end_type VARCHAR(50) NOT NULL DEFAULT 'people',
|
||||||
|
end_value INT NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
winner_id BIGINT NULL,
|
||||||
|
winner_name VARCHAR(255) NULL,
|
||||||
|
ended_at TIMESTAMPTZ NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS lottery_participants (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
event_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
username VARCHAR(255) NULL,
|
||||||
|
first_name VARCHAR(255) NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lottery_events_chat_status ON lottery_events(chat_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lottery_participants_event_user ON lottery_participants(event_id, user_id);
|
||||||
""")
|
""")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"⚠️ 自动补齐数据库字段失败: {type(e).__name__}: {e}")
|
logger.warning(f"⚠️ 自动补齐数据库字段失败: {type(e).__name__}: {e}")
|
||||||
|
|||||||
@@ -132,3 +132,38 @@ class GroupConfig(TimestampMixin, Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = "group_config"
|
table = "group_config"
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryEvent(TimestampMixin, Model):
|
||||||
|
"""群内抽奖活动"""
|
||||||
|
id = fields.BigIntField(pk=True, generated=True)
|
||||||
|
chat_id = fields.BigIntField()
|
||||||
|
message_id = fields.BigIntField(null=True)
|
||||||
|
creator_id = fields.BigIntField()
|
||||||
|
creator_name = fields.CharField(max_length=255, null=True)
|
||||||
|
prize = fields.CharField(max_length=500)
|
||||||
|
condition_type = fields.CharField(max_length=50, default="all") # all/checkin/min_points
|
||||||
|
condition_value = fields.IntField(default=0)
|
||||||
|
end_type = fields.CharField(max_length=50, default="people") # people/minutes/manual
|
||||||
|
end_value = fields.IntField(default=0)
|
||||||
|
status = fields.CharField(max_length=20, default="active")
|
||||||
|
winner_id = fields.BigIntField(null=True)
|
||||||
|
winner_name = fields.CharField(max_length=255, null=True)
|
||||||
|
ended_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "lottery_events"
|
||||||
|
indexes = (("chat_id", "status"), ("creator_id", "created_at"))
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryParticipant(TimestampMixin, Model):
|
||||||
|
"""抽奖参与者"""
|
||||||
|
id = fields.BigIntField(pk=True, generated=True)
|
||||||
|
event_id = fields.BigIntField()
|
||||||
|
user_id = fields.BigIntField()
|
||||||
|
username = fields.CharField(max_length=255, null=True)
|
||||||
|
first_name = fields.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "lottery_participants"
|
||||||
|
indexes = (("event_id", "user_id"),)
|
||||||
|
|||||||
@@ -292,6 +292,9 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
"✅ /checkin - 每日签到\n"
|
"✅ /checkin - 每日签到\n"
|
||||||
"🎮 /trivia - 知识答题\n"
|
"🎮 /trivia - 知识答题\n"
|
||||||
"🏆 /rank - 查看积分排行\n"
|
"🏆 /rank - 查看积分排行\n"
|
||||||
|
"🎁 /lottery - 发起群内抽奖\n"
|
||||||
|
" 格式:/lottery 奖品 | 参与条件 | 结束条件\n"
|
||||||
|
" 示例:/lottery 月卡一张 | all | people:3\n"
|
||||||
"🔗 /bind - 绑定账号\n\n"
|
"🔗 /bind - 绑定账号\n\n"
|
||||||
"管理员操作:\n"
|
"管理员操作:\n"
|
||||||
"🌙 /night on/off - 开启/关闭夜间模式\n"
|
"🌙 /night on/off - 开启/关闭夜间模式\n"
|
||||||
|
|||||||
+132
-14
@@ -9,7 +9,9 @@ from db.models import Action
|
|||||||
from services.game_service import (
|
from services.game_service import (
|
||||||
get_trivia_question, check_trivia_answer,
|
get_trivia_question, check_trivia_answer,
|
||||||
get_leaderboard, daily_checkin, get_scores_summary,
|
get_leaderboard, daily_checkin, get_scores_summary,
|
||||||
lottery_draw, SHOP_ITEMS, buy_shop_item,
|
SHOP_ITEMS, buy_shop_item,
|
||||||
|
parse_lottery_spec, create_lottery_event, lottery_condition_text, lottery_end_text,
|
||||||
|
count_lottery_participants, join_lottery_event, draw_lottery_event,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("spam_guard")
|
logger = logging.getLogger("spam_guard")
|
||||||
@@ -19,7 +21,7 @@ PANEL_IDLE_SECONDS = 60
|
|||||||
def panel_keyboard():
|
def panel_keyboard():
|
||||||
return InlineKeyboardMarkup([
|
return InlineKeyboardMarkup([
|
||||||
[InlineKeyboardButton("👤 我的信息", callback_data="panel:me"), InlineKeyboardButton("✅ 签到", callback_data="panel:checkin")],
|
[InlineKeyboardButton("👤 我的信息", callback_data="panel:me"), InlineKeyboardButton("✅ 签到", callback_data="panel:checkin")],
|
||||||
[InlineKeyboardButton("🎁 抽奖", callback_data="panel:lottery"), InlineKeyboardButton("🛒 商城", callback_data="panel:shop")],
|
[InlineKeyboardButton("🎁 发起抽奖", callback_data="panel:lottery"), InlineKeyboardButton("🛒 商城", callback_data="panel:shop")],
|
||||||
[InlineKeyboardButton("📖 帮助", callback_data="panel:help")],
|
[InlineKeyboardButton("📖 帮助", callback_data="panel:help")],
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -136,21 +138,90 @@ async def cmd_rank(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
asyncio.create_task(auto_delete(reply, seconds=60))
|
asyncio.create_task(auto_delete(reply, seconds=60))
|
||||||
|
|
||||||
|
|
||||||
async def cmd_lottery(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def build_lottery_text(event, count: int | None = None) -> str:
|
||||||
|
if count is None:
|
||||||
|
count = await count_lottery_participants(event.id)
|
||||||
|
return (
|
||||||
|
"🎁 群内抽奖\n"
|
||||||
|
"━━━━━━━━━━━━\n"
|
||||||
|
f"🎯 奖品:{event.prize}\n"
|
||||||
|
f"👤 发起人:@{event.creator_name or event.creator_id}\n"
|
||||||
|
f"✅ 参与条件:{await lottery_condition_text(event)}\n"
|
||||||
|
f"⏱️ 结束条件:{await lottery_end_text(event)}\n"
|
||||||
|
f"👥 当前参与:{count} 人\n\n"
|
||||||
|
"点击下方按钮参与抽奖。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lottery_keyboard(event_id: int) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup([[InlineKeyboardButton("🎁 参与抽奖", callback_data=f"lottery:join:{event_id}")]])
|
||||||
|
|
||||||
|
|
||||||
|
async def schedule_lottery_draw(context: ContextTypes.DEFAULT_TYPE, event_id: int, seconds: int):
|
||||||
|
async def _run():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(seconds)
|
||||||
|
result = await draw_lottery_event(event_id)
|
||||||
|
event = result.get("event")
|
||||||
|
if not event or not event.message_id:
|
||||||
|
return
|
||||||
|
if result.get("ok"):
|
||||||
|
winner = result["winner"]
|
||||||
|
text = (
|
||||||
|
"🎉 抽奖已开奖\n"
|
||||||
|
"━━━━━━━━━━━━\n"
|
||||||
|
f"🎯 奖品:{event.prize}\n"
|
||||||
|
f"👥 参与人数:{result['count']} 人\n"
|
||||||
|
f"🏆 中奖用户:@{winner.username or winner.first_name or winner.user_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = f"🎁 抽奖已结束\n━━━━━━━━━━━━\n🎯 奖品:{event.prize}\n❌ {result.get('reason', '未开奖')}"
|
||||||
|
await context.bot.edit_message_text(chat_id=event.chat_id, message_id=event.message_id, text=text)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ 自动开奖失败: {type(e).__name__}: {e}")
|
||||||
|
asyncio.create_task(_run())
|
||||||
|
|
||||||
|
|
||||||
|
async def create_lottery_from_text(update: Update, context: ContextTypes.DEFAULT_TYPE, spec_text: str):
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
result = await lottery_draw(user.id, user.username or user.first_name or str(user.id))
|
msg = update.effective_message
|
||||||
if not result["ok"]:
|
spec = parse_lottery_spec(spec_text)
|
||||||
reply = await update.message.reply_text(f"🎁 抽奖需要 {result.get('cost', 10)} 积分,当前积分不足。\n发送 /checkin 或 /trivia 先赚积分吧。")
|
event = await create_lottery_event(
|
||||||
|
chat_id=msg.chat_id,
|
||||||
|
creator_id=user.id,
|
||||||
|
creator_name=user.username or user.first_name or str(user.id),
|
||||||
|
spec=spec,
|
||||||
|
)
|
||||||
|
sent = await msg.reply_text(await build_lottery_text(event), reply_markup=lottery_keyboard(event.id))
|
||||||
|
event.message_id = sent.message_id
|
||||||
|
await event.save()
|
||||||
|
if event.end_type == "minutes":
|
||||||
|
await schedule_lottery_draw(context, event.id, event.end_value * 60)
|
||||||
|
return sent
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_lottery(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
parts = (update.message.text or "").split(maxsplit=1)
|
||||||
|
if len(parts) > 1:
|
||||||
|
try:
|
||||||
|
await create_lottery_from_text(update, context, parts[1])
|
||||||
|
except Exception as e:
|
||||||
|
reply = await update.message.reply_text(f"❌ 抽奖格式错误:{e}\n\n示例:/lottery 月卡一张 | all | people:3")
|
||||||
|
asyncio.create_task(auto_delete(reply, seconds=90))
|
||||||
else:
|
else:
|
||||||
|
context.user_data["awaiting_lottery_spec"] = True
|
||||||
reply = await update.message.reply_text(
|
reply = await update.message.reply_text(
|
||||||
"🎁 抽奖结果\n"
|
"🎁 发起群内抽奖\n"
|
||||||
f"🎯 {result['prize']}\n"
|
"请发送:奖品 | 参与条件 | 结束条件\n\n"
|
||||||
f"💸 消耗:{result['cost']} 分\n"
|
"示例:\n"
|
||||||
f"🎉 奖励:+{result['reward']} 分\n"
|
"月卡一张 | all | people:3\n"
|
||||||
f"📊 当前总积分:{result['total']} 分"
|
"邀请码 | checkin | minutes:10\n"
|
||||||
|
"徽章 | points:100 | people:5\n\n"
|
||||||
|
"参与条件:all / checkin / points:N\n"
|
||||||
|
"结束条件:people:N / minutes:N"
|
||||||
)
|
)
|
||||||
|
asyncio.create_task(auto_delete(reply, seconds=120))
|
||||||
asyncio.create_task(auto_delete(update.message, seconds=60))
|
asyncio.create_task(auto_delete(update.message, seconds=60))
|
||||||
asyncio.create_task(auto_delete(reply, seconds=60))
|
|
||||||
|
|
||||||
|
|
||||||
async def cmd_shop(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cmd_shop(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -200,8 +271,14 @@ async def handle_panel_callback(update: Update, context: ContextTypes.DEFAULT_TY
|
|||||||
result = await daily_checkin(user.id, user.username or user.first_name or str(user.id))
|
result = await daily_checkin(user.id, user.username or user.first_name or str(user.id))
|
||||||
text = "❌ 你今天已经签到过了" if not result["ok"] else f"✅ 签到成功!奖励 +{result['bonus']} 分,连续 {result['streak']} 天"
|
text = "❌ 你今天已经签到过了" if not result["ok"] else f"✅ 签到成功!奖励 +{result['bonus']} 分,连续 {result['streak']} 天"
|
||||||
elif action == "lottery":
|
elif action == "lottery":
|
||||||
result = await lottery_draw(user.id, user.username or user.first_name or str(user.id))
|
context.user_data["awaiting_lottery_spec"] = True
|
||||||
text = f"❌ {result.get('reason')},抽奖需要 {result.get('cost', 10)} 分" if not result["ok"] else f"🎁 {result['prize']}\n当前总积分:{result['total']} 分"
|
text = (
|
||||||
|
"🎁 发起群内抽奖\n"
|
||||||
|
"请在群里发送:奖品 | 参与条件 | 结束条件\n\n"
|
||||||
|
"示例:月卡一张 | all | people:3\n"
|
||||||
|
"参与条件:all / checkin / points:N\n"
|
||||||
|
"结束条件:people:N / minutes:N"
|
||||||
|
)
|
||||||
elif action == "shop":
|
elif action == "shop":
|
||||||
summary = await get_scores_summary(user.id)
|
summary = await get_scores_summary(user.id)
|
||||||
text = f"🛒 积分商城\n当前总积分:{summary['total']} 分\n" + "\n".join([f"{k}. {v['name']} — {v['cost']} 分" for k, v in SHOP_ITEMS.items()]) + "\n\n购买请发送 /buy 商品编号"
|
text = f"🛒 积分商城\n当前总积分:{summary['total']} 分\n" + "\n".join([f"{k}. {v['name']} — {v['cost']} 分" for k, v in SHOP_ITEMS.items()]) + "\n\n购买请发送 /buy 商品编号"
|
||||||
@@ -224,6 +301,16 @@ async def handle_trivia_answer(update: Update, context: ContextTypes.DEFAULT_TYP
|
|||||||
if not msg or not msg.text or not user or user.is_bot:
|
if not msg or not msg.text or not user or user.is_bot:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if context.user_data.get("awaiting_lottery_spec"):
|
||||||
|
try:
|
||||||
|
await create_lottery_from_text(update, context, msg.text.strip())
|
||||||
|
context.user_data.pop("awaiting_lottery_spec", None)
|
||||||
|
asyncio.create_task(auto_delete(msg, seconds=60))
|
||||||
|
except Exception as e:
|
||||||
|
reply = await msg.reply_text(f"❌ 抽奖格式错误:{e}\n请重新发送,或发送 /panel 重新开始。")
|
||||||
|
asyncio.create_task(auto_delete(reply, seconds=90))
|
||||||
|
raise ApplicationHandlerStop
|
||||||
|
|
||||||
correct_answer = context.user_data.get("trivia_answer")
|
correct_answer = context.user_data.get("trivia_answer")
|
||||||
options = context.user_data.get("trivia_options") or []
|
options = context.user_data.get("trivia_options") or []
|
||||||
if not correct_answer:
|
if not correct_answer:
|
||||||
@@ -259,6 +346,36 @@ async def handle_trivia_answer(update: Update, context: ContextTypes.DEFAULT_TYP
|
|||||||
raise ApplicationHandlerStop
|
raise ApplicationHandlerStop
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lottery_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
parts = query.data.split(":")
|
||||||
|
event_id = int(parts[-1])
|
||||||
|
user = query.from_user
|
||||||
|
result = await join_lottery_event(event_id, user.id, user.username or "", user.first_name or "")
|
||||||
|
if not result["ok"]:
|
||||||
|
await query.answer(result["reason"], show_alert=True)
|
||||||
|
return
|
||||||
|
event = result["event"]
|
||||||
|
if result["should_draw"]:
|
||||||
|
draw = await draw_lottery_event(event.id)
|
||||||
|
if draw.get("ok"):
|
||||||
|
winner = draw["winner"]
|
||||||
|
text = (
|
||||||
|
"🎉 抽奖已开奖\n"
|
||||||
|
"━━━━━━━━━━━━\n"
|
||||||
|
f"🎯 奖品:{event.prize}\n"
|
||||||
|
f"👥 参与人数:{draw['count']} 人\n"
|
||||||
|
f"🏆 中奖用户:@{winner.username or winner.first_name or winner.user_id}"
|
||||||
|
)
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
else:
|
||||||
|
await query.edit_message_text(f"🎁 抽奖已结束\n❌ {draw.get('reason', '未开奖')}")
|
||||||
|
else:
|
||||||
|
await query.edit_message_text(await build_lottery_text(event, result["count"]), reply_markup=lottery_keyboard(event.id))
|
||||||
|
await query.answer(f"参与成功,当前 {result['count']} 人")
|
||||||
|
|
||||||
|
|
||||||
async def auto_delete(message, seconds=60):
|
async def auto_delete(message, seconds=60):
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(seconds)
|
await asyncio.sleep(seconds)
|
||||||
@@ -277,5 +394,6 @@ def register_fun(app):
|
|||||||
app.add_handler(CommandHandler("buy", cmd_buy, filters.ChatType.GROUPS))
|
app.add_handler(CommandHandler("buy", cmd_buy, filters.ChatType.GROUPS))
|
||||||
app.add_handler(CommandHandler("panel", cmd_panel, filters.ChatType.GROUPS))
|
app.add_handler(CommandHandler("panel", cmd_panel, filters.ChatType.GROUPS))
|
||||||
app.add_handler(CallbackQueryHandler(handle_panel_callback, pattern=r"^panel:"))
|
app.add_handler(CallbackQueryHandler(handle_panel_callback, pattern=r"^panel:"))
|
||||||
|
app.add_handler(CallbackQueryHandler(handle_lottery_callback, pattern=r"^lottery:"))
|
||||||
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS & ~filters.COMMAND, handle_trivia_answer), group=-1)
|
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS & ~filters.COMMAND, handle_trivia_answer), group=-1)
|
||||||
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS, handle_trigger), group=2)
|
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS, handle_trigger), group=2)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ async def start_tg_bot():
|
|||||||
BotCommand("stats", "📊 今日统计"),
|
BotCommand("stats", "📊 今日统计"),
|
||||||
BotCommand("checkin", "✅ 每日签到"),
|
BotCommand("checkin", "✅ 每日签到"),
|
||||||
BotCommand("trivia", "🎮 知识答题"),
|
BotCommand("trivia", "🎮 知识答题"),
|
||||||
BotCommand("lottery", "🎁 积分抽奖"),
|
BotCommand("lottery", "🎁 发起群内抽奖"),
|
||||||
BotCommand("shop", "🛒 积分商城"),
|
BotCommand("shop", "🛒 积分商城"),
|
||||||
BotCommand("panel", "🎛️ 操作面板"),
|
BotCommand("panel", "🎛️ 操作面板"),
|
||||||
BotCommand("bind", "🔗 绑定账号"),
|
BotCommand("bind", "🔗 绑定账号"),
|
||||||
|
|||||||
+131
-2
@@ -1,9 +1,9 @@
|
|||||||
"""小游戏服务"""
|
"""小游戏服务"""
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from db.models import GameScore
|
from db.models import GameScore, LotteryEvent, LotteryParticipant
|
||||||
|
|
||||||
logger = logging.getLogger("spam_guard")
|
logger = logging.getLogger("spam_guard")
|
||||||
|
|
||||||
@@ -127,3 +127,132 @@ async def buy_shop_item(user_id: int, item_id: str) -> dict:
|
|||||||
return {"ok": False, "reason": "积分不足", "item": item}
|
return {"ok": False, "reason": "积分不足", "item": item}
|
||||||
summary = await get_scores_summary(user_id)
|
summary = await get_scores_summary(user_id)
|
||||||
return {"ok": True, "item": item, "total": summary["total"]}
|
return {"ok": True, "item": item, "total": summary["total"]}
|
||||||
|
|
||||||
|
|
||||||
|
LOTTERY_CONDITION_HELP = "参与条件:all=所有人;checkin=今日已签到;points:N=总积分不少于 N"
|
||||||
|
LOTTERY_END_HELP = "结束条件:people:N=满 N 人开奖;minutes:N=N 分钟后开奖;manual=手动开奖(预留)"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lottery_condition(raw: str) -> tuple[str, int, str]:
|
||||||
|
text = (raw or "all").strip().lower()
|
||||||
|
if text in {"all", "所有人", "不限"}:
|
||||||
|
return "all", 0, "所有人可参与"
|
||||||
|
if text in {"checkin", "签到", "今日签到"}:
|
||||||
|
return "checkin", 0, "今日已签到用户"
|
||||||
|
if text.startswith("points:") or text.startswith("积分:"):
|
||||||
|
value = int(text.split(":", 1)[1])
|
||||||
|
return "min_points", value, f"总积分 ≥ {value}"
|
||||||
|
raise ValueError(LOTTERY_CONDITION_HELP)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lottery_end(raw: str) -> tuple[str, int, str]:
|
||||||
|
text = (raw or "people:3").strip().lower()
|
||||||
|
if text.startswith("people:") or text.startswith("人数:"):
|
||||||
|
value = max(1, int(text.split(":", 1)[1]))
|
||||||
|
return "people", value, f"满 {value} 人自动开奖"
|
||||||
|
if text.startswith("minutes:") or text.startswith("分钟:"):
|
||||||
|
value = max(1, int(text.split(":", 1)[1]))
|
||||||
|
return "minutes", value, f"{value} 分钟后自动开奖"
|
||||||
|
if text in {"manual", "手动"}:
|
||||||
|
return "manual", 0, "手动开奖"
|
||||||
|
raise ValueError(LOTTERY_END_HELP)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lottery_spec(text: str) -> dict:
|
||||||
|
parts = [p.strip() for p in text.replace("|", "|").split("|")]
|
||||||
|
if len(parts) != 3:
|
||||||
|
raise ValueError("格式:奖品 | 参与条件 | 结束条件")
|
||||||
|
prize = parts[0]
|
||||||
|
if not prize:
|
||||||
|
raise ValueError("奖品不能为空")
|
||||||
|
condition_type, condition_value, condition_text = parse_lottery_condition(parts[1])
|
||||||
|
end_type, end_value, end_text = parse_lottery_end(parts[2])
|
||||||
|
return {
|
||||||
|
"prize": prize,
|
||||||
|
"condition_type": condition_type,
|
||||||
|
"condition_value": condition_value,
|
||||||
|
"condition_text": condition_text,
|
||||||
|
"end_type": end_type,
|
||||||
|
"end_value": end_value,
|
||||||
|
"end_text": end_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_lottery_event(chat_id: int, creator_id: int, creator_name: str, spec: dict) -> LotteryEvent:
|
||||||
|
return await LotteryEvent.create(
|
||||||
|
chat_id=chat_id,
|
||||||
|
creator_id=creator_id,
|
||||||
|
creator_name=creator_name,
|
||||||
|
prize=spec["prize"],
|
||||||
|
condition_type=spec["condition_type"],
|
||||||
|
condition_value=spec["condition_value"],
|
||||||
|
end_type=spec["end_type"],
|
||||||
|
end_value=spec["end_value"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def lottery_condition_text(event: LotteryEvent) -> str:
|
||||||
|
if event.condition_type == "checkin":
|
||||||
|
return "今日已签到用户"
|
||||||
|
if event.condition_type == "min_points":
|
||||||
|
return f"总积分 ≥ {event.condition_value}"
|
||||||
|
return "所有人可参与"
|
||||||
|
|
||||||
|
|
||||||
|
async def lottery_end_text(event: LotteryEvent) -> str:
|
||||||
|
if event.end_type == "people":
|
||||||
|
return f"满 {event.end_value} 人自动开奖"
|
||||||
|
if event.end_type == "minutes":
|
||||||
|
return f"{event.end_value} 分钟后自动开奖"
|
||||||
|
return "手动开奖"
|
||||||
|
|
||||||
|
|
||||||
|
async def count_lottery_participants(event_id: int) -> int:
|
||||||
|
return await LotteryParticipant.filter(event_id=event_id).count()
|
||||||
|
|
||||||
|
|
||||||
|
async def check_lottery_eligible(event: LotteryEvent, user_id: int) -> tuple[bool, str]:
|
||||||
|
if event.condition_type == "checkin":
|
||||||
|
score = await GameScore.filter(user_id=user_id, game_type="checkin").first()
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
if not score or not score.last_played or score.last_played.strftime("%Y-%m-%d") != today:
|
||||||
|
return False, "需要今日已签到后才能参与"
|
||||||
|
elif event.condition_type == "min_points":
|
||||||
|
summary = await get_scores_summary(user_id)
|
||||||
|
if summary["total"] < event.condition_value:
|
||||||
|
return False, f"总积分需要 ≥ {event.condition_value},当前 {summary['total']}"
|
||||||
|
return True, "ok"
|
||||||
|
|
||||||
|
|
||||||
|
async def join_lottery_event(event_id: int, user_id: int, username: str = "", first_name: str = "") -> dict:
|
||||||
|
event = await LotteryEvent.filter(id=event_id).first()
|
||||||
|
if not event or event.status != "active":
|
||||||
|
return {"ok": False, "reason": "抽奖不存在或已结束"}
|
||||||
|
ok, reason = await check_lottery_eligible(event, user_id)
|
||||||
|
if not ok:
|
||||||
|
return {"ok": False, "reason": reason}
|
||||||
|
exists = await LotteryParticipant.filter(event_id=event_id, user_id=user_id).exists()
|
||||||
|
if exists:
|
||||||
|
return {"ok": False, "reason": "你已经参与过了"}
|
||||||
|
await LotteryParticipant.create(event_id=event_id, user_id=user_id, username=username, first_name=first_name)
|
||||||
|
count = await count_lottery_participants(event_id)
|
||||||
|
return {"ok": True, "event": event, "count": count, "should_draw": event.end_type == "people" and count >= event.end_value}
|
||||||
|
|
||||||
|
|
||||||
|
async def draw_lottery_event(event_id: int) -> dict:
|
||||||
|
event = await LotteryEvent.filter(id=event_id).first()
|
||||||
|
if not event or event.status != "active":
|
||||||
|
return {"ok": False, "reason": "抽奖不存在或已结束"}
|
||||||
|
participants = await LotteryParticipant.filter(event_id=event_id).all()
|
||||||
|
if not participants:
|
||||||
|
event.status = "cancelled"
|
||||||
|
event.ended_at = datetime.now()
|
||||||
|
await event.save()
|
||||||
|
return {"ok": False, "reason": "无人参与,抽奖已取消", "event": event}
|
||||||
|
winner = random.choice(participants)
|
||||||
|
event.status = "ended"
|
||||||
|
event.winner_id = winner.user_id
|
||||||
|
event.winner_name = winner.username or winner.first_name or str(winner.user_id)
|
||||||
|
event.ended_at = datetime.now()
|
||||||
|
await event.save()
|
||||||
|
return {"ok": True, "event": event, "winner": winner, "count": len(participants)}
|
||||||
|
|||||||
Reference in New Issue
Block a user