Files
LMS/docs/09-teacher-student-profile.md

286 lines
12 KiB
Markdown
Raw 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.

# 09. Контракт API: профиль студента курса
**Статус:** контракт этапа 0, реализация — этапы 3 и 5.
**Связанные документы:** [07-groups-courses.md](07-groups-courses.md),
[08-groups-courses-plan.md](08-groups-courses-plan.md),
[ADR 0001](adr/0001-groups-courses-foundation.md).
## 1. Назначение
Преподаватель своего курса открывает профиль студента и видит:
- статистику по лабораторным курса;
- комментарии преподавателей по каждой лабе;
- все попытки сдачи и может скачать файлы любой попытки.
Профиль ограничен одним курсом. Он не заменяет ведомость и не даёт доступа
к другим курсам или чужим сдачам.
## 2. Авторизация и скоуп
| Роль | Доступ |
|---|---|
| `ADMIN` | любой студент любого курса |
| `TEACHER` | только активный преподаватель курса (`course_teachers.removed_at IS NULL`) и только студент группы, привязанной к курсу (`course_groups.removed_at IS NULL`) |
| `STUDENT` | только свой профиль и только опубликованные оценки/комментарии (опционально) |
| Аноним | нет |
Проверки выполняются на сервисах:
1. JWT валиден, роль допустима.
2. Курс существует и не архивирован (для преподавателя).
3. Для `TEACHER` есть активная запись в `course_teachers`.
4. Студент существует, его аккаунт не архивный.
5. Группа студента привязана к курсу (`course_groups.removed_at IS NULL`).
6. Для скачивания сдача принадлежит этому студенту и лабе этого курса.
Чужой курс, студент или сдача → `403`/`404` без раскрытия данных.
## 3. Основной endpoint
```http
GET /api/v1/courses/{courseId}/students/{studentId}/profile
Authorization: Bearer <JWT>
```
Параметры:
- `courseId` — UUID курса;
- `studentId` — UUID аккаунта студента (`students.account_id`).
Ответ `200 application/json`:
```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`; файлы — имена без пути.
- `downloadUrl` относительный; web-BFF добавляет авторизацию.
## 4. Скачивание попытки
```http
GET /api/v1/courses/{courseId}/submissions/{submissionId}/download
Authorization: Bearer <JWT>
```
- Возвращает ZIP со всеми файлами попытки.
- Проверки: сдача принадлежит студенту и лабе курса; преподаватель активен в
курсе; админ — всегда.
- Имя файла: `submission-{labId-prefix}-try{seq}.zip`.
- Ошибки: `403` — чужая сдача/курс, `404` — сдача не найдена.
Опционально:
```http
GET /api/v1/courses/{courseId}/submissions/{submissionId}/files/{filename}
```
- Отдаёт один файл.
- Имя проверяется на path traversal; файл должен быть в каталоге попытки.
## 5. Внутренние эндпоинты
Не маршрутизируются gateway, защищены `X-Internal-Token` и пользовательским JWT
там, где он нужен.
### identity
```http
GET /api/v1/internal/students/{studentId}/context
```
```json
{
"accountId": "7a1b2c3d-...",
"role": "STUDENT",
"accountStatus": "ACTIVE",
"studentStatus": "ACTIVE",
"studentNo": "123456",
"fullName": "Иванов Иван Иванович",
"groupId": "11111111-...",
"groupName": "ПИ-21БО",
"enrollmentYear": 2025
}
```
### academic
```http
GET /api/v1/internal/courses/{courseId}/access?accountId={accountId}&groupId={groupId}
```
```json
{
"courseId": "3f2a7c1e-...",
"courseStatus": "ACTIVE",
"teacherActive": true,
"groupAttached": true
}
```
```http
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` по группе и лабам.
- Список лаб курса кэшируется на время запроса.
- Для больших курсов — пагинация попыток (по умолчанию все, но с лимитом).
- Индексы: `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`.
- Студент не видит неопубликованные оценки/комментарии.
- Все попытки возвращаются, а не только последняя.
- Комментарии содержат автора или `null`.
- Запрос не делает N+1 (проверка по числу SQL-запросов в интеграционном тесте).
## 10. Открытые вопросы
- Показывать ли профиль для архивных/отвязанных студентов (история или `404`).
- Нужен ли студенческий просмотр своего профиля в первом релизе.
- Формат аудита скачиваний: общий `audit_events` или отдельная таблица.
- Ограничение количества попыток в ответе и пагинация.