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

12 KiB
Raw Permalink Blame History

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 только свой профиль и только опубликованные оценки/комментарии (опционально)
Аноним нет

Проверки выполняются на сервисах:

  1. JWT валиден, роль допустима.
  2. Курс существует и не архивирован (для преподавателя).
  3. Для TEACHER есть активная запись в course_teachers.
  4. Студент существует, его аккаунт не архивный.
  5. Группа студента привязана к курсу (course_groups.removed_at IS NULL).
  6. Для скачивания сдача принадлежит этому студенту и лабе этого курса.

Чужой курс, студент или сдача → 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).
  • Нужен ли студенческий просмотр своего профиля в первом релизе.
  • Формат аудита скачиваний: общий audit_events или отдельная таблица. Решено на этапе 6: общий identity.audit_events (V10).
  • Ограничение количества попыток в ответе и пагинация — follow-up.