Files
game-marathon/docs/disputes.md
mamonov.ep 89dbe2c018 Улучшение системы оспариваний и исправления
- Оспаривания теперь требуют решения админа после 24ч голосования
  - Можно повторно оспаривать после разрешённых споров
  - Исправлены бонусные очки при перепрохождении после оспаривания
  - Сброс серии при невалидном пруфе
  - Колесо показывает только доступные игры
  - Rate limiting только через backend (RATE_LIMIT_ENABLED)
2025-12-29 22:23:34 +03:00

19 KiB
Raw Permalink Blame History

Система оспаривания (Disputes)

Система оспаривания позволяет участникам марафона проверять доказательства (пруфы) выполненных заданий друг друга и голосовать за их валидность.

Общий принцип работы

┌──────────────────────────────────────────────────────────────────────────┐
│                          ЖИЗНЕННЫЙ ЦИКЛ ДИСПУТА                          │
└──────────────────────────────────────────────────────────────────────────┘

  Участник A                        Участник B                    Все участники
  выполняет задание                 замечает проблему             голосуют
       │                                  │                            │
       ▼                                  ▼                            ▼
 ┌───────────┐    24 часа          ┌───────────┐    24 часа     ┌───────────┐
 │ Завершено │ ─────────────────▶  │ Оспорено  │ ─────────────▶ │ Решено    │
 │           │   окно оспаривания  │ (OPEN)    │   голосование  │           │
 └───────────┘                     └───────────┘                └───────────┘
       │                                  │                            │
       │                                  │                            ├──▶ VALID (пруф OK)
       │                                  │                            │    Задание остаётся
       │                                  │                            │
       │                                  │                            └──▶ INVALID (пруф не OK)
       │                                  │                                 Задание возвращается
       │                                  │
       └──────────────────────────────────┘
              Если не оспорено — задание засчитано

Кто может оспаривать

Условие Можно оспорить?
Своё задание Нельзя
Чужое задание (статус COMPLETED) Можно (в течение 24 часов)
Чужое задание (статус ACTIVE/DROPPED) Нельзя
Прошло более 24 часов с момента выполнения Нельзя
Уже есть активный диспут на это задание Нельзя

Типы оспариваемых заданий

1. Обычные челленджи

Можно оспорить выполнение любого челленджа. При признании пруфа невалидным:

  • Задание переходит в статус RETURNED
  • Очки снимаются с участника
  • Участник должен переделать задание

2. Прохождения игр (Playthrough)

Основное задание прохождения можно оспорить. При признании невалидным:

  • Основное задание переходит в статус RETURNED
  • Очки снимаются
  • Все бонусные челленджи сбрасываются в статус PENDING

3. Бонусные челленджи

Каждый бонусный челлендж можно оспорить отдельно. При признании невалидным:

  • Только этот бонусный челлендж сбрасывается в PENDING
  • Участник может переделать его
  • Основное задание и другие бонусы не затрагиваются

Важно: Очки за бонусные челленджи начисляются только при завершении основного задания. Поэтому при оспаривании бонуса очки не снимаются — просто сбрасывается статус.

Процесс голосования

Создание диспута

  1. Участник нажимает "Оспорить" на странице деталей задания
  2. Вводит причину оспаривания (минимум 10 символов)
  3. Создаётся диспут со статусом OPEN
  4. Владельцу задания отправляется уведомление в Telegram

Голосование

  • Любой участник марафона может голосовать
  • Два варианта: "Валидно" (пруф OK) или "Невалидно" (пруф не OK)
  • Можно изменить свой голос до завершения голосования
  • Голосование длится 24 часа с момента создания диспута

Комментарии

  • Участники могут оставлять комментарии для обсуждения
  • Комментарии помогают другим участникам принять решение
  • Комментарии доступны только пока диспут открыт

Разрешение диспута

Автоматическое (по таймеру)

Через 24 часа диспут автоматически разрешается:

  • Система подсчитывает голоса
  • При равенстве голосов — в пользу обвиняемого (пруф валиден)
  • Результат: RESOLVED_VALID или RESOLVED_INVALID

Технически: Фоновый планировщик (DisputeScheduler) проверяет истёкшие диспуты каждые 5 минут.

Результаты

Результат Условие Последствия
RESOLVED_VALID Голосов "валидно" ≥ голосов "невалидно" Задание остаётся выполненным
RESOLVED_INVALID Голосов "невалидно" > голосов "валидно" Задание возвращается

Что происходит при INVALID

Для обычного задания:

  1. Статус → RETURNED
  2. Очки (points_earned) вычитаются из общего счёта участника
  3. Пруфы сохраняются для истории

Для прохождения:

  1. Основное задание → RETURNED
  2. Очки вычитаются
  3. Все бонусные челленджи сбрасываются:
    • Статус → PENDING
    • Пруфы удаляются
    • Очки обнуляются

Для бонусного челленджа:

  1. Только этот бонус → PENDING
  2. Пруфы удаляются
  3. Можно переделать

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: Успешное оспаривание

  1. Иван выполняет челлендж "Пройти уровень без смертей"
  2. Иван прикладывает скриншот финального экрана
  3. Петр открывает детали задания и видит, что на скриншоте есть смерти
  4. Петр нажимает "Оспорить" и пишет: "На скриншоте видно 3 смерти"
  5. Участники марафона голосуют: 5 за "невалидно", 2 за "валидно"
  6. Через 24 часа диспут закрывается как RESOLVED_INVALID
  7. Задание Ивана возвращается, очки снимаются
  8. Иван получает уведомление и должен переделать задание

Сценарий 2: Оспаривание бонуса

  1. Анна проходит игру и выполняет бонусный челлендж
  2. Сергей замечает проблему с пруфом бонуса
  3. Сергей оспаривает только бонусный челлендж
  4. Голосование: 4 за "невалидно", 1 за "валидно"
  5. Результат: бонус сбрасывается в PENDING
  6. Основное задание Анны не затронуто
  7. Анна может переделать бонус (пока основное задание активно)

Ручное разрешение диспутов

Администраторы системы и организаторы марафонов могут вручную разрешать диспуты, не дожидаясь окончания 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 для аудита действий администраторов.