"""小游戏服务""" import random import logging from datetime import datetime, timedelta from db.models import GameScore, LotteryEvent, LotteryParticipant, ShopItem, ShopTransaction from tortoise.transactions import in_transaction from tortoise.exceptions import IntegrityError logger = logging.getLogger("spam_guard") TRIVIA_QUESTIONS = [ {"q": "收割机项目的 GitHub 地址是?", "a": "github.com/ngfchl/go-harvest", "opts": ["github.com/ngfchl/go-harvest", "github.com/ngfchl/harvest", "github.com/harvest/go-harvest"]}, {"q": "以下哪个不是 PT 站的常见等级?", "a": "Diamond", "opts": ["User", "Power User", "Diamond", "Elite"]}, {"q": "Docker 默认的日志驱动是?", "a": "json-file", "opts": ["json-file", "syslog", "journald", "fluentd"]}, ] SHOP_ITEMS = { "1": {"name": "补签卡", "cost": 50, "desc": "预留权益:后续可用于补签"}, "2": {"name": "群内称号券", "cost": 80, "desc": "预留权益:联系管理员兑换称号"}, "3": {"name": "好运盲盒", "cost": 30, "desc": "兑换后记录一次好运盲盒"}, } async def get_or_create_score(chat_id: int, user_id: int, username: str, game_type: str) -> GameScore: score, _ = await GameScore.get_or_create( chat_id=chat_id, user_id=user_id, game_type=game_type, defaults={"username": username}, ) if username and score.username != username: score.username = username await score.save() return score async def get_trivia_question() -> dict: return random.choice(TRIVIA_QUESTIONS) async def check_trivia_answer(chat_id: int, user_id: int, username: str, answer: str, correct_answer: str) -> dict: score = await get_or_create_score(chat_id, user_id, username, "trivia") score.total += 1 is_correct = answer.strip().lower() in correct_answer.strip().lower() if is_correct: score.score += 10 score.wins += 1 score.streak += 1 if score.streak > 1: score.score += 5 else: score.streak = 0 score.last_played = datetime.now() await score.save() return {"correct": is_correct, "score": score.score, "streak": score.streak} async def get_leaderboard(chat_id: int, game_type: str = "trivia", limit: int = 10) -> list: return await GameScore.filter(chat_id=chat_id, game_type=game_type).order_by("-score").limit(limit).all() async def daily_checkin(chat_id: int, user_id: int, username: str) -> dict: score = await get_or_create_score(chat_id, user_id, username, "checkin") today = datetime.now().strftime("%Y-%m-%d") if score.last_played and score.last_played.strftime("%Y-%m-%d") == today: return {"ok": False, "reason": "already_checked", "score": score.score, "streak": score.streak} score.total += 1 base = random.randint(5, 12) streak_bonus = min(score.streak + 1, 7) bonus = base + streak_bonus score.score += bonus score.streak += 1 score.wins += 1 score.last_played = datetime.now() await score.save() return {"ok": True, "score": score.score, "streak": score.streak, "bonus": bonus, "base": base, "streak_bonus": streak_bonus} async def get_scores_summary(user_id: int, chat_id: int = 0) -> dict: scores = await GameScore.filter(chat_id=chat_id, user_id=user_id).all() total = sum((s.score or 0) for s in scores) by_type = {s.game_type: s for s in scores} return {"scores": scores, "total": total, "by_type": by_type} async def _deduct_points_locked(conn, chat_id: int, user_id: int, cost: int) -> bool: scores = await GameScore.filter(chat_id=chat_id, user_id=user_id).using_db(conn).select_for_update().all() total = sum((s.score or 0) for s in scores) if total < cost: return False remaining = cost for score in sorted(scores, key=lambda x: x.score or 0, reverse=True): if remaining <= 0: break take = min(score.score or 0, remaining) score.score -= take remaining -= take await score.save(using_db=conn) return True async def deduct_points(chat_id: int, user_id: int, cost: int) -> bool: async with in_transaction() as conn: return await _deduct_points_locked(conn, chat_id, user_id, cost) async def lottery_draw(chat_id: int, user_id: int, username: str) -> dict: cost = 10 if not await deduct_points(chat_id, user_id, cost): return {"ok": False, "reason": "积分不足", "cost": cost} roll = random.random() reward = 0 prize = "谢谢参与" if roll < 0.08: reward, prize = 60, "一等奖 +60" elif roll < 0.25: reward, prize = 25, "二等奖 +25" elif roll < 0.55: reward, prize = 12, "三等奖 +12" score = await get_or_create_score(chat_id, user_id, username, "lottery") score.total += 1 if reward: score.wins += 1 score.score += reward score.last_played = datetime.now() await score.save() summary = await get_scores_summary(user_id, chat_id) return {"ok": True, "cost": cost, "reward": reward, "prize": prize, "total": summary["total"]} async def list_shop_items(chat_id: int, include_disabled: bool = False) -> list[dict]: rows = await ShopItem.filter(chat_id=chat_id).order_by("item_key").all() if not rows and chat_id != 0: rows = await ShopItem.filter(chat_id=0).order_by("item_key").all() if not rows: return [{"item_key": k, "name": v["name"], "cost": v["cost"], "stock": -1, "desc": v["desc"], "enabled": True} for k, v in SHOP_ITEMS.items()] result = [] for r in rows: if include_disabled or r.enabled: result.append({"id": r.id, "chat_id": r.chat_id, "item_key": r.item_key, "name": r.name, "cost": r.cost, "stock": r.stock, "desc": r.desc, "enabled": r.enabled}) return result async def upsert_shop_item(chat_id: int, item_key: str, name: str, cost: int, desc: str = "", enabled: bool = True, stock: int = -1) -> ShopItem: item, _ = await ShopItem.get_or_create(chat_id=chat_id, item_key=item_key, defaults={"name": name, "cost": cost, "desc": desc, "enabled": enabled, "stock": stock}) item.name = name item.cost = cost item.stock = stock item.desc = desc item.enabled = enabled await item.save() return item async def delete_shop_item(chat_id: int, item_key: str) -> bool: deleted = await ShopItem.filter(chat_id=chat_id, item_key=item_key).delete() return bool(deleted) async def list_shop_transactions(chat_id: int, limit: int = 100) -> list[dict]: rows = await ShopTransaction.filter(chat_id=chat_id).order_by("-created_at").limit(limit).all() return [{"id": r.id, "chat_id": r.chat_id, "user_id": r.user_id, "username": r.username, "item_key": r.item_key, "item_name": r.item_name, "cost": r.cost, "status": r.status, "note": r.note, "created_at": r.created_at} for r in rows] async def buy_shop_item(chat_id: int, user_id: int, item_id: str, username: str = "") -> dict: async with in_transaction() as conn: db_item = await ShopItem.filter(chat_id=chat_id, item_key=str(item_id), enabled=True).using_db(conn).select_for_update().first() if not db_item: # 兼容默认内置商品:没有数据库记录时视为无限库存。 builtin = SHOP_ITEMS.get(str(item_id)) if not builtin: return {"ok": False, "reason": "商品不存在"} item = {"item_key": str(item_id), "name": builtin["name"], "cost": builtin["cost"], "stock": -1, "desc": builtin["desc"], "enabled": True} else: item = {"id": db_item.id, "chat_id": db_item.chat_id, "item_key": db_item.item_key, "name": db_item.name, "cost": db_item.cost, "stock": db_item.stock, "desc": db_item.desc, "enabled": db_item.enabled} if db_item.stock == 0: return {"ok": False, "reason": "库存不足", "item": item} if not await _deduct_points_locked(conn, chat_id, user_id, int(item["cost"])): return {"ok": False, "reason": "积分不足", "item": item} if db_item and db_item.stock > 0: db_item.stock -= 1 await db_item.save(using_db=conn, update_fields=["stock", "updated_at"]) item["stock"] = db_item.stock await ShopTransaction.create( chat_id=chat_id, user_id=user_id, username=username, item_key=str(item.get("item_key")), item_name=item.get("name") or "", cost=int(item.get("cost") or 0), status="success", using_db=conn, ) summary = await get_scores_summary(user_id, chat_id) 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 in {"instant", "即开", "即开即中"}: return "instant", 1, "即开即中:首位参与者立即中奖" 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 "time", value, f"{value} 分钟后自动开奖" if text.startswith("time:") or text.startswith("时间:"): return "time_at", 0, f"指定时间开奖:{raw.split(':', 1)[1].strip()}" 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, title=spec.get("title") or spec["prize"], description=spec.get("description") or "", prize=spec["prize"], condition_type=spec["condition_type"], condition_value=spec["condition_value"], draw_type=spec.get("draw_type") or spec["end_type"], draw_at=spec.get("draw_at"), max_participants=spec.get("max_participants") or (spec["end_value"] if spec["end_type"] == "people" else 0), 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 in {"min_points", "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(chat_id=event.chat_id, 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 in {"min_points", "points"}: summary = await get_scores_summary(user_id, event.chat_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: async with in_transaction() as conn: event = await LotteryEvent.filter(id=event_id).using_db(conn).select_for_update().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).using_db(conn).exists() if exists: return {"ok": False, "reason": "你已经参与过了"} is_instant = event.draw_type == "instant" or event.end_type == "instant" if (not is_instant) and event.end_type == "people" and int(event.end_value or 0) > 0: current_count = await LotteryParticipant.filter(event_id=event_id).using_db(conn).count() if current_count >= int(event.end_value or 0): return {"ok": False, "reason": "参与人数已满,等待开奖"} if is_instant and int(event.remaining_count or 0) <= 0: event.status = "ended" event.ended_at = datetime.now() await event.save(using_db=conn) return {"ok": False, "reason": "奖品已抽完,活动已结束"} try: participant = await LotteryParticipant.create(event_id=event_id, user_id=user_id, username=username, first_name=first_name, is_winner=is_instant, using_db=conn) except IntegrityError: return {"ok": False, "reason": "你已经参与过了"} count = await LotteryParticipant.filter(event_id=event_id).using_db(conn).count() won = False ended = False if is_instant: won = True event.remaining_count = max(0, int(event.remaining_count or 0) - 1) event.winner_id = user_id event.winner_name = username or first_name or str(user_id) if event.remaining_count <= 0: event.status = "ended" event.ended_at = datetime.now() ended = True await event.save(using_db=conn) return {"ok": True, "event": event, "participant": participant, "count": count, "won": won, "ended": ended, "should_draw": (not is_instant) and event.end_type == "people" and count >= event.end_value} def lottery_event_payload(event: LotteryEvent | None) -> dict | None: if not event: return None return { "id": event.id, "chat_id": event.chat_id, "title": event.title, "prize": event.prize, "description": event.description, "condition_type": event.condition_type, "condition_value": event.condition_value, "end_type": event.end_type, "end_value": event.end_value, "draw_at": event.draw_at, "prize_count": event.prize_count, "remaining_count": event.remaining_count, "status": event.status, "winner_id": event.winner_id, "winner_name": event.winner_name, "ended_at": event.ended_at, "created_at": event.created_at, "updated_at": event.updated_at, } def lottery_participant_payload(p: LotteryParticipant | None) -> dict | None: if not p: return None return { "id": p.id, "event_id": p.event_id, "user_id": p.user_id, "username": p.username, "first_name": p.first_name, "is_winner": getattr(p, "is_winner", False), "created_at": p.created_at, "updated_at": p.updated_at, } async def draw_lottery_event(event_id: int) -> dict: async with in_transaction() as conn: event = await LotteryEvent.filter(id=event_id).using_db(conn).select_for_update().first() if not event or event.status != "active": return {"ok": False, "reason": "抽奖不存在或已结束"} participants = await LotteryParticipant.filter(event_id=event_id).using_db(conn).all() if not participants: event.status = "cancelled" event.ended_at = datetime.now() await event.save(using_db=conn) return {"ok": False, "reason": "无人参与,抽奖已取消", "event": lottery_event_payload(event)} winner = random.choice(participants) winner.is_winner = True await winner.save(using_db=conn) 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(using_db=conn) return {"ok": True, "event": lottery_event_payload(event), "winner": lottery_participant_payload(winner), "count": len(participants)}