This commit is contained in:
silverpro89
2026-01-20 20:29:07 +07:00
parent 97e2e8402e
commit 53d97ba5db
12 changed files with 3461 additions and 20 deletions

View File

@@ -247,7 +247,325 @@ const lesson4 = await Lesson.create({
}); });
``` ```
### Bước 3c: Các loại Content JSON khác ### Bước 3c: Composite Lesson (Nhiều thành phần)
```javascript
// Lesson có nhiều components: story, game, results, leaderboard
const compositeLesson = await Lesson.create({
chapter_id: chapter1.id,
lesson_number: 5,
lesson_title: 'Bài học tổng hợp: Đếm số và so sánh',
lesson_type: 'json_content',
lesson_description: 'Bài học tích hợp nhiều hoạt động: xem câu chuyện, chơi game, xem kết quả',
content_json: {
type: 'composite_lesson', // Type đặc biệt cho multi-component
version: '2.0',
layout: 'vertical', // vertical, horizontal, tabs
components: [
// Component 1: Story Game (Câu chuyện tương tác)
{
id: 'story-1',
type: 'story_game',
order: 1,
title: 'Câu chuyện: Gấu đếm quả táo',
config: {
skippable: true,
auto_play: false,
show_subtitles: true
},
content: {
story_id: 'bear-counting-story',
scenes: [
{
scene_id: 1,
background: 'https://cdn.senaai.tech/scenes/forest.jpg',
character: 'bear',
dialogue: 'Chào các em! Hôm nay chú gấu sẽ dạy các em đếm táo.',
audio: 'https://cdn.senaai.tech/audio/scene-1.mp3',
duration: 5
},
{
scene_id: 2,
background: 'https://cdn.senaai.tech/scenes/apple-tree.jpg',
character: 'bear',
dialogue: 'Nhìn kìa! Có bao nhiêu quả táo trên cây?',
interactive: true,
question: {
type: 'counting',
correct_answer: 5,
hint: 'Đếm từ trái sang phải nhé!'
}
},
{
scene_id: 3,
background: 'https://cdn.senaai.tech/scenes/celebration.jpg',
dialogue: 'Chính xác! Có 5 quả táo. Giỏi lắm!',
reward_stars: 3
}
]
}
},
// Component 2: Main Game (Game chính)
{
id: 'game-1',
type: 'game',
order: 2,
title: 'Trò chơi: Thử thách đếm số',
required: true, // Bắt buộc phải hoàn thành
unlock_after: 'story-1', // Mở khóa sau khi xem story
config: {
game_type: 'counting_quiz', // Tham chiếu tới Game.type
difficulty: 'medium',
time_limit: 300,
max_attempts: 3,
show_hints: true,
save_progress: true
},
content: {
questions: [
{
id: 1,
question: 'Đếm số con vịt',
image: 'https://cdn.senaai.tech/images/ducks-7.png',
correct_answer: 7,
options: [5, 6, 7, 8],
points: 10,
time_limit: 30
},
{
id: 2,
question: 'Đếm số bông hoa',
image: 'https://cdn.senaai.tech/images/flowers-9.png',
correct_answer: 9,
options: [7, 8, 9, 10],
points: 10,
time_limit: 30
},
{
id: 3,
question: 'So sánh: Bên nào nhiều hơn?',
images: [
'https://cdn.senaai.tech/images/group-a.png',
'https://cdn.senaai.tech/images/group-b.png'
],
correct_answer: 'left',
options: ['left', 'right', 'equal'],
points: 15,
time_limit: 45
}
],
pass_score: 70,
perfect_score: 100
}
},
// Component 3: Results Board (Bảng kết quả)
{
id: 'results-1',
type: 'results_board',
order: 3,
title: 'Kết quả của bạn',
unlock_after: 'game-1',
config: {
show_comparison: true, // So sánh với lần trước
show_statistics: true, // Thống kê chi tiết
show_recommendations: true // Gợi ý học tiếp
},
content: {
display_fields: [
{
field: 'score',
label: 'Điểm số',
format: 'number',
show_progress_bar: true
},
{
field: 'accuracy',
label: 'Độ chính xác',
format: 'percentage',
color_coded: true // Đỏ/Vàng/Xanh theo %
},
{
field: 'time_spent',
label: 'Thời gian',
format: 'duration',
unit: 'seconds'
},
{
field: 'stars_earned',
label: 'Số sao đạt được',
format: 'stars',
max: 5
},
{
field: 'correct_answers',
label: 'Câu đúng/Tổng số',
format: 'fraction'
}
],
achievements: [
{
id: 'perfect_score',
name: 'Hoàn hảo',
condition: 'score === 100',
icon: 'trophy',
unlocked: false
},
{
id: 'speed_demon',
name: 'Thần tốc',
condition: 'time_spent < 120',
icon: 'lightning',
unlocked: false
}
],
recommendations: {
next_lesson_id: 'uuid-next-lesson',
practice_areas: ['counting_10_20', 'comparison'],
difficulty_adjustment: 'increase' // increase, maintain, decrease
}
}
},
// Component 4: Leaderboard (Bảng xếp hạng)
{
id: 'leaderboard-1',
type: 'leaderboard',
order: 4,
title: 'Bảng xếp hạng',
config: {
scope: 'class', // class, school, global
time_range: 'week', // day, week, month, all_time
max_entries: 50,
show_user_rank: true,
show_avatars: true,
realtime_updates: true
},
content: {
ranking_criteria: [
{
field: 'total_score',
weight: 0.5,
label: 'Tổng điểm'
},
{
field: 'completion_time',
weight: 0.3,
label: 'Thời gian hoàn thành',
sort: 'asc' // Thời gian ngắn = tốt hơn
},
{
field: 'accuracy',
weight: 0.2,
label: 'Độ chính xác'
}
],
display_columns: [
{ field: 'rank', label: '#', width: '10%' },
{ field: 'avatar', label: '', width: '10%' },
{ field: 'name', label: 'Tên', width: '30%' },
{ field: 'score', label: 'Điểm', width: '15%' },
{ field: 'time', label: 'Thời gian', width: '15%' },
{ field: 'accuracy', label: 'Độ chính xác', width: '20%' }
],
filters: [
{ field: 'class_id', label: 'Lớp học' },
{ field: 'date_range', label: 'Thời gian' }
],
badges: [
{ rank: 1, icon: 'gold-medal', color: '#FFD700' },
{ rank: 2, icon: 'silver-medal', color: '#C0C0C0' },
{ rank: 3, icon: 'bronze-medal', color: '#CD7F32' }
]
}
}
],
// Global config cho toàn bộ lesson
global_config: {
enable_navigation: true,
allow_skip: false, // Phải làm theo thứ tự
save_progress: true,
allow_replay: true,
completion_criteria: {
required_components: ['game-1'], // Chỉ cần hoàn thành game
min_score: 60,
require_all: false
}
},
// Theme/Styling
theme: {
primary_color: '#4CAF50',
background: 'https://cdn.senaai.tech/backgrounds/forest-theme.jpg',
font_family: 'Quicksand',
sound_enabled: true,
background_music: 'https://cdn.senaai.tech/music/cheerful-learning.mp3'
}
},
duration_minutes: 25,
is_published: true,
is_free: false,
display_order: 5
});
```
### Bước 3d: Drag-and-Drop Lesson Builder
```javascript
// Component Builder - Để giáo viên tự tạo lesson bằng kéo thả
const dragDropLesson = await Lesson.create({
chapter_id: chapter1.id,
lesson_number: 6,
lesson_title: 'Bài học tùy chỉnh',
lesson_type: 'json_content',
content_json: {
type: 'custom_builder',
version: '1.0',
builder_config: {
editable: true, // Giáo viên có thể chỉnh sửa
allow_reorder: true, // Cho phép sắp xếp lại
component_library: [ // Thư viện components có sẵn
'story_game',
'game',
'quiz',
'video',
'reading',
'results_board',
'leaderboard',
'discussion',
'assignment'
]
},
components: [
// Giáo viên kéo thả components vào đây
{
id: 'comp-1',
type: 'video',
order: 1,
content: {
url: 'https://youtube.com/watch?v=...',
title: 'Video giới thiệu'
}
},
{
id: 'comp-2',
type: 'quiz',
order: 2,
content: {
questions: [/* ... */]
}
}
// ... thêm components khác
]
},
duration_minutes: 30,
is_published: false // Draft
});
```
### Bước 3e: Các loại Content JSON khác
```javascript ```javascript
// Quiz trắc nghiệm // Quiz trắc nghiệm
@@ -455,26 +773,66 @@ GET /api/chapters/:chapter_id/lessons
GET /api/lessons/:lesson_id GET /api/lessons/:lesson_id
Trả về chi tiết Lesson Trả về chi tiết Lesson
// 5a. Nếu lesson_type = 'json_content' // 5. Render lesson theo type
const lessonType = lesson.content_json.type; // vd: 'counting_quiz' if (lesson.lesson_type === 'json_content') {
const lessonType = lesson.content_json.type;
// Tìm game engine phù hợp // 5a. Single Component Lesson
GET /api/games?type=counting_quiz if (lessonType !== 'composite_lesson') {
Trả về Game type khớp // Tìm game engine phù hợp
const game = await fetch(`/api/games?type=${lessonType}`);
// Load game và truyền content vào // Render game với content
<iframe <GamePlayer
src={game.url} gameUrl={game.url}
data-content={JSON.stringify(lesson.content_json)} content={lesson.content_json}
/> />
}
// 5b. Nếu lesson_type = 'url_content' // 5b. Composite Lesson (Multi Components)
if (lesson.content_type === 'youtube') { else if (lessonType === 'composite_lesson') {
const components = lesson.content_json.components;
// Render từng component theo order
components.sort((a, b) => a.order - b.order).map(comp => {
switch(comp.type) {
case 'story_game':
return <StoryPlayer content={comp.content} />;
case 'game':
// Tìm game engine theo comp.config.game_type
const game = await fetch(`/api/games?type=${comp.config.game_type}`);
return <GamePlayer gameUrl={game.url} content={comp.content} />;
case 'results_board':
return <ResultsBoard
fields={comp.content.display_fields}
achievements={comp.content.achievements}
/>;
case 'leaderboard':
return <Leaderboard
scope={comp.config.scope}
timeRange={comp.config.time_range}
criteria={comp.content.ranking_criteria}
/>;
default:
return null;
}
});
}
}
// 5c. URL Content
else if (lesson.lesson_type === 'url_content') {
if (lesson.content_type === 'youtube') {
<YouTubePlayer url={lesson.content_url} /> <YouTubePlayer url={lesson.content_url} />
} else if (lesson.content_type === 'pdf') { } else if (lesson.content_type === 'pdf') {
<PDFViewer url={lesson.content_url} /> <PDFViewer url={lesson.content_url} />
} else if (lesson.content_type === 'audio') { } else if (lesson.content_type === 'audio') {
<AudioPlayer url={lesson.content_url} /> <AudioPlayer url={lesson.content_url} />
}
} }
``` ```
@@ -594,6 +952,8 @@ Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id' });
### 1. Lesson Type Naming Convention ### 1. Lesson Type Naming Convention
```javascript ```javascript
// Đặt tên theo format: <category>_<action> // Đặt tên theo format: <category>_<action>
// Single Component Lessons
'counting_quiz' // Đếm số - Quiz 'counting_quiz' // Đếm số - Quiz
'multiple_choice_quiz' // Trắc nghiệm 'multiple_choice_quiz' // Trắc nghiệm
'math_practice' // Luyện toán 'math_practice' // Luyện toán
@@ -603,8 +963,279 @@ Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id' });
'speaking_practice' // Luyện nói 'speaking_practice' // Luyện nói
'writing_assignment' // Bài tập viết 'writing_assignment' // Bài tập viết
'project_based' // Dự án 'project_based' // Dự án
#### Single Component Lesson
```javascript
{
type: 'counting_quiz', // REQUIRED: Phải khớp với Game.type
version: '1.0', // Version của schema
metadata: { // Optional: Thông tin thêm
author: 'Nguyễn Văn A',
created_at: '2026-01-19',
tags: ['counting', 'basic', 'grade1']
},
config: {}, // Cấu hình riêng của lesson
data: {} // Dữ liệu chính (questions, tasks, etc.)
}
``` ```
#### Composite Lesson (Multi Components)
```javascript
{
type: 'composite_lesson', // Type đặc biệt
version: '2.0',
layout: 'vertical', // vertical, horizontal, tabs
components: [ // MẢNG các components
{
id: 'story-1',
type: 'story_game', // Component type
order: 1,
title: 'Câu chuyện',
required: false,
unlock_after: null, // Dependency
config: { /* ... */ },
content: { /* ... */ }
},
{
id: 'game-1',
type: 'game',
order: 2,
title: 'Trò chơi chính',
required: true,
unlock_after: 'story-1',
config: {
game_type: 'counting_quiz', // Tham chiếu Game.type
difficulty: 'medium'
},
content: {
questions: [/* ... */]
}
},
{
id: 'results-1',
type: 'results_board',
order: 3,
unlock_after: 'game-1',
content: {
display_fields: [/* ... */]
}
},
{
id: 'leaderboard-1',
type: 'leaderboard',
order: 4,
config: {
scope: 'class',
time_range: 'week'
}
}
],
global_config: {
enable_navigation: true,
allow_skip: false,
completion_criteria: {
required_components: ['game-1'],
min_score: 60
}
}
'reading' // Đọc hiểu
'discussion' // Thảo luận
'assignment' // Bài tập
```🎨 Frontend Component Examples
### Composite Lesson Renderer
```jsx
// CompositeLesson.jsx
import React, { useState } from 'react';
import StoryPlayer from './components/StoryPlayer';
import GamePlayer from './components/GamePlayer';
import ResultsBoard from './components/ResultsBoard';
import Leaderboard from './components/Leaderboard';
const CompositeLesson = ({ lesson }) => {
const [currentComponent, setCurrentComponent] = useState(0);
const [completedComponents, setCompletedComponents] = useState([]);
const components = lesson.content_json.components;
const handleComponentComplete = (componentId) => {
setCompletedComponents([...completedComponents, componentId]);
// Auto advance to next component
if (currentComponent < components.length - 1) {
setCurrentComponent(currentComponent + 1);
}
};
const isComponentUnlocked = (component) => {
if (!component.unlock_after) return true;
return completedComponents.includes(component.unlock_after);
};
return (
<div className="composite-lesson">
{/* Navigation tabs */}
<div className="component-tabs">
{components.map((comp, index) => (
<button
key={comp.id}
disabled={!isComponentUnlocked(comp)}
className={currentComponent === index ? 'active' : ''}
onClick={() => setCurrentComponent(index)}
>
{comp.title}
{completedComponents.includes(comp.id) && ' ✓'}
</button>
))}
</div>
{/* Current component */}
<div className="component-content">
{renderComponent(
components[currentComponent],
handleComponentComplete
)}
</div>
</div>
);
};
function renderComponent(component, onComplete) {
switch(component.type) {
case 'story_game':
return (
<StoryPlayer
scenes={component.content.scenes}
config={component.config}
onComplete={() => onComplete(component.id)}
/>
);
case 'game':
return (
<GamePlayer
gameType={component.config.game_type}
questions={component.content.questions}
config={component.config}
onComplete={(results) => {
// Save results to API
saveGameResults(component.id, results);
onComplete(component.id);
}}
/>
);
case 'results_board':
return (
<ResultsBoard
fields={component.content.display_fields}
achievements={component.content.achievements}
recommendations={component.content.recommendations}
/>
);
case 'leaderboard':
return (
<Leaderboard
scope={component.config.scope}
timeRange={component.config.time_range}
rankingCriteria={component.content.ranking_criteria}
displayColumns={component.content.display_columns}
/>
);
default:
return <div>Unknown component type: {component.type}</div>;
}
}
async function saveGameResults(componentId, results) {
await fetch(`/api/lessons/components/${componentId}/results`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
score: results.score,
accuracy: results.accuracy,
time_spent: results.time_spent,
answers: results.answers
})
});
}
```
---
## 📊 Database Schema cho Composite Lessons
### Tracking Student Progress
```sql
-- Bảng theo dõi progress cho từng component
CREATE TABLE lesson_component_progress (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users_auth(id),
lesson_id UUID NOT NULL REFERENCES lessons(id),
component_id VARCHAR(50) NOT NULL, -- Từ content_json.components[].id
component_type VARCHAR(50), -- story_game, game, etc.
-- Progress tracking
status VARCHAR(20) DEFAULT 'not_started', -- not_started, in_progress, completed
started_at TIMESTAMP,
completed_at TIMESTAMP,
-- Results (nếu là game/quiz)
score INTEGER,
max_score INTEGER,
accuracy DECIMAL(5,2),
time_spent INTEGER, -- seconds
attempts INTEGER DEFAULT 0,
-- Data
results_data JSONB, -- Chi tiết câu trả lời, achievements, etc.
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, lesson_id, component_id)
);
-- Indexes
CREATE INDEX idx_component_progress_user ON lesson_component_progress(user_id);
CREATE INDEX idx_component_progress_lesson ON lesson_component_progress(lesson_id);
CREATE INDEX idx_component_progress_status ON lesson_component_progress(status);
-- Bảng leaderboard (cache)
CREATE TABLE lesson_leaderboard (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
lesson_id UUID NOT NULL REFERENCES lessons(id),
user_id UUID NOT NULL REFERENCES users_auth(id),
-- Ranking data
total_score INTEGER,
accuracy DECIMAL(5,2),
completion_time INTEGER,
rank INTEGER,
-- Scope
scope VARCHAR(20), -- class, school, global
scope_id UUID, -- class_id hoặc school_id
time_range VARCHAR(20), -- day, week, month, all_time
-- Metadata
computed_at TIMESTAMP DEFAULT NOW(),
UNIQUE(lesson_id, user_id, scope, time_range)
);
CREATE INDEX idx_leaderboard_lesson_scope ON lesson_leaderboard(lesson_id, scope, time_range);
CREATE INDEX idx_leaderboard_rank ON lesson_leaderboard(rank);
```
---
##
### 2. Content JSON Structure ### 2. Content JSON Structure
```javascript ```javascript
{ {

File diff suppressed because it is too large Load Diff

5
app.js
View File

@@ -32,6 +32,8 @@ const trainingRoutes = require('./routes/trainingRoutes');
const parentTaskRoutes = require('./routes/parentTaskRoutes'); const parentTaskRoutes = require('./routes/parentTaskRoutes');
const chapterRoutes = require('./routes/chapterRoutes'); const chapterRoutes = require('./routes/chapterRoutes');
const gameRoutes = require('./routes/gameRoutes'); const gameRoutes = require('./routes/gameRoutes');
const lessonRoutes = require('./routes/lessonRoutes');
const chapterLessonRoutes = require('./routes/chapterLessonRoutes');
/** /**
* Initialize Express Application * Initialize Express Application
@@ -142,6 +144,7 @@ app.get('/api', (req, res) => {
training: '/api/training', training: '/api/training',
parentTasks: '/api/parent-tasks', parentTasks: '/api/parent-tasks',
chapters: '/api/chapters', chapters: '/api/chapters',
lessons: '/api/lessons',
games: '/api/games', games: '/api/games',
}, },
documentation: '/api-docs', documentation: '/api-docs',
@@ -192,7 +195,9 @@ app.use('/api/subscriptions', subscriptionRoutes);
app.use('/api/training', trainingRoutes); app.use('/api/training', trainingRoutes);
app.use('/api/parent-tasks', parentTaskRoutes); app.use('/api/parent-tasks', parentTaskRoutes);
app.use('/api/chapters', chapterRoutes); app.use('/api/chapters', chapterRoutes);
app.use('/api/chapters', chapterLessonRoutes); // Nested route: /api/chapters/:id/lessons
app.use('/api/games', gameRoutes); app.use('/api/games', gameRoutes);
app.use('/api/lessons', lessonRoutes);
/** /**
* Queue Status Endpoint * Queue Status Endpoint

View File

@@ -169,6 +169,105 @@ const options = {
}, },
}, },
}, },
Lesson: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'uuid',
},
chapter_id: {
type: 'string',
format: 'uuid',
},
lesson_number: {
type: 'integer',
example: 1,
},
lesson_title: {
type: 'string',
example: 'Đếm từ 1 đến 5',
},
lesson_type: {
type: 'string',
enum: ['json_content', 'url_content'],
example: 'json_content',
},
lesson_description: {
type: 'string',
},
content_json: {
type: 'object',
description: 'Nội dung bài học dạng JSON',
},
content_url: {
type: 'string',
description: 'URL nội dung (video, PDF, etc.)',
},
content_type: {
type: 'string',
example: 'youtube',
},
duration_minutes: {
type: 'integer',
example: 15,
},
is_published: {
type: 'boolean',
example: true,
},
is_free: {
type: 'boolean',
example: true,
},
display_order: {
type: 'integer',
example: 1,
},
},
},
Game: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'uuid',
},
title: {
type: 'string',
example: 'Trò chơi đếm số',
},
description: {
type: 'string',
},
url: {
type: 'string',
example: 'https://games.senaai.tech/counting/',
},
thumbnail: {
type: 'string',
},
type: {
type: 'string',
example: 'counting_quiz',
},
config: {
type: 'object',
},
is_active: {
type: 'boolean',
example: true,
},
is_premium: {
type: 'boolean',
example: false,
},
difficulty_level: {
type: 'string',
enum: ['easy', 'medium', 'hard'],
},
},
},
}, },
}, },
tags: [ tags: [
@@ -188,6 +287,18 @@ const options = {
name: 'Teachers', name: 'Teachers',
description: 'Teacher management endpoints', description: 'Teacher management endpoints',
}, },
{
name: 'Lessons',
description: 'Lesson management endpoints',
},
{
name: 'Chapter Lessons',
description: 'Get lessons by chapter',
},
{
name: 'Games',
description: 'Game engine management',
},
], ],
}, },
apis: ['./controllers/*.js', './routes/*.js'], // Path to the API docs apis: ['./controllers/*.js', './routes/*.js'], // Path to the API docs

View File

@@ -0,0 +1,381 @@
const { Lesson, Chapter, Subject, Game } = require('../models');
const { Op } = require('sequelize');
/**
* Lesson Controller
* Quản lý các bài học trong chương
*/
class LessonController {
/**
* Lấy danh sách tất cả bài học
*/
async getAllLessons(req, res, next) {
try {
const {
page = 1,
limit = 20,
is_published,
is_free,
lesson_type,
search
} = req.query;
const offset = (page - 1) * limit;
const where = {};
// Filters
if (is_published !== undefined) {
where.is_published = is_published === 'true';
}
if (is_free !== undefined) {
where.is_free = is_free === 'true';
}
if (lesson_type) {
where.lesson_type = lesson_type;
}
if (search) {
where.lesson_title = { [Op.like]: `%${search}%` };
}
const { rows: lessons, count } = await Lesson.findAndCountAll({
where,
include: [
{
model: Chapter,
as: 'chapter',
attributes: ['id', 'chapter_title', 'chapter_number'],
include: [
{
model: Subject,
as: 'subject',
attributes: ['id', 'subject_name', 'subject_code']
}
]
}
],
order: [['display_order', 'ASC'], ['lesson_number', 'ASC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
data: {
lessons,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
total_pages: Math.ceil(count / limit)
}
}
});
} catch (error) {
next(error);
}
}
/**
* Lấy chi tiết bài học
*/
async getLessonById(req, res, next) {
try {
const { id } = req.params;
const lesson = await Lesson.findByPk(id, {
include: [
{
model: Chapter,
as: 'chapter',
include: [
{
model: Subject,
as: 'subject'
}
]
}
]
});
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy bài học'
});
}
// Nếu là composite lesson, lấy thông tin games cho từng component
if (lesson.lesson_type === 'json_content' &&
lesson.content_json?.type === 'composite_lesson') {
const components = lesson.content_json.components || [];
// Tìm tất cả game_type trong components
const gameTypes = components
.filter(c => c.type === 'game' && c.config?.game_type)
.map(c => c.config.game_type);
if (gameTypes.length > 0) {
const games = await Game.findAll({
where: {
type: { [Op.in]: gameTypes },
is_active: true
},
attributes: ['id', 'title', 'type', 'url', 'thumbnail', 'config']
});
// Gắn game info vào lesson
lesson.dataValues.available_games = games;
}
}
res.json({
success: true,
data: lesson
});
} catch (error) {
next(error);
}
}
/**
* Lấy danh sách bài học của một chương
*/
async getLessonsByChapter(req, res, next) {
try {
const { chapter_id } = req.params;
const { include_unpublished = false } = req.query;
const where = { chapter_id };
if (!include_unpublished || include_unpublished === 'false') {
where.is_published = true;
}
const lessons = await Lesson.findAll({
where,
order: [['display_order', 'ASC'], ['lesson_number', 'ASC']],
attributes: {
exclude: ['content_json', 'content_url'] // Không trả full content trong list
}
});
res.json({
success: true,
data: lessons
});
} catch (error) {
next(error);
}
}
/**
* Tạo bài học mới
*/
async createLesson(req, res, next) {
try {
const lessonData = req.body;
// Validate chapter exists
const chapter = await Chapter.findByPk(lessonData.chapter_id);
if (!chapter) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy chương học'
});
}
// Auto increment lesson_number if not provided
if (!lessonData.lesson_number) {
const maxLesson = await Lesson.findOne({
where: { chapter_id: lessonData.chapter_id },
order: [['lesson_number', 'DESC']]
});
lessonData.lesson_number = maxLesson ? maxLesson.lesson_number + 1 : 1;
}
// Auto increment display_order if not provided
if (!lessonData.display_order) {
const maxOrder = await Lesson.findOne({
where: { chapter_id: lessonData.chapter_id },
order: [['display_order', 'DESC']]
});
lessonData.display_order = maxOrder ? maxOrder.display_order + 1 : 1;
}
const lesson = await Lesson.create(lessonData);
res.status(201).json({
success: true,
message: 'Tạo bài học thành công',
data: lesson
});
} catch (error) {
next(error);
}
}
/**
* Cập nhật bài học
*/
async updateLesson(req, res, next) {
try {
const { id } = req.params;
const updateData = req.body;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy bài học'
});
}
// Validate chapter if changing
if (updateData.chapter_id && updateData.chapter_id !== lesson.chapter_id) {
const chapter = await Chapter.findByPk(updateData.chapter_id);
if (!chapter) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy chương học'
});
}
}
await lesson.update(updateData);
res.json({
success: true,
message: 'Cập nhật bài học thành công',
data: lesson
});
} catch (error) {
next(error);
}
}
/**
* Xóa bài học
*/
async deleteLesson(req, res, next) {
try {
const { id } = req.params;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy bài học'
});
}
await lesson.destroy();
res.json({
success: true,
message: 'Xóa bài học thành công'
});
} catch (error) {
next(error);
}
}
/**
* Đánh dấu hoàn thành bài học
*/
async completeLesson(req, res, next) {
try {
const { id } = req.params;
const userId = req.user.id; // From auth middleware
const { score, time_spent, results_data } = req.body;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy bài học'
});
}
// TODO: Save to UserLessonProgress table
// This would track completion, score, etc.
res.json({
success: true,
message: 'Đã hoàn thành bài học',
data: {
lesson_id: id,
user_id: userId,
score,
time_spent,
completed_at: new Date()
}
});
} catch (error) {
next(error);
}
}
/**
* Lấy game phù hợp cho lesson
*/
async getMatchingGames(req, res, next) {
try {
const { id } = req.params;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Không tìm thấy bài học'
});
}
if (lesson.lesson_type !== 'json_content' || !lesson.content_json?.type) {
return res.json({
success: true,
data: [],
message: 'Bài học này không sử dụng game engine'
});
}
const lessonType = lesson.content_json.type;
// Handle composite lesson
if (lessonType === 'composite_lesson') {
const components = lesson.content_json.components || [];
const gameTypes = components
.filter(c => c.type === 'game' && c.config?.game_type)
.map(c => c.config.game_type);
const games = await Game.findAll({
where: {
type: { [Op.in]: gameTypes },
is_active: true
}
});
return res.json({
success: true,
data: games
});
}
// Handle single component lesson
const games = await Game.findAll({
where: {
type: lessonType,
is_active: true
}
});
res.json({
success: true,
data: games
});
} catch (error) {
next(error);
}
}
}
module.exports = new LessonController();

210
middleware/auth.js Normal file
View File

@@ -0,0 +1,210 @@
const jwt = require('jsonwebtoken');
const { UsersAuth, UserProfile } = require('../models');
// JWT Secret - nên lưu trong environment variable
const JWT_SECRET = process.env.JWT_SECRET || 'sena-secret-key-2026';
/**
* Middleware xác thực token
*/
const authenticateToken = async (req, res, next) => {
try {
// Lấy token từ header
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Token không được cung cấp',
});
}
// Verify token
const decoded = jwt.verify(token, JWT_SECRET);
// Kiểm tra user còn tồn tại và session hợp lệ
const user = await UsersAuth.findByPk(decoded.userId, {
include: [{
model: UserProfile,
as: 'profile',
}],
});
if (!user || !user.is_active) {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ hoặc tài khoản đã bị vô hiệu hóa',
});
}
// Kiểm tra session
if (user.current_session_id !== decoded.sessionId) {
return res.status(401).json({
success: false,
message: 'Phiên đăng nhập đã hết hạn',
});
}
// Gắn thông tin user vào request
req.user = {
userId: user.id,
username: user.username,
email: user.email,
role: decoded.role,
sessionId: decoded.sessionId,
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token đã hết hạn',
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ',
});
}
console.error('Auth middleware error:', error);
return res.status(500).json({
success: false,
message: 'Lỗi xác thực',
error: error.message,
});
}
};
/**
* Middleware kiểm tra role
*/
const requireRole = (allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Chưa xác thực',
});
}
const userRole = req.user.role;
if (!allowedRoles.includes(userRole)) {
return res.status(403).json({
success: false,
message: 'Không có quyền truy cập',
required_role: allowedRoles,
your_role: userRole,
});
}
next();
};
};
/**
* Middleware kiểm tra permission cụ thể
*/
const requirePermission = (permission) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Chưa xác thực',
});
}
try {
const user = await UsersAuth.findByPk(req.user.userId, {
include: [{
model: Role,
as: 'role',
include: [{
model: Permission,
as: 'permissions',
}],
}],
});
if (!user) {
return res.status(401).json({
success: false,
message: 'User không tồn tại',
});
}
// Kiểm tra user có permission này không
const hasPermission = user.role?.permissions?.some(
p => p.permission_code === permission
);
if (!hasPermission) {
return res.status(403).json({
success: false,
message: 'Không có quyền thực hiện hành động này',
required_permission: permission,
});
}
next();
} catch (error) {
console.error('Permission check error:', error);
return res.status(500).json({
success: false,
message: 'Lỗi kiểm tra quyền',
error: error.message,
});
}
};
};
/**
* Middleware xác thực optional - không bắt buộc phải có token
*/
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.replace('Bearer ', '');
if (!token) {
// Không có token, tiếp tục nhưng không có req.user
req.user = null;
return next();
}
// Có token, thử verify
const decoded = jwt.verify(token, JWT_SECRET);
const user = await UsersAuth.findByPk(decoded.userId);
if (user && user.is_active && user.current_session_id === decoded.sessionId) {
req.user = {
userId: user.id,
username: user.username,
email: user.email,
role: decoded.role,
sessionId: decoded.sessionId,
};
} else {
req.user = null;
}
next();
} catch (error) {
// Token không hợp lệ, tiếp tục nhưng không có req.user
req.user = null;
next();
}
};
module.exports = {
authenticateToken,
requireRole,
requirePermission,
optionalAuth,
};

View File

@@ -0,0 +1,100 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* LessonComponentProgress Model
* Theo dõi tiến độ học viên cho từng component trong composite lesson
*/
const LessonComponentProgress = sequelize.define('lesson_component_progress', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
user_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'ID học viên'
},
lesson_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'ID bài học'
},
component_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: 'ID component từ content_json.components[].id'
},
component_type: {
type: DataTypes.STRING(50),
comment: 'Loại component: story_game, game, results_board, leaderboard'
},
// Progress tracking
status: {
type: DataTypes.ENUM('not_started', 'in_progress', 'completed'),
defaultValue: 'not_started',
comment: 'Trạng thái tiến độ'
},
started_at: {
type: DataTypes.DATE,
comment: 'Thời gian bắt đầu'
},
completed_at: {
type: DataTypes.DATE,
comment: 'Thời gian hoàn thành'
},
// Results (cho game/quiz components)
score: {
type: DataTypes.INTEGER,
comment: 'Điểm số đạt được'
},
max_score: {
type: DataTypes.INTEGER,
comment: 'Điểm tối đa'
},
accuracy: {
type: DataTypes.DECIMAL(5, 2),
comment: 'Độ chính xác (%)'
},
time_spent: {
type: DataTypes.INTEGER,
comment: 'Thời gian làm bài (giây)'
},
attempts: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Số lần thử'
},
// Detailed results
results_data: {
type: DataTypes.JSON,
comment: 'Chi tiết kết quả: câu trả lời, achievements, etc.'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'lesson_component_progress',
timestamps: true,
underscored: true,
indexes: [
{ fields: ['user_id'] },
{ fields: ['lesson_id'] },
{ fields: ['status'] },
{ fields: ['user_id', 'lesson_id', 'component_id'], unique: true }
]
});
module.exports = LessonComponentProgress;

View File

@@ -0,0 +1,88 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* LessonLeaderboard Model
* Cache bảng xếp hạng cho lessons
*/
const LessonLeaderboard = sequelize.define('lesson_leaderboard', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
lesson_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'ID bài học'
},
user_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'ID học viên'
},
// Ranking data
total_score: {
type: DataTypes.INTEGER,
comment: 'Tổng điểm'
},
accuracy: {
type: DataTypes.DECIMAL(5, 2),
comment: 'Độ chính xác (%)'
},
completion_time: {
type: DataTypes.INTEGER,
comment: 'Thời gian hoàn thành (giây)'
},
rank: {
type: DataTypes.INTEGER,
comment: 'Thứ hạng'
},
// Scope filters
scope: {
type: DataTypes.ENUM('class', 'school', 'global'),
allowNull: false,
comment: 'Phạm vi xếp hạng'
},
scope_id: {
type: DataTypes.UUID,
comment: 'ID của class hoặc school (tùy scope)'
},
time_range: {
type: DataTypes.ENUM('day', 'week', 'month', 'all_time'),
allowNull: false,
comment: 'Khoảng thời gian'
},
// Metadata
computed_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'Thời gian tính toán'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'lesson_leaderboard',
timestamps: true,
underscored: true,
indexes: [
{ fields: ['lesson_id', 'scope', 'time_range'] },
{ fields: ['rank'] },
{ fields: ['user_id'] },
{ fields: ['lesson_id', 'user_id', 'scope', 'time_range'], unique: true }
]
});
module.exports = LessonLeaderboard;

View File

@@ -29,6 +29,8 @@ const Room = require('./Room');
const Chapter = require('./Chapter'); const Chapter = require('./Chapter');
const Lesson = require('./Lesson'); const Lesson = require('./Lesson');
const Game = require('./Game'); const Game = require('./Game');
const LessonComponentProgress = require('./LessonComponentProgress');
const LessonLeaderboard = require('./LessonLeaderboard');
// Group 4: Attendance // Group 4: Attendance
const AttendanceLog = require('./AttendanceLog'); const AttendanceLog = require('./AttendanceLog');
@@ -136,6 +138,16 @@ const setupRelationships = () => {
Chapter.hasMany(Lesson, { foreignKey: 'chapter_id', as: 'lessons' }); Chapter.hasMany(Lesson, { foreignKey: 'chapter_id', as: 'lessons' });
Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id', as: 'chapter' }); Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id', as: 'chapter' });
// Lesson Progress relationships
LessonComponentProgress.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
LessonComponentProgress.belongsTo(Lesson, { foreignKey: 'lesson_id', as: 'lesson' });
Lesson.hasMany(LessonComponentProgress, { foreignKey: 'lesson_id', as: 'progress' });
// Leaderboard relationships
LessonLeaderboard.belongsTo(Lesson, { foreignKey: 'lesson_id', as: 'lesson' });
LessonLeaderboard.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
Lesson.hasMany(LessonLeaderboard, { foreignKey: 'lesson_id', as: 'leaderboard' });
// Attendance relationships // Attendance relationships
AttendanceLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' }); AttendanceLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
AttendanceLog.belongsTo(School, { foreignKey: 'school_id', as: 'school' }); AttendanceLog.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
@@ -243,6 +255,8 @@ module.exports = {
Chapter, Chapter,
Lesson, Lesson,
Game, Game,
LessonComponentProgress,
LessonLeaderboard,
// Group 4: Attendance // Group 4: Attendance
AttendanceLog, AttendanceLog,

View File

@@ -0,0 +1,36 @@
const express = require('express');
const router = express.Router();
const lessonController = require('../controllers/lessonController');
/**
* @swagger
* tags:
* name: Chapter Lessons
* description: Quản lý bài học theo chương
*/
/**
* @swagger
* /api/chapters/{chapter_id}/lessons:
* get:
* tags: [Chapter Lessons]
* summary: Lấy danh sách bài học của chương
* parameters:
* - in: path
* name: chapter_id
* required: true
* schema:
* type: string
* format: uuid
* - in: query
* name: include_unpublished
* schema:
* type: boolean
* default: false
* responses:
* 200:
* description: Danh sách bài học
*/
router.get('/:chapter_id/lessons', lessonController.getLessonsByChapter);
module.exports = router;

224
routes/lessonRoutes.js Normal file
View File

@@ -0,0 +1,224 @@
const express = require('express');
const router = express.Router();
const lessonController = require('../controllers/lessonController');
const { authenticateToken } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Lessons
* description: Quản lý bài học
*/
/**
* @swagger
* /api/lessons:
* get:
* tags: [Lessons]
* summary: Lấy danh sách bài học
* description: Lấy tất cả bài học với phân trang và filter
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* - in: query
* name: is_published
* schema:
* type: boolean
* - in: query
* name: is_free
* schema:
* type: boolean
* - in: query
* name: lesson_type
* schema:
* type: string
* enum: [json_content, url_content]
* - in: query
* name: search
* schema:
* type: string
* responses:
* 200:
* description: Danh sách bài học
*/
router.get('/', lessonController.getAllLessons);
/**
* @swagger
* /api/lessons/{id}:
* get:
* tags: [Lessons]
* summary: Lấy chi tiết bài học
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Chi tiết bài học
* 404:
* description: Không tìm thấy bài học
*/
router.get('/:id', lessonController.getLessonById);
/**
* @swagger
* /api/lessons/{id}/games:
* get:
* tags: [Lessons]
* summary: Lấy danh sách game phù hợp với bài học
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Danh sách game engines
*/
router.get('/:id/games', lessonController.getMatchingGames);
/**
* @swagger
* /api/lessons:
* post:
* tags: [Lessons]
* summary: Tạo bài học mới
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - chapter_id
* - lesson_title
* - lesson_type
* properties:
* chapter_id:
* type: string
* format: uuid
* lesson_number:
* type: integer
* lesson_title:
* type: string
* lesson_type:
* type: string
* enum: [json_content, url_content]
* lesson_description:
* type: string
* content_json:
* type: object
* content_url:
* type: string
* content_type:
* type: string
* duration_minutes:
* type: integer
* is_published:
* type: boolean
* is_free:
* type: boolean
* responses:
* 201:
* description: Tạo bài học thành công
*/
router.post('/', authenticateToken, lessonController.createLesson);
/**
* @swagger
* /api/lessons/{id}:
* put:
* tags: [Lessons]
* summary: Cập nhật bài học
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: Cập nhật thành công
*/
router.put('/:id', authenticateToken, lessonController.updateLesson);
/**
* @swagger
* /api/lessons/{id}:
* delete:
* tags: [Lessons]
* summary: Xóa bài học
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Xóa thành công
*/
router.delete('/:id', authenticateToken, lessonController.deleteLesson);
/**
* @swagger
* /api/lessons/{id}/complete:
* post:
* tags: [Lessons]
* summary: Đánh dấu hoàn thành bài học
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* score:
* type: integer
* time_spent:
* type: integer
* results_data:
* type: object
* responses:
* 200:
* description: Hoàn thành bài học
*/
router.post('/:id/complete', authenticateToken, lessonController.completeLesson);
module.exports = router;

View File

@@ -0,0 +1,390 @@
const { sequelize } = require('../config/database');
const { Subject, Chapter, Lesson, Game } = require('../models');
/**
* Script để seed sample data cho Content Management System
* Tạo môn học, chương, bài học mẫu và game engines
*/
async function seedSampleContent() {
try {
console.log('🌱 Starting to seed sample content...\n');
// =====================================
// 1. TẠO MÔN HỌC (SUBJECTS)
// =====================================
console.log('📚 Creating subjects...');
const mathSubject = await Subject.create({
subject_code: 'MATH_G1',
subject_name: 'Toán lớp 1',
subject_name_en: 'Math Grade 1',
description: 'Chương trình Toán học lớp 1 theo SGK mới',
is_active: true,
is_premium: false,
is_public: true,
min_subscription_tier: 'basic'
});
console.log('✅ Created:', mathSubject.subject_name);
const vietnameseSubject = await Subject.create({
subject_code: 'VIET_G1',
subject_name: 'Tiếng Việt lớp 1',
subject_name_en: 'Vietnamese Grade 1',
description: 'Chương trình Tiếng Việt lớp 1',
is_active: true,
is_premium: false,
is_public: true
});
console.log('✅ Created:', vietnameseSubject.subject_name);
const englishSubject = await Subject.create({
subject_code: 'ENG_G1',
subject_name: 'Tiếng Anh lớp 1',
subject_name_en: 'English Grade 1',
description: 'Chương trình Tiếng Anh cơ bản',
is_active: true,
is_premium: true,
is_public: false,
min_subscription_tier: 'premium'
});
console.log('✅ Created:', englishSubject.subject_name);
// =====================================
// 2. TẠO CHƯƠNG HỌC (CHAPTERS)
// =====================================
console.log('\n📖 Creating chapters...');
// Math Chapters
const mathChapter1 = await Chapter.create({
subject_id: mathSubject.id,
chapter_number: 1,
chapter_title: 'Số và chữ số',
chapter_description: 'Làm quen với các số từ 1 đến 10',
duration_minutes: 180,
is_published: true,
display_order: 1
});
console.log('✅ Created:', mathChapter1.chapter_title);
const mathChapter2 = await Chapter.create({
subject_id: mathSubject.id,
chapter_number: 2,
chapter_title: 'Phép cộng trong phạm vi 10',
chapter_description: 'Học cộng các số từ 1 đến 10',
duration_minutes: 240,
is_published: true,
display_order: 2
});
console.log('✅ Created:', mathChapter2.chapter_title);
// Vietnamese Chapter
const vietChapter1 = await Chapter.create({
subject_id: vietnameseSubject.id,
chapter_number: 1,
chapter_title: 'Bảng chữ cái',
chapter_description: 'Học bảng chữ cái tiếng Việt',
duration_minutes: 300,
is_published: true,
display_order: 1
});
console.log('✅ Created:', vietChapter1.chapter_title);
// =====================================
// 3. TẠO GAME ENGINES
// =====================================
console.log('\n🎮 Creating game engines...');
const countingGame = await Game.create({
title: 'Trò chơi đếm số',
description: 'Game tương tác giúp trẻ học đếm các vật thể',
url: 'https://games.senaai.tech/counting-game/index.html',
thumbnail: 'https://cdn.senaai.tech/game-thumbs/counting.jpg',
type: 'counting_quiz',
config: {
engine: 'phaser3',
version: '1.0.0',
features: ['sound', 'animation', 'reward_stars'],
controls: ['touch', 'mouse', 'keyboard']
},
is_active: true,
is_premium: false,
min_grade: 1,
max_grade: 2,
difficulty_level: 'easy',
display_order: 1
});
console.log('✅ Created:', countingGame.title);
const quizGame = await Game.create({
title: 'Trả lời nhanh',
description: 'Trò chơi trắc nghiệm với giới hạn thời gian',
url: 'https://games.senaai.tech/quiz-game/index.html',
thumbnail: 'https://cdn.senaai.tech/game-thumbs/quiz.jpg',
type: 'multiple_choice_quiz',
config: {
engine: 'react',
features: ['timer', 'leaderboard', 'achievements']
},
is_active: true,
is_premium: false,
min_grade: 1,
max_grade: 12,
difficulty_level: 'medium',
display_order: 2
});
console.log('✅ Created:', quizGame.title);
const mathPracticeGame = await Game.create({
title: 'Luyện tập Toán',
description: 'Game luyện toán với nhiều cấp độ khó',
url: 'https://games.senaai.tech/math-practice/index.html',
thumbnail: 'https://cdn.senaai.tech/game-thumbs/math.jpg',
type: 'math_practice',
config: {
engine: 'unity_webgl',
features: ['adaptive_difficulty', 'step_by_step_solution']
},
is_active: true,
is_premium: true,
min_grade: 1,
max_grade: 6,
difficulty_level: 'medium',
display_order: 3
});
console.log('✅ Created:', mathPracticeGame.title);
// =====================================
// 4. TẠO BÀI HỌC (LESSONS)
// =====================================
console.log('\n📝 Creating lessons...');
// Lesson 1: Simple counting quiz
const lesson1 = await Lesson.create({
chapter_id: mathChapter1.id,
lesson_number: 1,
lesson_title: 'Đếm từ 1 đến 5',
lesson_type: 'json_content',
lesson_description: 'Học đếm các số từ 1 đến 5 qua trò chơi',
content_json: {
type: 'counting_quiz',
theme: 'fruits',
questions: [
{
id: 1,
question: 'Có bao nhiêu quả táo?',
image: 'https://cdn.senaai.tech/images/apples-3.png',
correct_answer: 3,
options: [2, 3, 4, 5]
},
{
id: 2,
question: 'Có bao nhiêu quả cam?',
image: 'https://cdn.senaai.tech/images/oranges-5.png',
correct_answer: 5,
options: [3, 4, 5, 6]
}
],
instructions: 'Nhìn vào hình và đếm số lượng vật',
pass_score: 70
},
duration_minutes: 15,
is_published: true,
is_free: true,
display_order: 1
});
console.log('✅ Created:', lesson1.lesson_title);
// Lesson 2: Video lesson
const lesson2 = await Lesson.create({
chapter_id: mathChapter1.id,
lesson_number: 2,
lesson_title: 'Video: Hướng dẫn đếm số',
lesson_type: 'url_content',
lesson_description: 'Video hướng dẫn cách đếm số từ 1 đến 10',
content_url: 'https://www.youtube.com/watch?v=demo123',
content_type: 'youtube',
duration_minutes: 10,
is_published: true,
is_free: false,
display_order: 2
});
console.log('✅ Created:', lesson2.lesson_title);
// Lesson 3: Composite lesson with multiple components
const lesson3 = await Lesson.create({
chapter_id: mathChapter1.id,
lesson_number: 3,
lesson_title: 'Bài học tổng hợp: Đếm và so sánh',
lesson_type: 'json_content',
lesson_description: 'Bài học tích hợp nhiều hoạt động',
content_json: {
type: 'composite_lesson',
version: '2.0',
layout: 'vertical',
components: [
{
id: 'story-1',
type: 'story_game',
order: 1,
title: 'Câu chuyện: Gấu đếm táo',
config: {
skippable: true,
auto_play: false
},
content: {
scenes: [
{
scene_id: 1,
background: 'https://cdn.senaai.tech/scenes/forest.jpg',
character: 'bear',
dialogue: 'Chào các em! Hôm nay chú gấu sẽ dạy các em đếm.',
duration: 5
}
]
}
},
{
id: 'game-1',
type: 'game',
order: 2,
title: 'Trò chơi đếm số',
required: true,
unlock_after: 'story-1',
config: {
game_type: 'counting_quiz',
difficulty: 'medium'
},
content: {
questions: [
{
id: 1,
question: 'Đếm số con vịt',
image: 'https://cdn.senaai.tech/images/ducks-7.png',
correct_answer: 7,
options: [5, 6, 7, 8]
}
]
}
},
{
id: 'results-1',
type: 'results_board',
order: 3,
title: 'Kết quả',
unlock_after: 'game-1',
content: {
display_fields: [
{ field: 'score', label: 'Điểm số', format: 'number' },
{ field: 'accuracy', label: 'Độ chính xác', format: 'percentage' }
]
}
},
{
id: 'leaderboard-1',
type: 'leaderboard',
order: 4,
title: 'Bảng xếp hạng',
config: {
scope: 'class',
time_range: 'week'
}
}
],
global_config: {
enable_navigation: true,
allow_skip: false
}
},
duration_minutes: 25,
is_published: true,
is_free: false,
display_order: 3
});
console.log('✅ Created:', lesson3.lesson_title);
// Lesson 4: Math practice
const lesson4 = await Lesson.create({
chapter_id: mathChapter2.id,
lesson_number: 1,
lesson_title: 'Luyện tập phép cộng',
lesson_type: 'json_content',
content_json: {
type: 'math_practice',
difficulty: 'easy',
operations: ['addition'],
range: { min: 1, max: 10 },
question_count: 20,
show_solution_steps: true
},
duration_minutes: 20,
is_published: true,
display_order: 1
});
console.log('✅ Created:', lesson4.lesson_title);
// Lesson 5: Vietnamese alphabet
const lesson5 = await Lesson.create({
chapter_id: vietChapter1.id,
lesson_number: 1,
lesson_title: 'Học bảng chữ cái A-Z',
lesson_type: 'json_content',
content_json: {
type: 'word_puzzle',
letters: ['A', 'B', 'C', 'D', 'E'],
exercises: [
{
letter: 'A',
pronunciation: 'a',
examples: ['Áo', 'Ăn', 'An']
}
]
},
duration_minutes: 30,
is_published: true,
is_free: true,
display_order: 1
});
console.log('✅ Created:', lesson5.lesson_title);
// =====================================
// SUMMARY
// =====================================
console.log('\n🎉 Sample content seeding completed!');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 Summary:');
console.log(` • Subjects: 3`);
console.log(` • Chapters: 3`);
console.log(` • Lessons: 5`);
console.log(` • Games: 3`);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
console.log('✅ You can now:');
console.log(' 1. GET /api/subjects - View all subjects');
console.log(' 2. GET /api/subjects/:id/chapters - View chapters');
console.log(' 3. GET /api/chapters/:id/lessons - View lessons');
console.log(' 4. GET /api/lessons/:id - View lesson detail');
console.log(' 5. GET /api/games?type=counting_quiz - Find matching games\n');
} catch (error) {
console.error('❌ Error seeding data:', error);
throw error;
} finally {
await sequelize.close();
}
}
// Run the seeder
if (require.main === module) {
seedSampleContent()
.then(() => {
console.log('✅ Script completed successfully');
process.exit(0);
})
.catch((error) => {
console.error('❌ Script failed:', error);
process.exit(1);
});
}
module.exports = seedSampleContent;