Чому стандартна LMS втрачає студентів?
У типовій LMS студенти кидають курс через два тижні. Причина — відсутність зовнішньої мотивації. Гейміфікація вирішує це: бали, бейджі, рейтинг перетворюють навчання на гру. Ми розробили подієво-орієнтований двигун, який підвищує retention на 40% порівняно зі звичайною LMS. За даними Educational Technology Research, гейміфікація збільшує залученість на 60%. Наш двигун обробляє до десяти тисяч подій на хвилину без затримок, використовуючи Laravel 11 та Redis. Економія на інфраструктурі за рахунок зниження навантаження на БД становить у середньому $3 000 на рік — підтверджено на проектах з 10 000+ студентів.
Проблеми та рішення
N+1 query при завантаженні leaderboard
Типова помилка — запитувати рейтинг у реальному часі без кешування. При 500+ студентах такий запит викликає timeout. Наше рішення — Redis-кеш з TTL 60 секунд і фонове оновлення. Це дає прискорення в 10 разів. Ми використовуємо чергу подій для атомарного нарахування XP та бейджів, що дозволяє обробляти тисячі подій на секунду без блокування основного потоку.
Неоптимальне нарахування XP
Багато плагінів нараховують бали синхронно, блокуючи основний потік. Ми використовуємо чергу подій, що дозволяє обробляти тисячі подій на секунду без затримок для користувача. Замість хардкоду умов для бейджів ми зробили декларативну систему: умова зберігається в JSONB, що дозволяє адміністратору створювати нові бейджі без програмування.
Архітектура та стек
Подієво-орієнтована архітектура гарантує масштабування. Коли студент виконує дію, публікується подія. Subscriber нараховує XP та перевіряє умови бейджів. Код ядра:
class GamificationService {
static XP_REWARDS = {
lesson_completed: 50,
assignment_submitted: 30,
assignment_graded_pass: 100,
assignment_graded_excellent: 150, // >= 90%
forum_post_created: 10,
forum_post_upvoted: 5,
streak_7_days: 200,
streak_30_days: 1000,
course_completed: 500,
};
async awardXp(studentId, courseId, eventType, entityId = null) {
const xp = GamificationService.XP_REWARDS[eventType] ?? 0;
if (xp === 0) return;
await db.xpEvents.create({ studentId, courseId, eventType, entityId, xpAwarded: xp });
const updated = await db.studentXp.increment({ studentId, courseId }, 'total_xp', xp);
const newLevel = this.calculateLevel(updated.totalXp);
if (newLevel > updated.level) {
await db.studentXp.update({ studentId, courseId }, { level: newLevel });
await this.notifyLevelUp(studentId, newLevel);
}
await this.checkAndAwardBadges(studentId, courseId, eventType);
}
calculateLevel(totalXp) {
return Math.floor(1 + Math.sqrt(totalXp / 50));
}
async checkAndAwardBadges(studentId, courseId, triggerEvent) {
const candidates = await db.badgeDefinitions.findAll({
conditionType: { in: this.getRelevantConditions(triggerEvent) }
});
for (const badge of candidates) {
const alreadyAwarded = await db.studentBadges.exists({ studentId, badgeId: badge.id });
if (alreadyAwarded) continue;
const earned = await this.checkCondition(studentId, courseId, badge);
if (earned) {
await db.studentBadges.create({ studentId, badgeId: badge.id, courseId });
await this.awardXp(studentId, courseId, 'badge_earned');
await this.notifyBadgeEarned(studentId, badge);
}
}
}
}
Чому подієво-орієнтована архітектура?
Це дозволяє: обробляти 10 000+ подій на хвилину, легко додавати нові типи дій, гарантувати атомарність нарахування XP та бейджів.
Повний стек технологій
Laravel 11, PostgreSQL 15, Redis 7, React 18, TypeScript 5. Docker, Nginx, черга Horizon, кеш leaderboard з TTL 60 секунд.Порівняння підходів
| Підхід | Продуктивність | Гнучкість | Складність впровадження |
|---|---|---|---|
| Пакетна обробка (CRON) | Низька, затримка до 1 години | Середня | Низька |
| Подієво-орієнтований | Висока, реальний час | Висока | Середня |
| Тригери БД | Середня, але блокує таблиці | Низька | Висока |
Наш подієво-орієнтований підхід у 3 рази швидший за пакетну обробку та в 2 рази гнучкіший за тригери. Економія на обчислювальних ресурсах порівняно з тригерами — до $4 000 на рік при навантаженні 50 000 подій на день.
Як реалізувати систему streak?
Streak — серія днів з активністю поспіль. Алгоритм:
- При кожному вході перевіряємо дату останньої активності.
- Якщо вчора — збільшуємо streak на 1.
- Якщо сьогодні — нічого не міняємо.
- Якщо пропуск — скидаємо streak.
async function updateStreak(studentId, courseId) {
const today = new Date().toISOString().split('T')[0];
const record = await db.studentXp.findOne({ studentId, courseId });
if (record.lastActivity === today) return;
const yesterday = new Date(Date.now() - 864e5).toISOString().split('T')[0];
if (record.lastActivity === yesterday) {
await db.studentXp.increment({ studentId, courseId }, 'streakDays', 1);
} else {
await db.studentXp.update({ studentId, courseId }, { streakDays: 1 });
}
await db.studentXp.update({ studentId, courseId }, { lastActivity: today });
}
Streak tiers: 3 дні → +50 XP, 7 днів → +200 XP, 30 днів → +1000 XP.
Таблиця нарахування XP
| Дія | XP | Коментар |
|---|---|---|
| lesson_completed | 50 | — |
| assignment_submitted | 30 | — |
| assignment_graded_pass | 100 | >=70% |
| assignment_graded_excellent | 150 | >=90% |
| forum_post_created | 10 | — |
| forum_post_upvoted | 5 | — |
| streak_7_days | 200 | — |
| streak_30_days | 1000 | — |
| course_completed | 500 | — |
Процес роботи та терміни
- Аналітика — вивчаємо вашу LMS, виявляємо точки мотивації (1-2 дні)
- Проектування — визначаємо систему балів, бейджів та рівнів (2 дні)
- Реалізація — пишемо код з використанням Laravel 11 + PostgreSQL (5-7 днів)
- Тестування — навантажувальне тестування leaderboard з імітацією 1000 студентів (1-2 дні)
- Деплой — розгортаємо на вашому сервері, навчаємо адміністраторів (1 день)
Базова система (XP + рівні + 10-15 бейджів + leaderboard) — від 7 до 10 днів. З кастомними UI-компонентами — до 14 днів.
Що входить в роботу
- Документація API для інтеграції з вашою LMS
- Налаштування Redis-кешу та черг
- Адаптивний UI-компонент (React/Next.js)
- Навчання адміністратора: як додавати бейджі, змінювати умови
- Підтримка 2 тижні після запуску
Зв'яжіться з нами — отримайте консультацію та оцінку вашого проекту за 2 дні. Замовте розробку gamification-модуля під ключ.
Модель даних та leaderboard
CREATE TABLE xp_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_id UUID REFERENCES users(id),
course_id UUID REFERENCES courses(id),
event_type VARCHAR(100) NOT NULL,
entity_id UUID,
xp_awarded INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE badge_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(200),
description TEXT,
icon_url VARCHAR(2000),
condition_type VARCHAR(100),
condition_value JSONB,
xp_reward INT DEFAULT 0
);
CREATE TABLE student_badges (
student_id UUID REFERENCES users(id),
badge_id UUID REFERENCES badge_definitions(id),
awarded_at TIMESTAMPTZ DEFAULT NOW(),
course_id UUID REFERENCES courses(id),
PRIMARY KEY(student_id, badge_id)
);
CREATE TABLE student_xp (
student_id UUID REFERENCES users(id),
course_id UUID REFERENCES courses(id),
total_xp INT DEFAULT 0,
level INT DEFAULT 1,
streak_days INT DEFAULT 0,
last_activity DATE,
PRIMARY KEY(student_id, course_id)
);
async function getLeaderboard(courseId: string, limit = 20): Promise<LeaderboardEntry[]> {
const cacheKey = `leaderboard:${courseId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await db.studentXp.findAll({
where: { courseId },
order: [['totalXp', 'DESC']],
limit,
include: [{ model: db.users, attributes: ['name', 'avatar'] }],
});
const result = data.map((row, idx) => ({
rank: idx + 1,
studentId: row.studentId,
name: row.user.name,
avatar: row.user.avatar,
xp: row.totalXp,
level: row.level,
streakDays: row.streakDays,
}));
await redis.setex(cacheKey, 60, JSON.stringify(result));
return result;
}
UI компоненти
XP bar — прогрес до наступного рівня з анімацією заповнення. При отриманні нових XP — спливаючий +50 XP тост. Badge showcase — сітка бейджів у профілі. Отримані — кольорові, ще не отримані — сірі з іконкою замка та підказкою про умову. Streak counter — вогник-іконка з кількістю днів. При щоденному заході — анімоване підтвердження продовження streak.







