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

694 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API Статистики v2 - Документация для Frontend
## Обзор
Эндпоинт `/challenge/stats/v2` предоставляет расширенную статистику системы проверки заданий с детальными данными для построения таблиц и прогресс-баров.
## Эндпоинт
```
GET /challenge/stats/v2
```
### Аутентификация
Не требуется.
### Параметры запроса
| Параметр | Тип | Обязательный | Описание |
|----------|-----|--------------|----------|
| `chainId` | string | Нет | ID цепочки для фильтрации статистики. Если указан, возвращается статистика только по заданиям из этой цепочки |
#### Примеры использования
Получить полную статистику:
```
GET /challenge/stats/v2
```
Получить статистику только по одной цепочке:
```
GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439031
```
## Структура ответа
```typescript
{
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)
}>
}>
}
}
```
## Пример ответа
```json
{
"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** - содержит информацию только об указанной цепочке
### Пример фильтрованного ответа
```bash
curl http://localhost:3000/challenge/stats/v2?chainId=507f1f77bcf86cd799439031
```
```json
{
"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` для отображения статистики по каждому заданию:
```tsx
// 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` для отображения прогресса каждого участника:
```tsx
// 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` для отображения детального прогресса всех участников в рамках каждой цепочки:
```tsx
// 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 индикаторы во время загрузки данных
```tsx
// Пример с кэшированием
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>
);
}
```
### Фильтрация и сортировка
Все массивы данных можно фильтровать и сортировать на клиенте:
```tsx
// Сортировка таблицы заданий по проценту успеха
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** - для прогресс-баров
```tsx
// Пример с 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. **Страница отдельной цепочки** - пользователь просматривает детали конкретной цепочки
```tsx
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 |
|----------|-------------|----------------|--------------|
| Базовая статистика | ✅ | ✅ | ✅ |
| Таблица заданий | ❌ | ✅ (все) | ✅ (фильтр) |
| Прогресс участников | ❌ | ✅ (все) | ✅ (фильтр) |
| Детальный прогресс по цепочкам | ❌ | ✅ (все) | ✅ (одна) |
| Статистика по попыткам | Общая | Детальная | Детальная (фильтр) |
| Скорость работы | Быстро | Медленнее | Средняя |
## Обработка ошибок
```tsx
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 для анализа:
```typescript
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_FRONTEND_GUIDE.md) - общее руководство по работе с Challenge API
- [CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md) - примеры React компонентов