Разработка образовательной AI-платформы с геймификацией (LMS)

Пошаговый план создания полнофункционального веб-портала для учебных заведений на стеке Node.js, React и Supabase. Курс охватывает архитектуру базы данных, внедрение AI-ассистента, систему турниров на Redis и создание впечатляющего UI для успешной защиты проекта.

Проектирование архитектуры, настройка Git и базы данных Supabase

Проектирование архитектуры, настройка Git и базы данных Supabase

Добро пожаловать в курс по разработке образовательной AI-платформы (LMS). Мы создаем не просто сайт, а сложную экосистему с геймификацией, ролевой моделью (ученик, учитель, родитель, учреждение) и искусственным интеллектом. Чтобы проект получил высокую оценку и стал достойным кейсом в портфолио, мы начнем с фундамента: правильной архитектуры, профессиональной работы с Git и проектирования базы данных.

1. Архитектура системы: Модульный монолит

Для учебного проекта такого масштаба (LMS с элементами соцсети и турнирами) микросервисная архитектура может быть избыточной и сложной в развертывании. Мы выберем модульный монолит. Это позволит нам держать код в одном репозитории (удобно для показа преподавателю), но сохранять четкое разделение логики.

Согласно Степанов М.А. и др., современные образовательные системы требуют интеграции разнородных данных (цифровой след, успеваемость) и масштабируемости. Наша архитектура будет состоять из трех слоев:

  1. Frontend (Клиентская часть): React (Vite) + TailwindCSS. Отвечает за интерфейс, анимации геймификации и отображение графиков.
  2. Backend (Серверная часть): Node.js (Express/NestJS). Обрабатывает бизнес-логику: создание турниров, генерацию курсов через AI, валидацию данных.
  3. Data Layer (Слой данных):
    • Supabase (PostgreSQL): Основное хранилище (пользователи, курсы, оценки).
    • Redis: Кэширование, списки лидеров (leaderboards) в реальном времени и статусы «онлайн».

Схема взаимодействия

Пользователь (Student/Teacher) ightarrow ightarrow React App ightarrow ightarrow Node.js API ightarrow ightarrow Supabase/Redis.

2. Профессиональная настройка Git

Для получения высокой оценки важно показать историю разработки. Мы будем использовать методологию Conventional Commits. Это стандарт оформления коммитов, который делает историю читаемой.

Инициализация репозитория

Откройте терминал в папке проекта:

git init
echo "node_modules/" >> .gitignore
echo ".env" >> .gitignore
git add .
git commit -m "chore: initial project setup"

Правила именования коммитов

Каждый ваш коммит должен начинаться с типа изменения. Это покажет преподавателю вашу дисциплину.

  • feat: — новая функциональность (например, feat: add tournament logic)
  • fix: — исправление бага (например, fix: user login error)
  • docs: — изменения в документации
  • style: — форматирование, отсутсвие изменений кода (пробелы, точки с запятой)
  • refactor: — правка кода без изменения функциональности

Использование понятной истории коммитов критически важно для командной разработки и оценки вклада каждого участника.

NikoCerens

3. Проектирование базы данных в Supabase

Supabase предоставляет нам PostgreSQL «из коробки» с готовым API и аутентификацией. Это идеально подходит для быстрого старта сложного проекта. Как отмечают в статье про подключение Supabase, использование CLI и миграций позволяет автоматизировать развертывание схемы БД, что является признаком зрелого подхода к разработке (согласно @ansaril3).

ER-диаграмма (Схема данных)

Нам нужно реализовать 4 роли: Institution (Учреждение), Teacher (Учитель), Student (Ученик), Parent (Родитель). В Supabase таблица auth.users управляет входом, но данные профилей мы будем хранить в отдельной таблице public.profiles.

Основные таблицы:
  1. profiles

    • id (uuid, PK) — ссылка на auth.users
    • role (enum: 'admin', 'institution', 'teacher', 'student', 'parent')
    • full_name (text)
    • avatar_url (text)
    • institution_id (uuid, FK) — принадлежность к школе/вузу
  2. institutions (Учреждения)

    • id (uuid, PK)
    • name (text)
    • type (enum: 'school', 'college', 'university')
    • subscription_plan (enum: 'basic', 'pro', 'max')
  3. courses (Курсы)

    • id (uuid, PK)
    • title (text)
    • author_id (uuid, FK) — учитель или учреждение
    • is_public (boolean) — для глобального поиска
    • price (numeric) — если курс платный
  4. course_progress (Прогресс)

    • student_id (uuid, FK)
    • course_id (uuid, FK)
    • progress_percent (int)
    • completed_at (timestamp)
  5. gamification_stats (Геймификация)

    • user_id (uuid, FK)
    • xp (int) — опыт
    • level (int) — уровень
    • coins (int) — внутренняя валюта

Математика геймификации

Чтобы система уровней была сбалансированной, мы не можем использовать линейный рост. Нам нужна формула, где каждый следующий уровень требует больше опыта. Используем квадратичную зависимость.

Формула расчета необходимого опыта (XPXP) для достижения уровня (LL):

XPreq=KimesL2XP_{req} = K imes L^2

где XPreqXP_{req} — необходимое количество очков опыта для достижения уровня, KK — коэффициент сложности (например, 100), LL — номер уровня.

При K=100K=100:

  • Уровень 1: 100imes12=100100 imes 1^2 = 100 XP
  • Уровень 2: 100imes22=400100 imes 2^2 = 400 XP
  • Уровень 10: 100imes102=10000100 imes 10^2 = 10000 XP

Для расчета текущего уровня игрока на основе его опыта используем обратную формулу:

L_{current} = igg ⌊ ext{sqrt}igg( rac{XP_{total}}{K}igg) igg ⌋

где LcurrentL_{current} — текущий уровень (округленный вниз), XPtotalXP_{total} — текущий опыт игрока, KK — коэффициент сложности.

SQL для создания таблиц (Supabase SQL Editor)

Вставьте этот код в SQL Editor в панели Supabase:

-- Создание ENUM для ролей
create type user_role as enum ('institution', 'teacher', 'student', 'parent', 'admin');

-- Таблица профилей
create table public.profiles (
  id uuid references auth.users on delete cascade not null primary key,
  role user_role default 'student',
  full_name text,
  institution_id uuid, -- связь добавим позже
  created_at timestamp with time zone default timezone('utc'::text, now())
);

-- Включение RLS (Row Level Security)
alter table public.profiles enable row level security;

-- Политика: каждый может читать базовые данные, но менять только свои
create policy "Public profiles are viewable by everyone."
  on profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on profiles for insert
  with check ( auth.uid() = id );

4. Настройка Redis для турниров

Redis нам нужен для двух критических функций, которые сложно реализовать быстро на чистом SQL:

  1. Статус Online: Родители и учителя должны видеть, в сети ли ученик.
  2. Leaderboards (Турнирные таблицы): Сортировка тысяч учеников по очкам в реальном времени.

В Node.js мы будем использовать структуру Sorted Set в Redis для хранения очков турнира. Это работает быстрее, чем SELECT * FROM users ORDER BY xp DESC в PostgreSQL при больших нагрузках.

5. Инициализация проекта (Node.js + React)

Создадим структуру папок, которая впечатлит любого проверяющего своей организованностью.

mkdir edu-platform-lms
cd edu-platform-lms

# Создаем папку для бэкенда
mkdir backend
cd backend
npm init -y
npm install express @supabase/supabase-js redis cors dotenv

# Создаем папку для фронтенда (в корне проекта)
cd ..
npm create vite@latest frontend -- --template react-ts

В итоге ваша структура должна выглядеть так:

/edu-platform-lms
  /backend       # API, работа с AI, Redis
  /frontend      # React, UI компоненты
  /docs          # Документация и схемы
  .gitignore
  README.md

Итоги

В этой статье мы заложили фундамент нашей LMS-платформы:

  1. Утвердили архитектуру Модульного Монолита (React + Node.js + Supabase + Redis).
  2. Настроили Git с использованием Conventional Commits для профессиональной истории изменений.
  3. Спроектировали схему базы данных с учетом ролей (Учреждение, Учитель, Родитель, Студент) и настроили RLS в Supabase.
  4. Ввели математическую модель геймификации для расчета уровней.
  5. Подготовили структуру проекта для дальнейшей разработки функционала.

Реализация сложной ролевой модели и иерархии учреждений

Реализация сложной ролевой модели и иерархии учреждений

В предыдущей статье мы развернули базу данных Supabase и настроили Git. Теперь переходим к «сердцу» нашей LMS — системе ролей и доступов. Чтобы ваш проект получил отличную оценку, недостаточно просто сделать вход и выход. Мы реализуем Role-Based Access Control (RBAC) — управление доступом на основе ролей, которое позволит моделировать реальные образовательные экосистемы.

Суть нашего проекта — дать качественное образование через геймификацию. Но, как отмечают в исследовании Института новых технологий, для образовательного процесса полное равноправие участников неприемлемо: ученик, учитель, завуч и директор принципиально не равноправны. Нам необходимо ввести строгую иерархию.

Иерархическая структура — это заданное отношение, при котором один участник является лидером, а второй — подписчиком. У каждого участника может быть не более одного непосредственного лидера.

Институт новых технологий

1. Проектирование схемы данных для иерархии

В простой системе у пользователя есть поле role: 'admin'. В нашей сложной системе один и тот же человек может быть учителем в одной школе и родителем в другой. Поэтому мы выносим связи в отдельные таблицы.

Доработка схемы в Supabase

Нам понадобятся две новые ключевые таблицы: institution_members (связь людей с учреждениями) и family_links (связь родителей и детей).

Выполните этот SQL-запрос в редакторе Supabase:

-- Таблица связей пользователей с учреждениями
create type member_role as enum ('owner', 'admin', 'teacher', 'student');

create table public.institution_members (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references public.profiles(id) on delete cascade,
  institution_id uuid references public.institutions(id) on delete cascade,
  role member_role default 'student',
  title text, -- Например: "Завуч по воспитательной работе" или "Староста 11Б"
  joined_at timestamp with time zone default now(),
  unique(user_id, institution_id)
);

-- Таблица семейных связей
create table public.family_links (
  parent_id uuid references public.profiles(id) on delete cascade,
  student_id uuid references public.profiles(id) on delete cascade,
  is_confirmed boolean default false,
  primary key (parent_id, student_id)
);

-- Включаем RLS
alter table public.institution_members enable row level security;
alter table public.family_links enable row level security;

Такая структура позволяет учреждению (например, школе) назначать свои внутренние должности (директор, завуч), не ломая глобальную логику платформы.

2. Реализация Middleware для проверки прав (Node.js)

На бэкенде (Node.js) нам нужно создать «стражей», которые будут проверять права перед выполнением любого действия. Это покажет преподавателю, что вы понимаете принципы безопасной разработки.

Создайте файл backend/src/middleware/authMiddleware.js:

const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);

// Проверка, авторизован ли пользователь
const requireAuth = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });

  const { data: { user }, error } = await supabase.auth.getUser(token);
  if (error || !user) return res.status(401).json({ error: 'Invalid token' });

  req.user = user;
  next(); // Передаем управление следующей функции
};

// Проверка роли внутри учреждения
const requireInstitutionRole = (requiredRole) => async (req, res, next) => {
  const { institutionId } = req.params;
  const userId = req.user.id;

  const { data, error } = await supabase
    .from('institution_members')
    .select('role')
    .eq('user_id', userId)
    .eq('institution_id', institutionId)
    .single();

  if (error || !data) return res.status(403).json({ error: 'Access denied' });

  // Иерархия ролей: owner > admin > teacher > student
  const roles = ['student', 'teacher', 'admin', 'owner'];
  if (roles.indexOf(data.role) < roles.indexOf(requiredRole)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }

  next();
};

module.exports = { requireAuth, requireInstitutionRole };

Теперь, если учитель захочет создать курс, мы защитим роут так: router.post('/courses', requireAuth, requireInstitutionRole('teacher'), createCourseController);

3. Математика рейтинга учреждений

Чтобы реализовать соревновательный элемент между учреждениями (турниры школ), нам нужно вычислять Global Rating Index (GRI). Просто сложить опыт всех учеников нельзя — большая школа всегда победит маленькую. Нам нужна формула взвешенного среднего.

Согласно SKOLKOVO, успешность развития образовательной экосистемы зависит от скоординированных действий всех участников. Мы выразим это математически.

Формула расчета рейтинга учреждения (RinstR_{inst}):

Rinst=∑i=1N(XPi×Ai)N×log⁡10(N+10)R_{inst} = \frac{\sum_{i=1}^{N} (XP_i \times A_i)}{N} \times \log_{10}(N + 10)

Где:

  • RinstR_{inst} — итоговый рейтинг учреждения.
  • NN — количество активных учеников (чтобы отсеять «мертвые души»).
  • XPiXP_i — опыт ii-го ученика за текущий сезон.
  • AiA_i — коэффициент активности (например, 1.2, если ученик участвовал в турнирах, иначе 1.0).
  • log⁡10(N+10)\log_{10}(N + 10) — логарифмический множитель масштаба. Он дает небольшое преимущество крупным школам, но не позволяет им доминировать только за счет численности.

Пример расчета: Школа А (100 учеников, средний XP 500): 500×log⁡10(110)≈500×2.04=1020500 \times \log_{10}(110) \approx 500 \times 2.04 = 1020. Школа Б (10 учеников, средний XP 2000): 2000×log⁡10(20)≈2000×1.3=26002000 \times \log_{10}(20) \approx 2000 \times 1.3 = 2600.

Вывод: Маленькая, но очень активная школа может победить большую, но пассивную. Это создает честную мотивацию.

4. Frontend: Динамический интерфейс на React

Пользователь не должен видеть кнопки, которые ему недоступны. В React мы используем Context API для хранения состояния пользователя.

Создайте компонент frontend/src/components/ProtectedRoute.jsx:

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

const ProtectedRoute = ({ allowedRoles }) => {
  const { user, role } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (allowedRoles && !allowedRoles.includes(role)) {
    return <Navigate to="/dashboard" replace />;
  }

  return <Outlet />;
};

export default ProtectedRoute;

Это позволит вам строить маршрутизацию чисто и профессионально:

<Route element={<ProtectedRoute allowedRoles={['teacher', 'admin']} />}>
  <Route path="/create-course" element={<CreateCoursePage />} />
</Route>

5. Функционал для Родителей и AI-ассистент

Родители (Опекуны) играют роль наблюдателей и спонсоров. В нашей базе данных связь family_links позволяет родителю видеть student_id.

AI-ассистент для анализа прогресса

Мы будем использовать AI не просто как чат-бот, а как аналитика. Когда родитель заходит в профиль ребенка, бэкенд отправляет агрегированные данные (оценки, посещаемость) в AI-сервис (например, OpenAI API или локальную модель), который генерирует отчет.

Пример промпта, который ваш Node.js сервер будет отправлять AI:

"Ученик Иван имеет средний балл 4.2, но за последнюю неделю пропустил 3 занятия по математике. Его интерес — геймдев. Посоветуй родителю, как его мотивировать."

Согласно Илье Егорову, AI-агенты способны анализировать данные о пользователе и предлагать индивидуальные задания, что решает проблему недостатка персонального внимания.

6. Git: Фиксация прогресса

Чтобы преподаватель увидел вашу работу, зафиксируем изменения. Мы добавили сложные таблицы и логику защиты.

git add .
git commit -m "feat: add institution hierarchy and RBAC middleware"
git push origin main

Не забывайте делать коммиты часто! Например, после создания таблиц — один коммит, после написания middleware — другой. Это покажет процесс мышления.

Итоги

В этой статье мы реализовали сложную социальную структуру платформы:

  1. Создали SQL-схему для связей «Учреждение-Участник» и «Родитель-Ребенок», позволяющую пользователю иметь разные роли в разных контекстах.
  2. Написали Node.js Middleware, который защищает маршруты API и проверяет иерархию (директор может управлять учителем, но не наоборот).
  3. Разработали математическую модель (GRI) для честного рейтинга учреждений, используя логарифмическое сглаживание.
  4. Настроили React Protected Routes, чтобы интерфейс адаптировался под роль пользователя.
  5. Интегрировали концепцию AI-аналитики для родителей, чтобы повысить вовлеченность семьи в образование.

LMS-функционал: конструктор курсов, планирование и трекинг задач

LMS-функционал: конструктор курсов, планирование и трекинг задач

В предыдущих частях мы построили фундамент: настроили архитектуру (Node.js + Supabase), внедрили Git-флоу и реализовали сложную ролевую модель (RBAC). Теперь пришло время наполнить платформу смыслом. LMS (Learning Management System) — это, прежде всего, управление контентом и прогрессом.

Согласно flaton.systems, современная LMS должна автоматизировать рутину: отправку уведомлений, проверку тестов и формирование отчетов. Мы пойдем дальше и реализуем конструктор курсов, персонализированный трекер задач (в стиле Trello) и умную систему оценивания.

1. Архитектура учебного контента

Чтобы учитель мог создавать курсы, а ученик — видеть структуру «на 3-5 тем вперед», нам нужна иерархическая база данных. Простая таблица courses не подойдет. Мы используем структуру: Курс →\rightarrow Модуль →\rightarrow Урок →\rightarrow Задание.

Схема данных в Supabase

Выполните следующий SQL-скрипт для создания скелета контента:

-- Модули курса (например, "Введение", "Основы React")
create table public.course_modules (
  id uuid default gen_random_uuid() primary key,
  course_id uuid references public.courses(id) on delete cascade,
  title text not null,
  order_index int not null, -- порядок вывода
  created_at timestamp with time zone default now()
);

-- Уроки внутри модулей
create type lesson_type as enum ('video', 'text', 'quiz', 'webinar');

create table public.lessons (
  id uuid default gen_random_uuid() primary key,
  module_id uuid references public.course_modules(id) on delete cascade,
  title text not null,
  content text, -- Markdown текст или ссылка на видео
  type lesson_type default 'text',
  is_free boolean default false, -- доступно ли для предпросмотра
  order_index int not null
);

-- Задания и критерии оценки
create table public.assignments (
  id uuid default gen_random_uuid() primary key,
  lesson_id uuid references public.lessons(id) on delete cascade,
  max_score int default 100,
  weight numeric(3,2) default 1.0, -- вес задания в итоговой оценке
  criteria jsonb -- JSON с критериями (например, {"clean_code": 10, "logic": 20})
);

Такая структура позволяет учителю открывать доступ к темам наперед, просто меняя флаг видимости или дату публикации, сохраняя при этом строгую последовательность обучения.

2. Персонализация и AI-трекер задач (Student Planner)

Одной из ключевых фич вашего проекта является возможность студента управлять своим обучением как проектом. Мы реализуем Kanban-доску (аналог Trello) прямо внутри LMS.

Как отмечает Сережа Рис, просто знать средний балл недостаточно — важно понимать контекст и цель студента. Наш планировщик будет учитывать дедлайны и приоритеты.

Таблица задач студента

create type task_status as enum ('todo', 'in_progress', 'review', 'done');
create type task_priority as enum ('low', 'medium', 'high', 'critical');

create table public.student_tasks (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references public.profiles(id) on delete cascade,
  course_id uuid references public.courses(id),
  title text not null,
  status task_status default 'todo',
  priority task_priority default 'medium',
  due_date timestamp with time zone,
  ai_suggested boolean default false -- создана ли задача ИИ
);

AI-ассистент для планирования (Node.js)

Мы создадим эндпоинт, где AI анализирует расписание ученика и генерирует план на неделю. Это покажет преподавателю вашу работу с алгоритмами.

Пример логики контроллера (backend/src/controllers/aiPlanner.js):

// Псевдокод логики генерации задач
const generateStudyPlan = async (studentId) => {
  // 1. Получаем курсы и дедлайны студента из БД
  const courses = await getStudentCourses(studentId);

  // 2. Формируем промпт для AI
  const prompt = `
    У студента 3 активных курса.
    Курс А: дедлайн через 2 дня (вес оценки 0.8).
    Курс Б: дедлайн через 5 дней (вес оценки 0.2).
    Составь список задач с приоритетами 'high' и 'medium'.
  `;

  // 3. AI возвращает JSON с задачами, мы сохраняем их в student_tasks
  // ... сохранение в БД
};

3. Математика оценивания и прогресса

В серьезной LMS оценка за курс не может быть просто средним арифметическим. У контрольной работы вес должен быть выше, чем у домашнего задания. Мы реализуем взвешенную систему оценивания.

Формула расчета итогового балла (GfinalG_{final}):

Gfinal=∑i=1n(Si×Wi)∑i=1nWiG_{final} = \frac{\sum_{i=1}^{n} (S_i \times W_i)}{\sum_{i=1}^{n} W_i}

Где:

  • GfinalG_{final} — итоговая оценка за курс.
  • nn — количество выполненных заданий.
  • SiS_i — оценка, полученная учеником за ii-е задание (Score).
  • WiW_i — вес ii-го задания, установленный учителем (Weight).
  • ∑\sum — знак суммирования.

Пример: Ученик получил 100 баллов за тест (вес 0.2) и 80 баллов за экзамен (вес 0.8). Обычное среднее: (100+80)/2=90(100+80)/2 = 90. Взвешенное: (100×0.2)+(80×0.8)0.2+0.8=20+641=84\frac{(100 \times 0.2) + (80 \times 0.8)}{0.2 + 0.8} = \frac{20 + 64}{1} = 84.

Вывод: Экзамен важнее, поэтому итоговая оценка ниже. Это более справедливая система, которую оценят учителя.

Расчет прогресса (Геймификация)

Для геймификации (уровни, XP) мы будем использовать данные о завершенных уроках. Согласно Mirapolis, геймификация повышает вовлеченность, если правила прозрачны.

Формула прогресса курса (PP):

P=(LpassedLtotal)×100%P = \left( \frac{L_{passed}}{L_{total}} \right) \times 100\%

Где PP — прогресс в процентах, LpassedL_{passed} — количество пройденных уроков, LtotalL_{total} — общее количество уроков в курсе.

4. Интерфейс для Родителей и Учителей

Родители должны видеть не технические логи, а понятную сводку. Мы используем SQL Views (представления) в Supabase, чтобы упростить запросы с фронтенда.

SQL View для родительского дашборда

create view parent_dashboard as
select
  s.id as student_id,
  s.full_name as student_name,
  c.title as course_title,
  cp.progress_percent,
  avg(g.score) as average_score,
  last_seen.online_at
from family_links fl
join profiles s on fl.student_id = s.id
join course_progress cp on s.id = cp.student_id
join courses c on cp.course_id = c.id
left join grades g on s.id = g.student_id and g.course_id = c.id
left join auth.users last_seen on s.id = last_seen.id
where fl.is_confirmed = true
group by s.id, c.id, cp.progress_percent, last_seen.online_at;

Теперь на фронтенде (React) родителю достаточно сделать запрос: supabase.from('parent_dashboard').select('*').

5. Реализация на React (Frontend)

Для реализации конструктора курсов и Kanban-доски нам понадобятся библиотеки для Drag-and-Drop. Установим их:

npm install @hello-pangea/dnd react-beautiful-dnd

Компонент KanbanBoard (структура)

Создайте файл frontend/src/components/Planner/KanbanBoard.jsx. Это покажет, что вы умеете работать со сложным UI.

import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';

const KanbanBoard = ({ tasks, updateTaskStatus }) => {
  // tasks - массив задач из БД
  // updateTaskStatus - функция отправки нового статуса на бэкенд

  const onDragEnd = (result) => {
    if (!result.destination) return;
    const { draggableId, destination } = result;
    // Логика обновления статуса задачи
    updateTaskStatus(draggableId, destination.droppableId);
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className="flex gap-4">
        {['todo', 'in_progress', 'done'].map(status => (
          <Droppable key={status} droppableId={status}>
            {(provided) => (
              <div ref={provided.innerRef} {...provided.droppableProps} className="bg-gray-100 p-4 rounded">
                <h3 className="font-bold mb-4 uppercase">{status}</h3>
                {/* Рендер задач */}
                {provided.placeholder}
              </div>
            )}
          </Droppable>
        ))}
      </div>
    </DragDropContext>
  );
};

6. Фиксация в Git

Мы добавили много функционала. Разделим коммиты логически:

git add .
git commit -m "feat(db): add course modules, lessons and assignments schema"
git commit -m "feat(planner): implement student task tracker table"
git commit -m "feat(ui): add Kanban board component for student planner"
git push origin main

Итоги

В этой статье мы превратили платформу из «пустой оболочки» в рабочий инструмент:

  1. Спроектировали иерархическую структуру курсов (Модули/Уроки), позволяющую открывать контент поэтапно.
  2. Реализовали Student Planner — персональный трекер задач с возможностью AI-генерации плана.
  3. Внедрили математическую модель взвешенного оценивания, где экзамены влияют на рейтинг сильнее, чем тесты.
  4. Создали SQL View для родителей, обеспечив им прозрачный доступ к успехам детей без сложных запросов.
  5. Подготовили React-компоненты для визуального управления задачами (Drag-and-Drop).

Геймификация, турниры на Redis и интеграция AI-ассистента

Геймификация, турниры на Redis и интеграция AI-ассистента

В предыдущих статьях мы создали надежный бэкенд на Node.js, настроили базу данных Supabase и реализовали планировщик задач. Теперь пришло время превратить нашу LMS из скучного инструмента в увлекательную платформу. Мы внедрим геймификацию, турниры реального времени и умного AI-наставника.

Согласно Mirapolis, геймификация позволяет перенести обучение в среду, максимально приближенную к реальной, где ошибки воспринимаются как часть опыта. Чтобы реализовать это технически грамотно и получить высокую оценку за проект, мы будем использовать Redis для быстрых рейтингов и LLM (Large Language Models) для персонализации.

1. Redis: Высокопроизводительные таблицы лидеров

Почему не PostgreSQL? Представьте, что у вас идет турнир между школами, и 10 000 учеников одновременно решают задачи. Если мы будем каждый раз делать SELECT count(*) FROM grades ORDER BY score DESC, база данных «ляжет».

Для задач реального времени идеально подходит Redis и его структура данных Sorted Set (ZSET). Она хранит уникальные элементы, отсортированные по очкам, и позволяет получать ранг пользователя за O(log⁡N)O(\log N).

Установка и настройка клиента

В папке backend установите клиент Redis:

npm install ioredis

Создайте файл backend/src/config/redis.js:

const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

redis.on('connect', () => console.log('Redis connected 🚀'));
redis.on('error', (err) => console.error('Redis error', err));

module.exports = redis;

Логика начисления очков

Создадим сервис для обновления рейтинга. Когда ученик сдает задание, мы обновляем его счет в Redis.

// backend/src/services/leaderboardService.js
const redis = require('../config/redis');

const updateScore = async (tournamentId, userId, points) => {
  const key = `tournament:${tournamentId}:leaderboard`;
  // zincrby увеличивает счет пользователя на points
  await redis.zincrby(key, points, userId);
};

const getTopUsers = async (tournamentId, limit = 10) => {
  const key = `tournament:${tournamentId}:leaderboard`;
  // zrevrange возвращает топ пользователей от большего к меньшему
  // withScores: true также вернет количество очков
  return await redis.zrevrange(key, 0, limit - 1, 'WITHSCORES');
};

const getUserRank = async (tournamentId, userId) => {
  const key = `tournament:${tournamentId}:leaderboard`;
  // zrevrank возвращает позицию (0 - первое место)
  const rank = await redis.zrevrank(key, userId);
  return rank !== null ? rank + 1 : null;
};

module.exports = { updateScore, getTopUsers, getUserRank };

2. Математика турниров и стриков

Просто суммировать баллы скучно. Чтобы мотивировать учеников заниматься регулярно, мы внедрим систему стриков (серий побед/дней).

Формула начисления очков за действие с учетом серии (SactionS_{action}):

Saction=B×(1+k×min⁡(Nstreak,10))S_{action} = B \times (1 + k \times \min(N_{streak}, 10))

Где:

  • SactionS_{action} — итоговые очки за выполненное действие.
  • BB — базовые баллы за задание (например, 100 XP).
  • kk — коэффициент бонуса (например, 0.1 или 10%).
  • NstreakN_{streak} — текущая длина серии (количество дней подряд).
  • min⁡(Nstreak,10)\min(N_{streak}, 10) — ограничитель, чтобы бонус не стал бесконечным (максимум x2 при 10 днях).

Пример: Ученик решает задачу (100 баллов) на 5-й день подряд. S=100×(1+0.1×5)=100×1.5=150S = 100 \times (1 + 0.1 \times 5) = 100 \times 1.5 = 150 очков.

Это стимулирует возвращаться на платформу каждый день.

3. Интеграция AI-ассистента (Mentor AI)

Просто подключить ChatGPT недостаточно. Как пишет Сережа Рис, главное — это контекст. AI должен знать не только вопрос студента, но и его цель, слабые темы и текущий прогресс.

Мы создадим эндпоинт, который собирает «досье» на ученика перед отправкой запроса в LLM. Это называется RAG (Retrieval Augmented Generation) на минималках.

Подготовка контекста (Node.js)

// backend/src/controllers/aiController.js
const { OpenAI } = require('openai');
const supabase = require('../config/supabase');

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const askMentor = async (req, res) => {
  const { userId, message } = req.body;

  // 1. Получаем профиль и последние оценки
  const { data: profile } = await supabase
    .from('profiles')
    .select('full_name, gamification_stats(level)')
    .eq('id', userId)
    .single();

  const { data: grades } = await supabase
    .from('grades')
    .select('score, course:courses(title)')
    .eq('student_id', userId)
    .limit(5);

  // 2. Формируем системный промпт
  const systemPrompt = `
    Ты — AI-наставник в образовательной платформе.
    Ученик: ${profile.full_name}, Уровень: ${profile.gamification_stats.level}.
    Последние успехи: ${JSON.stringify(grades)}.
    Твоя цель: мотивировать и объяснять сложные темы простым языком.
    Не давай прямых ответов на тесты, а наводи на мысль.
  `;

  // 3. Запрос к LLM
  const completion = await openai.chat.completions.create({
    messages: [
      { role: "system", content: systemPrompt },
      { role: "user", content: message }
    ],
    model: "gpt-4o-mini",
  });

  res.json({ reply: completion.choices[0].message.content });
};

Такой подход решает проблему галлюцинаций и оторванности от контекста, о которой часто упоминают при разработке AI-агентов (согласно Петру Трипольскому).

4. Frontend: Турнирная таблица и Чат

На клиенте (React) нам нужно красиво отобразить данные. Для турнирной таблицы используем анимацию, чтобы смена мест выглядела динамично.

Компонент Leaderboard (React)

import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion'; // npm install framer-motion

const Leaderboard = ({ tournamentId }) => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // Периодический опрос (polling) каждые 10 секунд
    const fetchLeaderboard = async () => {
      const res = await fetch(`/api/tournaments/${tournamentId}/leaderboard`);
      const data = await res.json();
      setUsers(data);
    };

    fetchLeaderboard();
    const interval = setInterval(fetchLeaderboard, 10000);
    return () => clearInterval(interval);
  }, [tournamentId]);

  return (
    <div className="bg-white p-6 rounded-xl shadow-lg">
      <h2 className="text-2xl font-bold mb-4">🏆 Топ учеников</h2>
      <ul>
        {users.map((user, index) => (
          <motion.li
            key={user.id}
            layout // Автоматическая анимация перемещения
            className="flex justify-between p-3 border-b"
          >
            <span className="font-bold text-gray-600">#{index + 1}</span>
            <span>{user.name}</span>
            <span className="text-blue-600 font-bold">{user.score} XP</span>
          </motion.li>
        ))}
      </ul>
    </div>
  );
};

5. Git: Фиксация результата

Ваш репозиторий должен отражать профессиональный подход. Разделим изменения на логические блоки.

# 1. Настройка Redis
git add backend/src/config/redis.js
git commit -m "chore: setup redis connection"

# 2. Логика лидербордов
git add backend/src/services/leaderboardService.js
git commit -m "feat(gamification): implement redis sorted sets for leaderboards"

# 3. AI ассистент
git add backend/src/controllers/aiController.js
git commit -m "feat(ai): add context-aware mentor assistant"

# 4. Frontend компоненты
git add frontend/src/components/Leaderboard.jsx
git commit -m "feat(ui): add animated leaderboard component"

git push origin main

Итоги

В этой статье мы значительно расширили функционал платформы:

  1. Подключили Redis и использовали структуру Sorted Set для создания молниеносных рейтингов, которые не нагружают основную базу данных.
  2. Внедрили математическую модель стриков, мотивирующую студентов заниматься ежедневно через умножение баллов.
  3. Реализовали AI-наставника с контекстом: он знает имя ученика и его оценки, что делает общение персонализированным.
  4. Создали динамический UI компонент лидерборда с анимацией перемещения позиций.

Разработка UI/UX дизайна, монетизация и подготовка к демо

Разработка UI/UX дизайна, монетизация и подготовка к демо

Мы завершили разработку архитектуры, базы данных и логики геймификации. Теперь наша задача — «упаковать» сложный бэкенд в привлекательный интерфейс и подготовить проект к защите. Для получения высокой оценки важно не только то, как код работает, но и как он выглядит и продается.

1. UI/UX для разных ролей: Принцип адаптивности

В нашей LMS четыре роли (Ученик, Учитель, Родитель, Учреждение), и для каждой нужен свой подход к интерфейсу. Согласно purrweb.com, типичное приложение для обучения обязано включать дашборды, но их наполнение должно кардинально отличаться.

Студент: Геймификация и фокус

Интерфейс студента должен напоминать игру, а не Excel-таблицу. Используйте яркие акценты для прогресс-баров и наград.

  • Главный экран: Текущие задачи (Kanban), ближайшие дедлайны, уровень XP.
  • Микровзаимодействия: Анимация при получении очков (используйте библиотеку framer-motion).
  • Onboarding: Как отмечает ya.zerocoder.ru, интерактивные туториалы удерживают новичков лучше, чем статичные инструкции. Реализуйте всплывающие подсказки при первом входе.

Учреждение и Учитель: Аналитика и управление

Здесь важна плотность информации. Используйте табличные представления (Data Grids) с фильтрами.

  • Компоненты: Используйте готовую библиотеку компонентов, например, Shadcn/UI или Mantine. Это сэкономит время и придаст проекту профессиональный вид.
  • Цветовая схема: Более строгая, «корпоративная» (синий, серый, белый).

2. Монетизация: Бизнес-логика платформы

Чтобы проект выглядел как стартап, а не лабораторная работа, нужно внедрить финансовую модель. В нашем случае это гибридная модель: SaaS (подписка для учреждений) + Marketplace (комиссия с продаж курсов учителей).

Схема данных для финансов

Добавим таблицу транзакций в Supabase:

create type transaction_type as enum ('subscription', 'course_purchase', 'payout');

create table public.transactions (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references public.profiles(id), -- кто платит
  recipient_id uuid references public.profiles(id), -- кто получает (учитель/платформа)
  amount numeric(10, 2) not null,
  commission_amount numeric(10, 2) default 0,
  type transaction_type not null,
  status text default 'completed',
  created_at timestamp with time zone default now()
);

Математика комиссии (Marketplace)

Когда ученик покупает курс у учителя, платформа удерживает комиссию. Рассчитаем чистую прибыль учителя (PteacherP_{teacher}).

Pteacher=Cprice×(1−kcom)−FtransP_{teacher} = C_{price} \times (1 - k_{com}) - F_{trans}

Где:

  • PteacherP_{teacher} — сумма, которую получает учитель на баланс.
  • CpriceC_{price} — стоимость курса, установленная учителем.
  • kcomk_{com} — коэффициент комиссии платформы (например, 0.150.15 для 15%).
  • FtransF_{trans} — фиксированная комиссия платежного шлюза (например, 0.30$ за транзакцию).

Пример расчета: Учитель продает курс за 100$. Комиссия платформы 15%. Фикс шлюза 0.3$. Pteacher=100×(1−0.15)−0.3=100×0.85−0.3=84.7P_{teacher} = 100 \times (1 - 0.15) - 0.3 = 100 \times 0.85 - 0.3 = 84.7 долларов.

Расчет стоимости подписки для учреждения (SaaS)

Для учреждений (школ/вузов) цена часто зависит от количества учеников. Используем ступенчатую функцию стоимости (CosttotalCost_{total}).

Costtotal=Bplan+max⁡(0,Nstudents−Llimit)×CextraCost_{total} = B_{plan} + \max(0, N_{students} - L_{limit}) \times C_{extra}

Где:

  • CosttotalCost_{total} — итоговая цена подписки в месяц.
  • BplanB_{plan} — базовая цена тарифа (например, 500$ за тариф "Pro").
  • NstudentsN_{students} — текущее количество учеников в учреждении.
  • LlimitL_{limit} — лимит учеников, включенных в базовый тариф (например, 100 человек).
  • CextraC_{extra} — цена за каждого дополнительного ученика (например, 2$).

Пример: Школа на тарифе "Pro" (500$, лимит 100) имеет 150 учеников. Costtotal=500+max⁡(0,150−100)×2=500+50×2=600Cost_{total} = 500 + \max(0, 150 - 100) \times 2 = 500 + 50 \times 2 = 600 долларов.

3. Подготовка к Демо: «Happy Path»

Самая частая ошибка студентов — показывать пустую базу данных. Для высокой оценки платформа должна выглядеть живой.

Генерация данных (Seeding)

Напишите скрипт на Node.js, используя библиотеку @faker-js/faker, который заполнит базу:

  1. Создаст 1 школу и 1 университет.
  2. Зарегистрирует 5 учителей и 20 учеников.
  3. Сгенерирует 3 курса с уроками и заданиями.
  4. Важно: Создаст историю активности (прошлые оценки, выполненные задачи), чтобы графики на дашборде не были пустыми.

Сценарий показа (Script)

Не кликайте хаотично. Строго следуйте сценарию «Happy Path» (Счастливый путь):

  1. Роль Администратора школы: Заходим, видим дашборд с рейтингом (RinstR_{inst} из прошлой статьи), добавляем учителя.
  2. Роль Учителя: Создаем курс, назначаем цену, публикуем задание.
  3. Роль Ученика: Видим уведомление, покупаем курс (эмуляция), проходим тест, получаем XP и ачивку (анимация).
  4. Роль Родителя: Заходим, видим, что ребенок получил ачивку, отправляем награду.
  5. Финал: Показываем Leaderboard в реальном времени (Redis).

4. Финализация Git-репозитория

Ваш репозиторий — это ваше лицо. Перед сдачей приведите его в порядок.

  1. README.md: Это первое, что видит проверяющий. Он должен содержать:
    • Название и краткое описание (Elevator Pitch).
    • Скриншоты интерфейса (обязательно!).
    • Инструкцию по запуску (npm install && npm run dev).
    • Стек технологий.
  2. Clean Code: Удалите console.log, закомментированный код и неиспользуемые файлы.
  3. Финальный коммит:
git add .
git commit -m "chore: cleanup project, add seeding script and update readme"
git push origin main

Итоги

В этом завершающем модуле мы подготовили проект к релизу:

  1. Разработали UI-стратегию: геймификация для студентов и строгие дашборды для администрации.
  2. Внедрили монетизацию: рассчитали формулы комиссии для маркетплейса и динамического ценообразования для SaaS-подписки.
  3. Спроектировали таблицу transactions для финансового учета.
  4. Составили план Demo-показа с использованием скрипта генерации данных (Seeding), чтобы продемонстрировать «живую» систему.