diff --git a/app/services/dailyreward.py b/app/services/dailyreward.py index e1f91ca..2e000a3 100644 --- a/app/services/dailyreward.py +++ b/app/services/dailyreward.py @@ -1,86 +1,81 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo from app.db.database import users_collection, db coins_sessions_collection = db.coins_sessions +TZ = ZoneInfo("Asia/Yekaterinburg") + +def _day_bounds_utc(now_utc: datetime): + now_local = now_utc.astimezone(TZ) + start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) + start_utc = start_local.astimezone(timezone.utc) + next_utc = (start_local + timedelta(days=1)).astimezone(timezone.utc) + return now_local.date(), start_utc, next_utc, start_local + class DailyRewardService: async def claim_daily(self, username: str) -> dict: - now = datetime.utcnow() - today = now.date() + now_utc = datetime.now(timezone.utc) + today_local, start_today_utc, start_tomorrow_utc, _ = _day_bounds_utc(now_utc) user = await users_collection.find_one({"username": username}) if not user: return {"claimed": False, "reason": "user_not_found"} - last_claim_at = user.get("daily_last_claim_at") - last_claim_day = last_claim_at.date() if last_claim_at else None + last_claim_at = user.get("daily_last_claim_at") # ожидаем datetime (лучше хранить UTC) + last_local_day = last_claim_at.replace(tzinfo=timezone.utc).astimezone(TZ).date() if last_claim_at else None - # уже получал сегодня - if last_claim_day == today: + if last_local_day == today_local: return {"claimed": False, "reason": "already_claimed_today", "streak": user.get("daily_streak", 0)} - # вычисляем новый стрик - yesterday = today - timedelta(days=1) + yesterday_local = today_local - timedelta(days=1) prev_streak = int(user.get("daily_streak", 0) or 0) + new_streak = (prev_streak + 1) if (last_local_day == yesterday_local) else 1 - if last_claim_day == yesterday: - new_streak = prev_streak + 1 - else: - new_streak = 1 + # твоя новая формула: 10, 20, 30 ... до 50 (кап) + reward = min(10 + (new_streak - 1) * 10, 50) - # формула награды (пример) - base = 10 - bonus = min(new_streak, 7) * 2 # кап на 7 дней - reward = base + bonus - - # КЛЮЧЕВО: атомарная защита от двойного клика/параллельных запросов - # обновляем только если last_claim_day != today result = await users_collection.update_one( { "username": username, "$or": [ {"daily_last_claim_at": {"$exists": False}}, - {"daily_last_claim_at": {"$lt": datetime(today.year, today.month, today.day)}}, + {"daily_last_claim_at": {"$lt": start_today_utc.replace(tzinfo=None)}}, ], }, { "$inc": {"coins": reward}, - "$set": {"daily_last_claim_at": now, "daily_streak": new_streak}, + "$set": {"daily_last_claim_at": now_utc.replace(tzinfo=None), "daily_streak": new_streak}, }, ) if result.modified_count == 0: - # кто-то уже успел получить сегодня (гонка) user2 = await users_collection.find_one({"username": username}) return {"claimed": False, "reason": "already_claimed_today", "streak": user2.get("daily_streak", 0)} - # лог в coins_sessions (у тебя уже так сделано для coins_update) :contentReference[oaicite:4]{index=4} await coins_sessions_collection.insert_one({ "player_name": username, "update_type": "daily_login", - "timestamp": now, + "timestamp": now_utc.replace(tzinfo=None), "coins_added": reward, "streak": new_streak, }) return {"claimed": True, "coins_added": reward, "streak": new_streak} - + async def get_status(self, username: str) -> dict: - now = datetime.utcnow() - today = now.date() + now_utc = datetime.now(timezone.utc) + today_local, start_today_utc, start_tomorrow_utc, start_today_local = _day_bounds_utc(now_utc) user = await users_collection.find_one({"username": username}) if not user: return {"ok": False, "reason": "user_not_found"} last_claim_at = user.get("daily_last_claim_at") - last_claim_day = last_claim_at.date() if last_claim_at else None + last_local_day = last_claim_at.replace(tzinfo=timezone.utc).astimezone(TZ).date() if last_claim_at else None - start_of_today = datetime(today.year, today.month, today.day) - start_of_tomorrow = start_of_today + timedelta(days=1) - - can_claim = (last_claim_day != today) - seconds_to_next = 0 if can_claim else int((start_of_tomorrow - now).total_seconds()) + can_claim = (last_local_day != today_local) + seconds_to_next = 0 if can_claim else int((start_tomorrow_utc - now_utc).total_seconds()) if seconds_to_next < 0: seconds_to_next = 0 @@ -88,6 +83,8 @@ class DailyRewardService: "ok": True, "can_claim": can_claim, "seconds_to_next": seconds_to_next, - "next_claim_at": start_of_tomorrow.isoformat() + "Z", + # можно отдавать и UTC, и локальное время для UI: + "next_claim_at_utc": start_tomorrow_utc.isoformat().replace("+00:00", "Z"), + "next_claim_at_local": (start_today_local + timedelta(days=1)).isoformat(), "streak": int(user.get("daily_streak", 0) or 0), - } + } \ No newline at end of file