docs: план, ADR и контракты групп и курсов
This commit is contained in:
285
docs/09-teacher-student-profile.md
Normal file
285
docs/09-teacher-student-profile.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# 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` или отдельная таблица.
|
||||
- Ограничение количества попыток в ответе и пагинация.
|
||||
Reference in New Issue
Block a user