12 KiB
12 KiB
09. Контракт API: профиль студента курса
Статус: контракт этапа 0, реализация — этапы 3 и 5. Связанные документы: 07-groups-courses.md, 08-groups-courses-plan.md, ADR 0001.
1. Назначение
Преподаватель своего курса открывает профиль студента и видит:
- статистику по лабораторным курса;
- комментарии преподавателей по каждой лабе;
- все попытки сдачи и может скачать файлы любой попытки.
Профиль ограничен одним курсом. Он не заменяет ведомость и не даёт доступа к другим курсам или чужим сдачам.
2. Авторизация и скоуп
| Роль | Доступ |
|---|---|
ADMIN |
любой студент любого курса |
TEACHER |
только активный преподаватель курса (course_teachers.removed_at IS NULL) и только студент группы, привязанной к курсу (course_groups.removed_at IS NULL) |
STUDENT |
только свой профиль и только опубликованные оценки/комментарии (опционально) |
| Аноним | нет |
Проверки выполняются на сервисах:
- JWT валиден, роль допустима.
- Курс существует и не архивирован (для преподавателя).
- Для
TEACHERесть активная запись вcourse_teachers. - Студент существует, его аккаунт не архивный.
- Группа студента привязана к курсу (
course_groups.removed_at IS NULL). - Для скачивания сдача принадлежит этому студенту и лабе этого курса.
Чужой курс, студент или сдача → 403/404 без раскрытия данных.
3. Основной endpoint
GET /api/v1/courses/{courseId}/students/{studentId}/profile
Authorization: Bearer <JWT>
Параметры:
courseId— UUID курса;studentId— UUID аккаунта студента (students.account_id).
Ответ 200 application/json:
{
"course": {
"id": "3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e",
"courseNo": 1,
"title": "Промышленное программирование на языке C"
},
"student": {
"id": "7a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"studentNo": "123456",
"fullName": "Иванов Иван Иванович",
"groupId": "11111111-2222-3333-4444-555555555555",
"groupName": "ПИ-21БО",
"status": "ACTIVE",
"enrollmentYear": 2025
},
"summary": {
"labsTotal": 8,
"submitted": 6,
"graded": 5,
"averageScore": 4.2,
"defensesTotal": 5,
"defensesReady": 3
},
"labs": [
{
"labId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"number": 1,
"title": "Введение в C",
"deadlineAt": "2026-10-01T20:59:00Z",
"variant": {
"id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"number": "3",
"title": "Вариант 3"
},
"level": {
"id": "cccccccc-dddd-eeee-ffff-000000000000",
"code": "base",
"title": "Базовый"
},
"attemptsCount": 2,
"lastSubmissionAt": "2026-09-28T18:12:00Z",
"grade": {
"score": 4.5,
"published": true,
"generalComment": "Хорошая работа, но есть замечания по стилю.",
"updatedBy": {
"id": "dddddddd-eeee-ffff-0000-111111111111",
"name": "Петров Пётр Петрович"
}
},
"comments": [
{
"id": "eeeeeeee-ffff-0000-1111-222222222222",
"text": "Разделите функцию на части.",
"tone": "neutral",
"author": {
"id": "dddddddd-eeee-ffff-0000-111111111111",
"name": "Петров Пётр Петрович"
},
"createdAt": "2026-09-29T09:30:00Z"
}
],
"defense": {
"sessionId": "ffffffff-0000-1111-2222-333333333333",
"status": "ready",
"readyReason": "timeout",
"code": null,
"assignmentMd": "Дополнительное задание..."
},
"submissions": [
{
"id": "00000000-1111-2222-3333-444444444444",
"seq": 1,
"createdAt": "2026-09-27T16:00:00Z",
"files": ["main.c", "Makefile"],
"downloadUrl": "/api/v1/courses/3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e/submissions/00000000-1111-2222-3333-444444444444/download"
},
{
"id": "00000000-5555-6666-7777-888888888888",
"seq": 2,
"createdAt": "2026-09-28T18:12:00Z",
"files": ["main.c", "Makefile", "README.md"],
"downloadUrl": "/api/v1/courses/3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e/submissions/00000000-5555-6666-7777-888888888888/download"
}
]
}
]
}
Правила:
labs— все лабы курса для преподавателя/админа; для студента — только опубликованные.gradeиcommentsдля студента отдаются только если оценка опубликована (grade_publications/grade_publication_students).commentsвключает общий комментарий? Нет: общий комментарий — полеgrade.generalComment; отдельные комментарии — массивcomments.authorможет бытьnull, если автор не сохранён (старые данные).averageScore— среднее по выставленным оценкам,nullесли оценок нет.submissions— последние попытки по убываниюseq, по умолчанию не более 50 (submissionLimit, максимум 500;0— все);submissionsTotal— всего попыток,attemptsCount— тоже полное число. Файлы — имена без пути.downloadUrlотносительный; web-BFF добавляет авторизацию.
4. Скачивание попытки
GET /api/v1/courses/{courseId}/submissions/{submissionId}/download
Authorization: Bearer <JWT>
- Возвращает ZIP со всеми файлами попытки.
- Проверки: сдача принадлежит студенту и лабе курса; преподаватель активен в курсе; админ — всегда.
- Имя файла:
submission-{labId-prefix}-try{seq}.zip. - Ошибки:
403— чужая сдача/курс,404— сдача не найдена.
Опционально:
GET /api/v1/courses/{courseId}/submissions/{submissionId}/files/{filename}
- Отдаёт один файл.
- Имя проверяется на path traversal; файл должен быть в каталоге попытки.
5. Внутренние эндпоинты
Не маршрутизируются gateway, защищены X-Internal-Token и пользовательским JWT
там, где он нужен.
identity
GET /api/v1/internal/students/{studentId}/context
{
"accountId": "7a1b2c3d-...",
"role": "STUDENT",
"accountStatus": "ACTIVE",
"studentStatus": "ACTIVE",
"studentNo": "123456",
"fullName": "Иванов Иван Иванович",
"groupId": "11111111-...",
"groupName": "ПИ-21БО",
"enrollmentYear": 2025
}
academic
GET /api/v1/internal/courses/{courseId}/access?accountId={accountId}&groupId={groupId}
{
"courseId": "3f2a7c1e-...",
"courseStatus": "ACTIVE",
"teacherActive": true,
"groupAttached": true
}
GET /api/v1/internal/courses/{courseId}/labs
Возвращает лабы курса с id, number, title, deadlineAt, status,
pointsMax, variant/level при необходимости.
6. Ошибки
| Код | Когда |
|---|---|
401 |
нет/просрочен JWT |
403 |
роль не подходит, чужой курс, преподаватель не активен, студент не в группе курса |
404 |
курс, студент, лаба или сдача не найдены |
409 |
профиль недоступен из-за конфликта данных (например, студент без группы) |
Тело ошибки — общий ApiError проекта.
7. Производительность
- Один агрегирующий запрос на профиль, без N+1.
- Пакетная выборка по списку
labId:- оценки —
lab_gradesпоstudent_idиlab_id IN (...); - комментарии —
grade_commentsпоstudent_idиlab_id IN (...); - сдачи —
submissionsпоstudent_idиlab_id IN (...); - защиты —
defense_sessionsпоstudent_idиlab_id IN (...); - публикации —
grade_publications/grade_publication_studentsпо группе и лабам.
- оценки —
- Список лаб курса кэшируется на время запроса.
- Для больших курсов — лимит попыток:
submissionLimit(по умолчанию 50, максимум 500,0— все) иsubmissionsTotalдля индикатора «показаны последние N»;attemptsCountне зависит от лимита. - Индексы:
submissions(student_id, lab_id, seq)уже уникален;grade_comments(student_id, lab_id)уже есть;lab_grades(lab_id)добавлен;defense_sessions(student_id, lab_id)уникален.
8. UI
- В ведомости группы ФИО — ссылка на профиль.
- В колонке действий — кнопка «Профиль».
- Маршрут преподавателя:
/teacher/courses/{courseNo}/students/{studentId}. - Админ:
/admin/courses/{courseNo}/students/{studentId}или общий маршрут с проверкой роли. - В профиле:
- карточка студента;
- сводка;
- таблица лаб;
- блок комментариев;
- список попыток с кнопками скачивания.
- Статус защиты: «готова» / «кончилось время» (timeout).
9. Тесты
ADMINвидит любого студента.TEACHERсвоего курса —200; чужого курса —403.TEACHERчужого студента —403.STUDENTсвой профиль —200; чужой —403.- Аноним —
401. - Скачивание своей попытки —
200; чужой —403. - Студент не видит неопубликованные оценки/комментарии.
- Все попытки возвращаются (в пределах
submissionLimit), а не только последняя;submissionsTotal/attemptsCountпоказывают полное число. - Профиль на 10 лабораторных делает не более 15 SQL-запросов (без N+1).
- Комментарии содержат автора или
null. - Запрос не делает N+1 (проверка по числу SQL-запросов в интеграционном тесте).
10. Открытые вопросы
- Показывать ли профиль для архивных/отвязанных студентов (история или
404). - Нужен ли студенческий просмотр своего профиля в первом релизе.
Формат аудита скачиваний: общийРешено на этапе 6: общийaudit_eventsили отдельная таблица.identity.audit_events(V10).- Ограничение количества попыток в ответе и пагинация — follow-up.