Add new API endpoint for retrieving submissions by challenge chain; update frontend to support chain selection and display participant progress. Enhance localization for submissions page in English and Russian.
This commit is contained in:
236
docs/API_CHAIN_SUBMISSIONS.md
Normal file
236
docs/API_CHAIN_SUBMISSIONS.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Техническое задание: Эндпоинт получения попыток по цепочке
|
||||
|
||||
## Цель
|
||||
|
||||
Создать новый API эндпоинт для получения списка попыток (submissions) участников в рамках конкретной цепочки заданий. Это упростит работу админ-панели и уменьшит объём передаваемых данных.
|
||||
|
||||
## Текущая проблема
|
||||
|
||||
Сейчас для отображения попыток по цепочке фронтенд должен:
|
||||
1. Загрузить список цепочек (`GET /challenge/chains/admin`)
|
||||
2. Загрузить общую статистику (`GET /challenge/stats/v2`)
|
||||
3. Для каждого участника отдельно загрузить его submissions (`GET /challenge/user/:userId/submissions`)
|
||||
4. На клиенте фильтровать submissions по taskIds из выбранной цепочки
|
||||
|
||||
Это создаёт избыточные запросы и усложняет логику на фронтенде.
|
||||
|
||||
---
|
||||
|
||||
## Новый эндпоинт
|
||||
|
||||
### `GET /challenge/chain/:chainId/submissions`
|
||||
|
||||
Возвращает все попытки всех участников для заданий из указанной цепочки.
|
||||
|
||||
### Параметры URL
|
||||
|
||||
| Параметр | Тип | Обязательный | Описание |
|
||||
|----------|-----|--------------|----------|
|
||||
| `chainId` | string | Да | ID цепочки заданий |
|
||||
|
||||
### Query параметры (опциональные)
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
|----------|-----|--------------|----------|
|
||||
| `userId` | string | - | Фильтр по конкретному пользователю |
|
||||
| `status` | string | - | Фильтр по статусу: `pending`, `in_progress`, `accepted`, `needs_revision` |
|
||||
| `limit` | number | 100 | Лимит записей |
|
||||
| `offset` | number | 0 | Смещение для пагинации |
|
||||
|
||||
### Формат ответа
|
||||
|
||||
```typescript
|
||||
interface ChainSubmissionsResponse {
|
||||
success: boolean;
|
||||
body: {
|
||||
chain: {
|
||||
id: string;
|
||||
name: string;
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
}>;
|
||||
};
|
||||
participants: Array<{
|
||||
userId: string;
|
||||
nickname: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
progressPercent: number;
|
||||
}>;
|
||||
submissions: Array<{
|
||||
id: string;
|
||||
user: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
};
|
||||
task: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
status: 'pending' | 'in_progress' | 'accepted' | 'needs_revision';
|
||||
attemptNumber: number;
|
||||
submittedAt: string; // ISO date
|
||||
checkedAt?: string; // ISO date
|
||||
feedback?: string;
|
||||
}>;
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```bash
|
||||
GET /api/challenge/chain/607f1f77bcf86cd799439021/submissions?status=needs_revision&limit=50
|
||||
```
|
||||
|
||||
### Пример ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"body": {
|
||||
"chain": {
|
||||
"id": "607f1f77bcf86cd799439021",
|
||||
"name": "Основы JavaScript",
|
||||
"tasks": [
|
||||
{ "id": "507f1f77bcf86cd799439011", "title": "Реализовать сортировку массива" },
|
||||
{ "id": "507f1f77bcf86cd799439015", "title": "Валидация формы" }
|
||||
]
|
||||
},
|
||||
"participants": [
|
||||
{
|
||||
"userId": "user_123",
|
||||
"nickname": "alex_dev",
|
||||
"completedTasks": 1,
|
||||
"totalTasks": 2,
|
||||
"progressPercent": 50
|
||||
},
|
||||
{
|
||||
"userId": "user_456",
|
||||
"nickname": "maria_coder",
|
||||
"completedTasks": 2,
|
||||
"totalTasks": 2,
|
||||
"progressPercent": 100
|
||||
}
|
||||
],
|
||||
"submissions": [
|
||||
{
|
||||
"id": "sub_001",
|
||||
"user": {
|
||||
"id": "user_123",
|
||||
"nickname": "alex_dev"
|
||||
},
|
||||
"task": {
|
||||
"id": "507f1f77bcf86cd799439011",
|
||||
"title": "Реализовать сортировку массива"
|
||||
},
|
||||
"status": "needs_revision",
|
||||
"attemptNumber": 2,
|
||||
"submittedAt": "2024-12-10T14:30:00.000Z",
|
||||
"checkedAt": "2024-12-10T14:30:45.000Z",
|
||||
"feedback": "Алгоритм работает неверно для отрицательных чисел"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 15,
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Логика на бэкенде
|
||||
|
||||
### Алгоритм
|
||||
|
||||
1. Получить цепочку по `chainId`
|
||||
2. Если цепочка не найдена — вернуть 404
|
||||
3. Получить список `taskIds` из цепочки
|
||||
4. Найти все submissions где `task._id` входит в `taskIds`
|
||||
5. Применить фильтры (`userId`, `status`) если указаны
|
||||
6. Вычислить прогресс по каждому участнику:
|
||||
- Найти уникальных пользователей из submissions
|
||||
- Для каждого посчитать `completedTasks` (количество уникальных tasks со статусом `accepted`)
|
||||
- Рассчитать `progressPercent = (completedTasks / totalTasks) * 100`
|
||||
7. Применить пагинацию к submissions
|
||||
8. Вернуть результат
|
||||
|
||||
### Индексы MongoDB (рекомендуется)
|
||||
|
||||
```javascript
|
||||
// Для быстрой выборки submissions по task
|
||||
db.submissions.createIndex({ "task": 1, "submittedAt": -1 })
|
||||
|
||||
// Составной индекс для фильтрации
|
||||
db.submissions.createIndex({ "task": 1, "status": 1, "submittedAt": -1 })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Права доступа
|
||||
|
||||
Эндпоинт должен быть доступен только пользователям с ролями:
|
||||
- `challenge-admin`
|
||||
- `challenge-teacher`
|
||||
|
||||
---
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 200 | Успешный ответ |
|
||||
| 400 | Некорректные параметры запроса |
|
||||
| 401 | Не авторизован |
|
||||
| 403 | Недостаточно прав |
|
||||
| 404 | Цепочка не найдена |
|
||||
| 500 | Внутренняя ошибка сервера |
|
||||
|
||||
---
|
||||
|
||||
## Изменения на фронтенде после реализации
|
||||
|
||||
После добавления эндпоинта в `src/__data__/api/api.ts` нужно добавить:
|
||||
|
||||
```typescript
|
||||
// В endpoints builder
|
||||
getChainSubmissions: builder.query<ChainSubmissionsResponse, {
|
||||
chainId: string;
|
||||
userId?: string;
|
||||
status?: SubmissionStatus;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}>({
|
||||
query: ({ chainId, userId, status, limit, offset }) => ({
|
||||
url: `/challenge/chain/${chainId}/submissions`,
|
||||
params: { userId, status, limit, offset },
|
||||
}),
|
||||
transformResponse: (response: { body: ChainSubmissionsResponse }) => response.body,
|
||||
providesTags: ['Submission'],
|
||||
}),
|
||||
```
|
||||
|
||||
Это позволит упростить `SubmissionsPage.tsx`:
|
||||
- Один запрос вместо нескольких
|
||||
- Убрать клиентскую фильтрацию по taskIds
|
||||
- Получать готовый прогресс участников
|
||||
|
||||
---
|
||||
|
||||
## Приоритет
|
||||
|
||||
**Средний** — текущая реализация работает, но создаёт избыточную нагрузку при большом количестве участников.
|
||||
|
||||
## Оценка трудозатрат
|
||||
|
||||
~4-6 часов (включая тесты)
|
||||
|
||||
Reference in New Issue
Block a user