- Оспаривания теперь требуют решения админа после 24ч голосования - Можно повторно оспаривать после разрешённых споров - Исправлены бонусные очки при перепрохождении после оспаривания - Сброс серии при невалидном пруфе - Колесо показывает только доступные игры - Rate limiting только через backend (RATE_LIMIT_ENABLED)
19 KiB
Система оспаривания (Disputes)
Система оспаривания позволяет участникам марафона проверять доказательства (пруфы) выполненных заданий друг друга и голосовать за их валидность.
Общий принцип работы
┌──────────────────────────────────────────────────────────────────────────┐
│ ЖИЗНЕННЫЙ ЦИКЛ ДИСПУТА │
└──────────────────────────────────────────────────────────────────────────┘
Участник A Участник B Все участники
выполняет задание замечает проблему голосуют
│ │ │
▼ ▼ ▼
┌───────────┐ 24 часа ┌───────────┐ 24 часа ┌───────────┐
│ Завершено │ ─────────────────▶ │ Оспорено │ ─────────────▶ │ Решено │
│ │ окно оспаривания │ (OPEN) │ голосование │ │
└───────────┘ └───────────┘ └───────────┘
│ │ │
│ │ ├──▶ VALID (пруф OK)
│ │ │ Задание остаётся
│ │ │
│ │ └──▶ INVALID (пруф не OK)
│ │ Задание возвращается
│ │
└──────────────────────────────────┘
Если не оспорено — задание засчитано
Кто может оспаривать
| Условие | Можно оспорить? |
|---|---|
| Своё задание | ❌ Нельзя |
| Чужое задание (статус COMPLETED) | ✅ Можно (в течение 24 часов) |
| Чужое задание (статус ACTIVE/DROPPED) | ❌ Нельзя |
| Прошло более 24 часов с момента выполнения | ❌ Нельзя |
| Уже есть активный диспут на это задание | ❌ Нельзя |
Типы оспариваемых заданий
1. Обычные челленджи
Можно оспорить выполнение любого челленджа. При признании пруфа невалидным:
- Задание переходит в статус
RETURNED - Очки снимаются с участника
- Участник должен переделать задание
2. Прохождения игр (Playthrough)
Основное задание прохождения можно оспорить. При признании невалидным:
- Основное задание переходит в статус
RETURNED - Очки снимаются
- Все бонусные челленджи сбрасываются в статус
PENDING
3. Бонусные челленджи
Каждый бонусный челлендж можно оспорить отдельно. При признании невалидным:
- Только этот бонусный челлендж сбрасывается в
PENDING - Участник может переделать его
- Основное задание и другие бонусы не затрагиваются
Важно: Очки за бонусные челленджи начисляются только при завершении основного задания. Поэтому при оспаривании бонуса очки не снимаются — просто сбрасывается статус.
Процесс голосования
Создание диспута
- Участник нажимает "Оспорить" на странице деталей задания
- Вводит причину оспаривания (минимум 10 символов)
- Создаётся диспут со статусом
OPEN - Владельцу задания отправляется уведомление в Telegram
Голосование
- Любой участник марафона может голосовать
- Два варианта: "Валидно" (пруф OK) или "Невалидно" (пруф не OK)
- Можно изменить свой голос до завершения голосования
- Голосование длится 24 часа с момента создания диспута
Комментарии
- Участники могут оставлять комментарии для обсуждения
- Комментарии помогают другим участникам принять решение
- Комментарии доступны только пока диспут открыт
Разрешение диспута
Автоматическое (по таймеру)
Через 24 часа диспут автоматически разрешается:
- Система подсчитывает голоса
- При равенстве голосов — в пользу обвиняемого (пруф валиден)
- Результат:
RESOLVED_VALIDилиRESOLVED_INVALID
Технически: Фоновый планировщик (DisputeScheduler) проверяет истёкшие диспуты каждые 5 минут.
Результаты
| Результат | Условие | Последствия |
|---|---|---|
RESOLVED_VALID |
Голосов "валидно" ≥ голосов "невалидно" | Задание остаётся выполненным |
RESOLVED_INVALID |
Голосов "невалидно" > голосов "валидно" | Задание возвращается |
Что происходит при INVALID
Для обычного задания:
- Статус →
RETURNED - Очки (
points_earned) вычитаются из общего счёта участника - Пруфы сохраняются для истории
Для прохождения:
- Основное задание →
RETURNED - Очки вычитаются
- Все бонусные челленджи сбрасываются:
- Статус →
PENDING - Пруфы удаляются
- Очки обнуляются
- Статус →
Для бонусного челленджа:
- Только этот бонус →
PENDING - Пруфы удаляются
- Можно переделать
API эндпоинты
Создание диспута
POST /api/v1/assignments/{assignment_id}/dispute
POST /api/v1/bonus-assignments/{bonus_id}/dispute
Body: { "reason": "Описание проблемы с пруфом..." }
Голосование
POST /api/v1/disputes/{dispute_id}/vote
Body: { "vote": true } // true = валидно, false = невалидно
Комментарии
POST /api/v1/disputes/{dispute_id}/comments
Body: { "text": "Текст комментария" }
Получение информации
GET /api/v1/assignments/{assignment_id}
// В ответе включено поле dispute с полной информацией:
{
"dispute": {
"id": 1,
"status": "open",
"reason": "...",
"votes_valid": 3,
"votes_invalid": 2,
"my_vote": true,
"expires_at": "2024-12-30T12:00:00Z",
"comments": [...],
"votes": [...]
}
}
Структура базы данных
Таблица disputes
| Поле | Тип | Описание |
|---|---|---|
id |
INT | PK |
assignment_id |
INT | FK → assignments (nullable для бонусов) |
bonus_assignment_id |
INT | FK → bonus_assignments (nullable для основных) |
raised_by_id |
INT | FK → users |
reason |
TEXT | Причина оспаривания |
status |
VARCHAR(20) | open / valid / invalid |
created_at |
DATETIME | Время создания |
resolved_at |
DATETIME | Время разрешения |
Ограничение: Либо assignment_id, либо bonus_assignment_id должен быть заполнен (не оба).
Таблица dispute_votes
| Поле | Тип | Описание |
|---|---|---|
id |
INT | PK |
dispute_id |
INT | FK → disputes |
user_id |
INT | FK → users |
vote |
BOOLEAN | true = валидно, false = невалидно |
created_at |
DATETIME | Время голоса |
Ограничение: Один голос на участника (UNIQUE dispute_id + user_id).
Таблица dispute_comments
| Поле | Тип | Описание |
|---|---|---|
id |
INT | PK |
dispute_id |
INT | FK → disputes |
user_id |
INT | FK → users |
text |
TEXT | Текст комментария |
created_at |
DATETIME | Время комментария |
UI компоненты
Кнопка "Оспорить"
Появляется на странице деталей задания (/assignments/{id}) если:
- Статус задания:
COMPLETED - Это не своё задание
- Прошло меньше 24 часов с момента выполнения
- Нет активного диспута
Секция диспута
Показывается если есть активный или завершённый диспут:
- Статус (открыт / валиден / невалиден)
- Таймер до окончания (для открытых)
- Причина оспаривания
- Кнопки голосования с счётчиками
- Секция комментариев
Для бонусных челленджей
На каждом бонусном челлендже:
- Маленькая кнопка "Оспорить" (если можно)
- Бейдж статуса диспута
- Компактное голосование прямо в карточке бонуса
Уведомления
Telegram уведомления
| Событие | Получатель | Сообщение |
|---|---|---|
| Создание диспута | Владелец задания | "Ваше задание X оспорено в марафоне Y" |
| Результат: валидно | Владелец задания | "Диспут по заданию X решён в вашу пользу" |
| Результат: невалидно | Владелец задания | "Диспут по заданию X решён не в вашу пользу, задание возвращено" |
Конфигурация
# backend/app/api/v1/assignments.py
DISPUTE_WINDOW_HOURS = 24 # Окно для создания диспута
# backend/app/services/dispute_scheduler.py
CHECK_INTERVAL_SECONDS = 300 # Проверка каждые 5 минут
DISPUTE_WINDOW_HOURS = 24 # Время голосования
Пример сценария
Сценарий 1: Успешное оспаривание
- Иван выполняет челлендж "Пройти уровень без смертей"
- Иван прикладывает скриншот финального экрана
- Петр открывает детали задания и видит, что на скриншоте есть смерти
- Петр нажимает "Оспорить" и пишет: "На скриншоте видно 3 смерти"
- Участники марафона голосуют: 5 за "невалидно", 2 за "валидно"
- Через 24 часа диспут закрывается как
RESOLVED_INVALID - Задание Ивана возвращается, очки снимаются
- Иван получает уведомление и должен переделать задание
Сценарий 2: Оспаривание бонуса
- Анна проходит игру и выполняет бонусный челлендж
- Сергей замечает проблему с пруфом бонуса
- Сергей оспаривает только бонусный челлендж
- Голосование: 4 за "невалидно", 1 за "валидно"
- Результат: бонус сбрасывается в
PENDING - Основное задание Анны не затронуто
- Анна может переделать бонус (пока основное задание активно)
Ручное разрешение диспутов
Администраторы системы и организаторы марафонов могут вручную разрешать диспуты, не дожидаясь окончания 24-часового окна голосования.
Кто может разрешать
| Роль | Доступ |
|---|---|
| Системный админ | Все диспуты во всех марафонах (/admin/disputes) |
| Организатор марафона | Только диспуты в своём марафоне (секция "Оспаривания" на странице марафона) |
Интерфейс для системных админов
Путь: /admin/disputes
- Отдельная страница в админ-панели
- Фильтры: "Открытые" / "Все"
- Показывает диспуты из всех марафонов
- Информация: марафон, задание, участник, кто оспорил, причина
- Счётчик голосов и время до истечения
- Кнопки "Валидно" / "Невалидно" для мгновенного решения
Интерфейс для организаторов
Путь: На странице марафона (/marathons/{id}) → секция "Оспаривания"
- Доступна только организаторам активного марафона
- Показывает только диспуты текущего марафона
- Компактный вид с возможностью раскрытия
- Ссылка на страницу задания для детального просмотра
API для ручного разрешения
Системные админы:
GET /api/v1/admin/disputes?status_filter=open|all
POST /api/v1/admin/disputes/{dispute_id}/resolve
Body: { "is_valid": true|false }
Организаторы марафона:
GET /api/v1/marathons/{marathon_id}/disputes?status_filter=open|all
POST /api/v1/marathons/{marathon_id}/disputes/{dispute_id}/resolve
Body: { "is_valid": true|false }
Что происходит при ручном разрешении
Логика идентична автоматическому разрешению:
При is_valid: true:
- Диспут закрывается как
RESOLVED_VALID - Задание остаётся выполненным
- Участник получает уведомление
При is_valid: false:
- Диспут закрывается как
RESOLVED_INVALID - Задание возвращается, очки снимаются
- Участник получает уведомление
Важно: логика снятия очков за бонусы
При отклонении бонусного диспута система проверяет статус основного прохождения:
┌─────────────────────────────────────────────────────────────────┐
│ БОНУС ПРИЗНАН НЕВАЛИДНЫМ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Основное прохождение Основное прохождение │
│ НЕ завершено? УЖЕ завершено? │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Просто │ │ Вычитаем │ │
│ │ сбросить │ │ очки из │ │
│ │ бонус │ │ участника │ │
│ └───────────┘ └───────────┘ │
│ (очки ещё не (очки уже были │
│ были начислены) начислены при │
│ завершении прохождения) │
└─────────────────────────────────────────────────────────────────┘
Почему так? Очки за бонусные челленджи начисляются только в момент завершения основного прохождения (чтобы нельзя было получить очки за бонусы и потом дропнуть основное задание).
Логирование действий
Ручное разрешение диспутов логируется в системе:
| Действие | Тип лога |
|---|---|
| Админ подтвердил пруф | DISPUTE_RESOLVE_VALID |
| Админ отклонил пруф | DISPUTE_RESOLVE_INVALID |
Логи доступны в /admin/logs для аудита действий администраторов.