diff --git a/.env.example b/.env.example index c4a18d99..83e497d0 100644 --- a/.env.example +++ b/.env.example @@ -214,7 +214,7 @@ TRIAL_ACTIVATION_PRICE=0 # Сколько устройств доступно по дефолту при покупке платной подписки DEFAULT_DEVICE_LIMIT=3 -# Максимум устройств достопных к покупке (0 = Нет лимита) +# Максимум устройств доступных к покупке (0 = Нет лимита) MAX_DEVICES_LIMIT=15 # Дефолт параметры для подписок выданных через админку diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index b0387631..d5995957 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -36,15 +36,15 @@ jobs: TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🏷️ Собираем релизную версию: $VERSION" elif [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION="v3.0.0-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🚀 Собираем версию из main: $VERSION" elif [[ $GITHUB_REF == refs/heads/dev ]]; then - VERSION="v3.0.0-dev-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-dev-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🧪 Собираем dev версию: $VERSION" else - VERSION="v3.0.0-pr-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-pr-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)" echo "🔀 Собираем PR версию: $VERSION" fi diff --git a/.github/workflows/docker-registry.yml b/.github/workflows/docker-registry.yml index 22b7e48e..c2b9aa2b 100644 --- a/.github/workflows/docker-registry.yml +++ b/.github/workflows/docker-registry.yml @@ -49,13 +49,13 @@ jobs: VERSION=${GITHUB_REF#refs/tags/} echo "🏷️ Building release version: $VERSION" elif [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION="v3.0.0-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-$(git rev-parse --short HEAD)" echo "🚀 Building main version: $VERSION" elif [[ $GITHUB_REF == refs/heads/dev ]]; then - VERSION="v3.0.0-dev-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-dev-$(git rev-parse --short HEAD)" echo "🧪 Building dev version: $VERSION" else - VERSION="v3.0.0-pr-$(git rev-parse --short HEAD)" + VERSION="v3.1.0-pr-$(git rev-parse --short HEAD)" echo "🔀 Building PR version: $VERSION" fi echo "version=$VERSION" >> $GITHUB_OUTPUT diff --git a/Dockerfile b/Dockerfile index 7b563c2f..54a85def 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \ FROM python:3.13-slim -ARG VERSION="v3.0.0" +ARG VERSION="v3.1.0" ARG BUILD_DATE ARG VCS_REF diff --git a/app/cabinet/routes/contests.py b/app/cabinet/routes/contests.py index 5fff0120..1217404f 100644 --- a/app/cabinet/routes/contests.py +++ b/app/cabinet/routes/contests.py @@ -42,7 +42,8 @@ class ContestInfo(BaseModel): slug: str name: str description: Optional[str] = None - prize_days: int + prize_type: str + prize_value: str is_available: bool already_played: bool = False @@ -65,7 +66,8 @@ class ContestResult(BaseModel): """Result of contest attempt.""" is_winner: bool message: str - prize_days: Optional[int] = None + prize_type: Optional[str] = None + prize_value: Optional[str] = None # ============ Helpers ============ @@ -80,19 +82,47 @@ def _user_allowed(subscription) -> bool: } -async def _award_prize(db: AsyncSession, user_id: int, prize_days: int) -> str: +async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_value: str) -> str: """Award prize to winner.""" - subscription = await get_subscription_by_user_id(db, user_id) - if not subscription: - return "Error: subscription not found" + if prize_type == "days": + try: + days = int(prize_value) + except ValueError: + return "Error: invalid prize value" - subscription.end_date = subscription.end_date + timedelta(days=prize_days) - subscription.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(subscription) + subscription = await get_subscription_by_user_id(db, user_id) + if not subscription: + return "Error: subscription not found" - logger.info(f"🎁 Extended subscription for user {user_id} by {prize_days} days (contest prize)") - return f"Subscription extended by {prize_days} days" + subscription.end_date = subscription.end_date + timedelta(days=days) + subscription.updated_at = datetime.utcnow() + await db.commit() + await db.refresh(subscription) + + logger.info(f"🎁 Extended subscription for user {user_id} by {days} days (contest prize)") + return f"Subscription extended by {days} days" + + elif prize_type == "balance": + from app.database.crud.user import get_user_by_id + try: + amount = float(prize_value) + except ValueError: + return "Error: invalid prize value" + + user = await get_user_by_id(db, user_id) + if not user: + return "Error: user not found" + + user.balance += amount + await db.commit() + await db.refresh(user) + + logger.info(f"🎁 Added {amount} to balance for user {user_id} (contest prize)") + return f"Balance increased by {amount}" + + else: + logger.warning(f"Unknown prize type: {prize_type}") + return f"Prize type '{prize_type}' not supported" # ============ Routes ============ @@ -169,7 +199,8 @@ async def get_contests( slug=tpl_slug, name=rnd.template.name if rnd.template else tpl_slug, description=rnd.template.description if rnd.template else None, - prize_days=rnd.template.prize_days if rnd.template else 0, + prize_type=rnd.template.prize_type if rnd.template else "days", + prize_value=rnd.template.prize_value if rnd.template else "1", is_available=True, already_played=attempt is not None, )) @@ -368,11 +399,12 @@ async def submit_contest_answer( if is_winner: await increment_winner_count(db, round_obj) - prize_text = await _award_prize(db, user.id, tpl.prize_days) + prize_text = await _award_prize(db, user.id, tpl.prize_type, tpl.prize_value) return ContestResult( is_winner=True, message=f"🎉 Congratulations! You won! {prize_text}", - prize_days=tpl.prize_days, + prize_type=tpl.prize_type, + prize_value=tpl.prize_value, ) else: lose_messages = { diff --git a/app/handlers/admin/daily_contests.py b/app/handlers/admin/daily_contests.py index 88d7506b..ac1b4e9c 100644 --- a/app/handlers/admin/daily_contests.py +++ b/app/handlers/admin/daily_contests.py @@ -194,8 +194,8 @@ async def manual_start_round( return # Проверяем, есть ли уже активный раунд для этого шаблона - from app.database.crud.contest import get_active_rounds - exists = await get_active_rounds(db, tpl.id) + from app.database.crud.contest import get_active_round_by_template + exists = await get_active_round_by_template(db, tpl.id) if exists: await callback.answer(texts.t("ADMIN_ROUND_ALREADY_ACTIVE", "Раунд уже активен."), show_alert=True) await show_daily_contest(callback, db_user, db) diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index 33650263..128e91bf 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -571,11 +571,14 @@ def get_daily_contest_manage_keyboard( InlineKeyboardButton(text=_t(texts, "ADMIN_CONTEST_START_MANUAL", "🧪 Ручной старт"), callback_data=f"admin_daily_manual_{template_id}"), ], [ - InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_PRIZE", "🏅 Приз (дни)"), callback_data=f"admin_daily_edit_{template_id}_prize_days"), - InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_MAX_WINNERS", "👥 Победителей"), callback_data=f"admin_daily_edit_{template_id}_max_winners"), + InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_PRIZE_TYPE", "🏅 Тип приза"), callback_data=f"admin_daily_edit_{template_id}_prize_type"), + InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_PRIZE_VALUE", "💰 Значение приза"), callback_data=f"admin_daily_edit_{template_id}_prize_value"), ], [ + InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_MAX_WINNERS", "👥 Победителей"), callback_data=f"admin_daily_edit_{template_id}_max_winners"), InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_ATTEMPTS", "🔁 Попытки"), callback_data=f"admin_daily_edit_{template_id}_attempts_per_user"), + ], + [ InlineKeyboardButton(text=_t(texts, "ADMIN_EDIT_TIMES", "⏰ Раундов/день"), callback_data=f"admin_daily_edit_{template_id}_times_per_day"), ], [ diff --git a/docs/contests-api.md b/docs/contests-api.md index 29eb071a..8ca05102 100644 --- a/docs/contests-api.md +++ b/docs/contests-api.md @@ -6,7 +6,7 @@ - `GET /contests/daily/templates?enabled_only=false` — список шаблонов игр. - `GET /contests/daily/templates/{id}` — получить шаблон. -- `PATCH /contests/daily/templates/{id}` — обновить поля: `name`, `description`, `prize_days`, `max_winners`, `attempts_per_user`, `times_per_day`, `schedule_times`, `cooldown_hours`, `payload` (dict), `is_enabled`. +- `PATCH /contests/daily/templates/{id}` — обновить поля: `name`, `description`, `prize_type`, `prize_value`, `max_winners`, `attempts_per_user`, `times_per_day`, `schedule_times`, `cooldown_hours`, `payload` (dict), `is_enabled`. - `POST /contests/daily/templates/{id}/start-round` — запустить раунд вручную. Тело: ```json {