24 KiB
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:
- tasksTable - содержит только задания из указанной цепочки
- activeParticipants - включает только участников, которые делали попытки по заданиям этой цепочки. В
chainProgressбудет информация только об указанной цепочке - 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 когда:
-
Страница отдельной цепочки - пользователь просматривает детали конкретной цепочки
function ChainDetailsPage({ chainId }) { const { data } = useStatsV2(chainId); return <ChainDashboard data={data} />; } -
Улучшение производительности - когда нужны данные только по одной цепочке, фильтрация на сервере работает быстрее
-
Фокус на конкретной программе - преподаватель хочет видеть прогресс по конкретному курсу/модулю
НЕ используйте chainId когда:
- Общий дашборд - нужна статистика по всем цепочкам
- Сравнение цепочек - нужно показать метрики по всем цепочкам одновременно
- Общая аналитика - нужны агрегированные данные по всей системе
Различия между 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 компонентов.
Для дополнительной информации см. также:
- CHALLENGE_FRONTEND_GUIDE.md - общее руководство по работе с Challenge API
- CHALLENGE_REACT_EXAMPLE.md - примеры React компонентов