challenge-admin-pl/docs/stats-v2-api.md

24 KiB
Raw Permalink Blame History

API Статистики v2 - Документация для Frontend

Обзор

Эндпоинт /challenge/stats/v2 предоставляет расширенную статистику системы проверки заданий с детальными данными для построения таблиц и прогресс-баров.

Эндпоинт

GET /challenge/stats/v2

Аутентификация

Не требуется.

Параметры запроса

Параметр Тип Обязательный Описание
chainId string Нет ID цепочки для фильтрации статистики. Если указан, возвращается статистика только по заданиям из этой цепочки

Примеры использования

Получить полную статистику:

GET /challenge/stats/v2

Получить статистику только по одной цепочке:

GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439031

Структура ответа

{
  success: true,
  body: {
    // ========== БАЗОВАЯ СТАТИСТИКА (из v1) ==========
    users: number,              // Общее количество пользователей
    tasks: number,              // Общее количество заданий
    chains: number,             // Общее количество цепочек
    submissions: {
      total: number,            // Всего попыток
      accepted: number,         // Принятых
      rejected: number,         // Отклоненных
      pending: number,          // Ожидающих проверки
      inProgress: number        // В процессе проверки
    },
    averageCheckTimeMs: number, // Среднее время проверки в мс
    queue: {
      queueLength: number,
      waiting: number,
      inProgress: number,
      maxConcurrency: number,
      currentlyProcessing: number
    },
    
    // ========== НОВЫЕ ДАННЫЕ V2 ==========
    
    // Таблица заданий с детальной статистикой
    tasksTable: Array<{
      taskId: string,
      title: string,
      totalAttempts: number,              // Всего попыток по всем пользователям
      uniqueUsers: number,                // Количество уникальных пользователей
      acceptedCount: number,              // Количество успешных прохождений
      successRate: number,                // Процент успешного прохождения (0-100)
      averageAttemptsToSuccess: number    // Среднее количество попыток до успеха
    }>,
    
    // Активные участники с прогрессом
    activeParticipants: Array<{
      userId: string,
      nickname: string,
      totalSubmissions: number,           // Всего попыток пользователя
      completedTasks: number,             // Завершенных заданий
      chainProgress: Array<{              // Прогресс по каждой цепочке
        chainId: string,
        chainName: string,
        totalTasks: number,               // Всего заданий в цепочке
        completedTasks: number,           // Завершено заданий
        progressPercent: number           // Процент прохождения (0-100)
      }>
    }>,
    
    // Детальная информация по цепочкам
    chainsDetailed: Array<{
      chainId: string,
      name: string,
      totalTasks: number,
      tasks: Array<{                      // Список заданий в цепочке
        taskId: string,
        title: string,
        description: string
      }>,
      participantProgress: Array<{        // Прогресс каждого участника
        userId: string,
        nickname: string,
        taskProgress: Array<{             // Статус по каждому заданию
          taskId: string,
          taskTitle: string,
          status: 'not_started' | 'pending' | 'in_progress' | 'needs_revision' | 'completed'
        }>,
        completedCount: number,           // Завершено заданий
        progressPercent: number           // Процент прохождения (0-100)
      }>
    }>
  }
}

Пример ответа

{
  "success": true,
  "body": {
    "users": 34,
    "tasks": 22,
    "chains": 2,
    "submissions": {
      "total": 39,
      "accepted": 16,
      "rejected": 23,
      "pending": 0,
      "inProgress": 0
    },
    "averageCheckTimeMs": 2043,
    "queue": {
      "queueLength": 0,
      "waiting": 0,
      "inProgress": 0,
      "maxConcurrency": 1,
      "currentlyProcessing": 0
    },
    "tasksTable": [
      {
        "taskId": "507f1f77bcf86cd799439011",
        "title": "Создание REST API",
        "totalAttempts": 45,
        "uniqueUsers": 12,
        "acceptedCount": 8,
        "successRate": 67,
        "averageAttemptsToSuccess": 2.3
      },
      {
        "taskId": "507f1f77bcf86cd799439012",
        "title": "Работа с базой данных",
        "totalAttempts": 38,
        "uniqueUsers": 10,
        "acceptedCount": 6,
        "successRate": 60,
        "averageAttemptsToSuccess": 3.1
      }
    ],
    "activeParticipants": [
      {
        "userId": "507f1f77bcf86cd799439021",
        "nickname": "student1",
        "totalSubmissions": 15,
        "completedTasks": 5,
        "chainProgress": [
          {
            "chainId": "507f1f77bcf86cd799439031",
            "chainName": "Основы Backend",
            "totalTasks": 10,
            "completedTasks": 5,
            "progressPercent": 50
          },
          {
            "chainId": "507f1f77bcf86cd799439032",
            "chainName": "Frontend разработка",
            "totalTasks": 8,
            "completedTasks": 0,
            "progressPercent": 0
          }
        ]
      }
    ],
    "chainsDetailed": [
      {
        "chainId": "507f1f77bcf86cd799439031",
        "name": "Основы Backend",
        "totalTasks": 10,
        "tasks": [
          {
            "taskId": "507f1f77bcf86cd799439011",
            "title": "Создание REST API",
            "description": "Создайте простой REST API с использованием Express.js"
          },
          {
            "taskId": "507f1f77bcf86cd799439012",
            "title": "Работа с базой данных",
            "description": "Интегрируйте MongoDB в ваше приложение"
          }
        ],
        "participantProgress": [
          {
            "userId": "507f1f77bcf86cd799439021",
            "nickname": "student1",
            "taskProgress": [
              {
                "taskId": "507f1f77bcf86cd799439011",
                "taskTitle": "Создание REST API",
                "status": "completed"
              },
              {
                "taskId": "507f1f77bcf86cd799439012",
                "taskTitle": "Работа с базой данных",
                "status": "needs_revision"
              }
            ],
            "completedCount": 1,
            "progressPercent": 10
          }
        ]
      }
    ]
  }
}

Фильтрация по цепочке

При передаче параметра chainId:

  1. tasksTable - содержит только задания из указанной цепочки
  2. activeParticipants - включает только участников, которые делали попытки по заданиям этой цепочки. В chainProgress будет информация только об указанной цепочке
  3. chainsDetailed - содержит информацию только об указанной цепочке

Пример фильтрованного ответа

curl http://localhost:3000/challenge/stats/v2?chainId=507f1f77bcf86cd799439031
{
  "success": true,
  "body": {
    "users": 34,
    "tasks": 22,
    "chains": 2,
    "submissions": {
      "total": 39,
      "accepted": 16,
      "rejected": 23,
      "pending": 0,
      "inProgress": 0
    },
    "averageCheckTimeMs": 2043,
    "queue": { ... },
    "tasksTable": [
      // Только задания из цепочки 507f1f77bcf86cd799439031
      {
        "taskId": "507f1f77bcf86cd799439011",
        "title": "Создание REST API",
        "totalAttempts": 45,
        "uniqueUsers": 12,
        "acceptedCount": 8,
        "successRate": 67,
        "averageAttemptsToSuccess": 2.3
      }
    ],
    "activeParticipants": [
      // Только участники с попытками в этой цепочке
      {
        "userId": "507f1f77bcf86cd799439021",
        "nickname": "student1",
        "totalSubmissions": 10,
        "completedTasks": 3,
        "chainProgress": [
          // Только одна цепочка
          {
            "chainId": "507f1f77bcf86cd799439031",
            "chainName": "Основы Backend",
            "totalTasks": 10,
            "completedTasks": 3,
            "progressPercent": 30
          }
        ]
      }
    ],
    "chainsDetailed": [
      // Только указанная цепочка
      {
        "chainId": "507f1f77bcf86cd799439031",
        "name": "Основы Backend",
        "totalTasks": 10,
        "tasks": [...],
        "participantProgress": [...]
      }
    ]
  }
}

Использование в UI компонентах

1. Таблица заданий

Используйте tasksTable для отображения статистики по каждому заданию:

// React пример
function TasksTable({ tasksTable }) {
  return (
    <table>
      <thead>
        <tr>
          <th>Название задания</th>
          <th>Попыток</th>
          <th>Уникальных пользователей</th>
          <th>Успешно завершено</th>
          <th>% успеха</th>
          <th>Среднее попыток до успеха</th>
        </tr>
      </thead>
      <tbody>
        {tasksTable.map(task => (
          <tr key={task.taskId}>
            <td>{task.title}</td>
            <td>{task.totalAttempts}</td>
            <td>{task.uniqueUsers}</td>
            <td>{task.acceptedCount}</td>
            <td>{task.successRate}%</td>
            <td>{task.averageAttemptsToSuccess.toFixed(1)}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

2. Прогресс-бары участников

Используйте activeParticipants для отображения прогресса каждого участника:

// React пример
function ParticipantProgress({ activeParticipants }) {
  return (
    <div>
      {activeParticipants.map(participant => (
        <div key={participant.userId} className="participant-card">
          <h3>{participant.nickname}</h3>
          <p>Завершено заданий: {participant.completedTasks}</p>
          <p>Всего попыток: {participant.totalSubmissions}</p>
          
          <div className="chain-progress">
            {participant.chainProgress.map(chain => (
              <div key={chain.chainId} className="chain-item">
                <div className="chain-header">
                  <span>{chain.chainName}</span>
                  <span>{chain.completedTasks}/{chain.totalTasks}</span>
                </div>
                <div className="progress-bar">
                  <div 
                    className="progress-fill" 
                    style={{ width: `${chain.progressPercent}%` }}
                  />
                </div>
                <span className="progress-text">{chain.progressPercent}%</span>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

3. Детальный прогресс по цепочкам

Используйте chainsDetailed для отображения детального прогресса всех участников в рамках каждой цепочки:

// React пример
function ChainDetailedView({ chainsDetailed }) {
  return (
    <div>
      {chainsDetailed.map(chain => (
        <div key={chain.chainId} className="chain-detailed">
          <h2>{chain.name}</h2>
          <p>Заданий в цепочке: {chain.totalTasks}</p>
          
          {/* Список заданий */}
          <div className="tasks-list">
            <h3>Задания:</h3>
            {chain.tasks.map(task => (
              <div key={task.taskId} className="task-item">
                <h4>{task.title}</h4>
                <p>{task.description}</p>
              </div>
            ))}
          </div>
          
          {/* Прогресс участников */}
          <div className="participants-progress">
            <h3>Прогресс участников:</h3>
            <table>
              <thead>
                <tr>
                  <th>Участник</th>
                  {chain.tasks.map(task => (
                    <th key={task.taskId}>{task.title}</th>
                  ))}
                  <th>Прогресс</th>
                </tr>
              </thead>
              <tbody>
                {chain.participantProgress.map(participant => (
                  <tr key={participant.userId}>
                    <td>{participant.nickname}</td>
                    {participant.taskProgress.map(taskProg => (
                      <td key={taskProg.taskId}>
                        <StatusBadge status={taskProg.status} />
                      </td>
                    ))}
                    <td>{participant.progressPercent}%</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      ))}
    </div>
  );
}

// Компонент для отображения статуса
function StatusBadge({ status }) {
  const statusConfig = {
    'not_started': { label: 'Не начато', color: 'gray' },
    'pending': { label: 'Ожидает', color: 'yellow' },
    'in_progress': { label: 'В процессе', color: 'blue' },
    'needs_revision': { label: 'Доработка', color: 'orange' },
    'completed': { label: 'Завершено', color: 'green' }
  };
  
  const config = statusConfig[status];
  
  return (
    <span className={`badge badge-${config.color}`}>
      {config.label}
    </span>
  );
}

Рекомендации по использованию

Производительность

  • Эндпоинт выполняет множество агрегаций, поэтому может работать медленно при большом количестве данных
  • Рекомендуется кэшировать результат на клиенте на 30-60 секунд
  • Используйте loading индикаторы во время загрузки данных
// Пример с кэшированием
function useStatsV2(chainId?: string) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const lastFetch = useRef(0);
  
  const fetchStats = async (force = false) => {
    const now = Date.now();
    // Кэш на 60 секунд
    if (!force && data && (now - lastFetch.current) < 60000) {
      return data;
    }
    
    setLoading(true);
    try {
      const url = chainId 
        ? `/challenge/stats/v2?chainId=${chainId}`
        : '/challenge/stats/v2';
      const response = await fetch(url);
      const result = await response.json();
      if (result.success) {
        setData(result.body);
        lastFetch.current = now;
      }
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };
  
  useEffect(() => {
    fetchStats();
  }, [chainId]); // Перезагружаем при смене цепочки
  
  return { data, loading, error, refetch: () => fetchStats(true) };
}

// Использование
function ChainStatistics() {
  const [selectedChainId, setSelectedChainId] = useState<string | undefined>();
  const { data, loading } = useStatsV2(selectedChainId);
  
  if (loading) return <div>Загрузка...</div>;
  
  return (
    <div>
      <select 
        value={selectedChainId || ''} 
        onChange={(e) => setSelectedChainId(e.target.value || undefined)}
      >
        <option value="">Все цепочки</option>
        {data?.chainsDetailed.map(chain => (
          <option key={chain.chainId} value={chain.chainId}>
            {chain.name}
          </option>
        ))}
      </select>
      
      <TasksTable tasksTable={data?.tasksTable} />
      <ParticipantProgress activeParticipants={data?.activeParticipants} />
    </div>
  );
}

Фильтрация и сортировка

Все массивы данных можно фильтровать и сортировать на клиенте:

// Сортировка таблицы заданий по проценту успеха
const sortedTasks = [...tasksTable].sort((a, b) => b.successRate - a.successRate);

// Фильтрация активных участников с прогрессом > 50%
const activeStudents = activeParticipants.filter(p => 
  p.chainProgress.some(c => c.progressPercent > 50)
);

// Поиск участника по имени
const searchParticipant = (query) => 
  activeParticipants.filter(p => 
    p.nickname.toLowerCase().includes(query.toLowerCase())
  );

Визуализация данных

Для построения графиков и диаграмм используйте библиотеки типа:

  • Chart.js / Recharts - для графиков прогресса
  • AG Grid / TanStack Table - для таблиц с сортировкой
  • React Progress Bar - для прогресс-баров
// Пример с Chart.js
import { Bar } from 'react-chartjs-2';

function TasksSuccessChart({ tasksTable }) {
  const data = {
    labels: tasksTable.map(t => t.title),
    datasets: [{
      label: 'Процент успеха',
      data: tasksTable.map(t => t.successRate),
      backgroundColor: 'rgba(75, 192, 192, 0.6)',
    }]
  };
  
  return <Bar data={data} />;
}

Когда использовать фильтрацию по цепочке

Используйте chainId когда:

  1. Страница отдельной цепочки - пользователь просматривает детали конкретной цепочки

    function ChainDetailsPage({ chainId }) {
      const { data } = useStatsV2(chainId);
      return <ChainDashboard data={data} />;
    }
    
  2. Улучшение производительности - когда нужны данные только по одной цепочке, фильтрация на сервере работает быстрее

  3. Фокус на конкретной программе - преподаватель хочет видеть прогресс по конкретному курсу/модулю

НЕ используйте chainId когда:

  1. Общий дашборд - нужна статистика по всем цепочкам
  2. Сравнение цепочек - нужно показать метрики по всем цепочкам одновременно
  3. Общая аналитика - нужны агрегированные данные по всей системе

Различия между v1 и v2

Параметр v1 (/stats) v2 (/stats/v2) v2 с chainId
Базовая статистика
Таблица заданий (все) (фильтр)
Прогресс участников (все) (фильтр)
Детальный прогресс по цепочкам (все) (одна)
Статистика по попыткам Общая Детальная Детальная (фильтр)
Скорость работы Быстро Медленнее Средняя

Обработка ошибок

async function fetchStatsV2(chainId?: string) {
  try {
    const url = chainId 
      ? `/challenge/stats/v2?chainId=${chainId}`
      : '/challenge/stats/v2';
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    
    if (!data.success) {
      throw new Error(data.error?.message || 'Unknown error');
    }
    
    return data.body;
  } catch (error) {
    console.error('Failed to fetch stats v2:', error);
    
    // Специальная обработка для неверного chainId
    if (error.message.includes('Chain not found')) {
      showNotification('Цепочка не найдена', 'error');
      // Перенаправить на страницу всех цепочек
      window.location.href = '/chains';
    } else {
      showNotification('Не удалось загрузить статистику', 'error');
    }
  }
}

Типичные ошибки

Ошибка Причина Решение
Chain not found Передан несуществующий chainId Проверить ID цепочки, показать ошибку пользователю
500 Internal Server Error Проблема на сервере Показать общее сообщение об ошибке, повторить запрос
Timeout Слишком много данных Использовать фильтрацию по chainId для уменьшения объема данных

Дополнительные возможности

Экспорт данных

Данные можно экспортировать в CSV или Excel для анализа:

function exportToCSV(tasksTable) {
  const headers = ['Название', 'Попыток', 'Пользователей', 'Успешно', '% успеха', 'Средние попытки'];
  const rows = tasksTable.map(task => [
    task.title,
    task.totalAttempts,
    task.uniqueUsers,
    task.acceptedCount,
    task.successRate,
    task.averageAttemptsToSuccess
  ]);
  
  const csv = [headers, ...rows]
    .map(row => row.join(','))
    .join('\n');
    
  const blob = new Blob([csv], { type: 'text/csv' });
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'tasks-statistics.csv';
  a.click();
}

Заключение

Эндпоинт /stats/v2 предоставляет все необходимые данные для построения информативных дашбордов с таблицами заданий и прогресс-барами участников. Комбинируйте различные части данных для создания удобных UI компонентов.

Для дополнительной информации см. также: