init: learning-service skeleton

Микросервис обучения портала: тесты, курсы, видео-уроки, доступы,
public-ссылки для кандидатов с email-валидацией.

В этой итерации:
- Skeleton (config, migrate, main, health) по паттерну tasks/candidates
- Migration 001_init: 10 таблиц (tests/questions/answers/attempts/
  attempt_answers + courses/lessons/lesson_progress + access_grants +
  public_tokens) с подробными комментариями why
- Tests: полный CRUD + вопросы/ответы; non-owner'у is_correct и
  explanation скрываются в выдаче
- Заглушки 501 для attempts / courses / lessons / video-stream /
  access / public-tokens — следующие итерации
- k8s: namespace, configmap, secrets, postgres, deployment с HPA,
  service с portal-discovery annotations
- Dockerfile, Makefile, .gitignore

См. README.md для полного списка отложенного и инструкций запуска.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilya
2026-05-25 22:43:37 +03:00
commit 62519081e7
24 changed files with 1915 additions and 0 deletions

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# learning-service
Микросервис обучения портала: тесты, курсы с видео-уроками,
гранулярные доступы, public-ссылки для кандидатов с email-валидацией.
## Стек
- Go 1.25, chi/v5, pgx/v5, portal-common
- PostgreSQL (отдельная БД `learning`)
- MinIO (bucket `learning-videos`) — стрим-прокси, MinIO URL не светится
клиенту (по аналогии с telephony record stream)
- Redis (eventbus, опционально) — публикация событий
`test.submitted` / `course.enrolled` для portal-real-time
- k8s (namespace `learning`)
## Сущности (см. `migrations/001_init.up.sql`)
| Сущность | Назначение |
|---|---|
| `tests` | Тесты: title, passing_score, max_attempts, time_limit_sec, is_published, owner |
| `test_questions` | Вопросы (single / multi / text), points, explanation |
| `test_answers` | Варианты ответа с is_correct |
| `test_attempts` | Попытки прохождения: user_id ИЛИ public_token_id; score/passed |
| `test_attempt_answers` | Ответы пользователя в попытке (JSONB payload) |
| `courses` | Курсы: title, slug, cover, is_published |
| `lessons` | Уроки курса: markdown + video_key + опциональный test_id |
| `lesson_progress` | Прогресс «просмотрено / завершено» по урокам |
| `access_grants` | Гранулярный ACL: ресурс × subject (user/role/department/position/public) |
| `public_tokens` | Одноразовые ссылки кандидатам с intended_email + max_attempts |
## Permission-модель (на стороне portal-backend)
| Permission | Кому | Что даёт |
|---|---|---|
| `service.learning.access` | Все активные сотрудники | Видимость раздела, прохождение назначенного |
| `service.learning.author` | HR, тренеры, лиды | Создание/редактирование своих материалов, public-ссылки |
| `service.learning.admin` | Admin | Глобальный доступ к чужим материалам, аналитика |
См. portal-backend `migrations/049_learning_permissions.up.sql`.
## Что реализовано в этой итерации
**Backend:**
- Skeleton сервиса (config, migrate, main, health)
- Migration `001_init.up.sql` — полная схема всех 10 таблиц
- **Tests:** полный CRUD + вопросы/ответы + scoped-выдача (не-владельцу
is_correct скрывается, explanation тоже)
- Заглушки `notImplemented` (501) для остальных эндпоинтов
**Portal-backend:**
- Migration `049_learning_permissions.up.sql` — 3 пермы + грантование admin'у
- `/api/learning/*` — прокси под `service.learning.access`
- `/public/learning/*` — прокси без auth для кандидатов
- `Services.LearningURL` + env-переменная
**Frontend:**
- Route `/learning/*` под guard'ом `service.learning.access`
- Landing-страница с тремя картчоками
- `/learning/tests` — список + создание (полностью рабочий)
- `/learning/courses`, `/learning/admin` — заглушки `LearningStubComponent`
- Sidebar-пункт «Обучение» + sub-навигация
**k8s:** полный набор манифестов (namespace / configmap / secrets /
postgres / deployment с HPA / service с portal-discovery annotations).
## Что отложено в следующие итерации
| Фича | План |
|---|---|
| **Attempts (прохождение)** | Repository + handler: start attempt → fetch questions → submit → auto-grade (single/multi) + manual review для text |
| **Courses + Lessons CRUD** | Repository + handler по тому же паттерну что Tests |
| **Видео-стрим** | MinIO wrapper в `internal/storage/`, handler `/lessons/{id}/video/stream` с Range-поддержкой; копировать stream-pattern из telephony |
| **Access grants** | Repository + handler + helper «может ли user X пройти ресурс Y» (учитывает user/role/department/position иерархию через portal-internal API) |
| **Public tokens** | Repository + handler: создание (HR из portal'а), resolve (email-match) — на портале и стрим в proxy без auth |
| **Email-отправка** | Через portal Kerio integration — handler выдаёт сформированный URL + текст письма HR'у, реальная отправка через portal |
| **Frontend: конструктор теста** | Drag-and-drop редактор вопросов/ответов (паттерн как kanban-column в tasks) |
| **Frontend: плеер видео-урока** | Кастомный плеер по аналогии с call-audio-player (blob URL, controlsList=nodownload) |
| **Frontend: public-страница кандидата** | Отдельный route `/public/learning/test/:token` без layout'а портала |
## Локальный запуск
```bash
docker compose up -d postgres minio # или используй существующие
make tidy
DATABASE_URL=postgres://learning:learning@localhost:5432/learning?sslmode=disable \
INTERNAL_API_KEY=devkey \
MINIO_ENDPOINT=localhost:9000 \
MINIO_ACCESS_KEY=minioadmin \
MINIO_SECRET_KEY=minioadmin \
make run
```
## Deploy
```bash
kubectl apply -k k8s/
# Образ собирается в CI или вручную:
docker build -f Dockerfile.server -t localhost:30300/admin/learning-server:latest .
docker push localhost:30300/admin/learning-server:latest
```