1367 lines
38 KiB
Markdown
1367 lines
38 KiB
Markdown
# 📚 Hướng Dẫn Quản Lý Nội Dung Giảng Dạy và Game
|
|
|
|
## Tổng Quan Kiến Trúc
|
|
|
|
Hệ thống nội dung của bạn được xây dựng theo mô hình phân cấp:
|
|
|
|
```
|
|
Subject (Môn học/Giáo trình)
|
|
└─ Chapter (Chương)
|
|
└─ Lesson (Bài học)
|
|
├─ JSON Content → Game Engine render
|
|
└─ URL Content → Video/PDF player
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Cấu Trúc Models
|
|
|
|
### 1. Subject (Môn học)
|
|
**File**: `models/Subject.js`
|
|
**Table**: `subjects`
|
|
|
|
```javascript
|
|
{
|
|
id: UUID,
|
|
subject_code: 'MATH_G1', // Mã môn học (unique)
|
|
subject_name: 'Toán lớp 1', // Tên tiếng Việt
|
|
subject_name_en: 'Math Grade 1', // Tên tiếng Anh
|
|
description: TEXT, // Mô tả
|
|
is_active: true, // Đang hoạt động
|
|
is_premium: false, // Nội dung premium
|
|
is_training: false, // Đào tạo nhân sự
|
|
is_public: false, // Tự học công khai
|
|
required_role: 'student', // Role yêu cầu
|
|
min_subscription_tier: 'basic' // Gói tối thiểu
|
|
}
|
|
```
|
|
|
|
### 2. Chapter (Chương học)
|
|
**File**: `models/Chapter.js`
|
|
**Table**: `chapters`
|
|
|
|
```javascript
|
|
{
|
|
id: UUID,
|
|
subject_id: UUID, // FK → subjects
|
|
chapter_number: 1, // Số thứ tự chương
|
|
chapter_title: 'Số và chữ số', // Tiêu đề
|
|
chapter_description: TEXT, // Mô tả
|
|
duration_minutes: 180, // Thời lượng (phút)
|
|
is_published: true, // Đã xuất bản
|
|
display_order: 1 // Thứ tự hiển thị
|
|
}
|
|
```
|
|
|
|
### 3. Lesson (Bài học)
|
|
**File**: `models/Lesson.js`
|
|
**Table**: `lessons`
|
|
|
|
```javascript
|
|
{
|
|
id: UUID,
|
|
chapter_id: UUID, // FK → chapters
|
|
lesson_number: 1, // Số thứ tự bài học
|
|
lesson_title: 'Đếm từ 1 đến 5', // Tiêu đề
|
|
lesson_type: 'json_content', // hoặc 'url_content'
|
|
lesson_description: TEXT,
|
|
|
|
// Dạng 1: JSON Content (tương tác)
|
|
content_json: {
|
|
type: 'counting_quiz', // PHẢI khớp với Game.type
|
|
questions: [...],
|
|
instructions: '...',
|
|
pass_score: 70
|
|
},
|
|
|
|
// Dạng 2: URL Content (video/PDF/link)
|
|
content_url: 'https://youtube.com/...',
|
|
content_type: 'youtube', // video, audio, pdf, external_link
|
|
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true, // Học thử miễn phí
|
|
display_order: 1,
|
|
thumbnail_url: '...'
|
|
}
|
|
```
|
|
|
|
### 4. Game (Game Engine/Template)
|
|
**File**: `models/Game.js`
|
|
**Table**: `games`
|
|
|
|
```javascript
|
|
{
|
|
id: UUID,
|
|
title: 'Trò chơi đếm số',
|
|
description: TEXT,
|
|
url: 'https://games.senaai.tech/counting-game/', // HTML5/Unity WebGL
|
|
thumbnail: '...',
|
|
type: 'counting_quiz', // PHẢI khớp với Lesson.content_json.type
|
|
config: {
|
|
engine: 'phaser3',
|
|
features: ['sound', 'animation'],
|
|
controls: ['touch', 'mouse'],
|
|
max_time: 300
|
|
},
|
|
is_active: true,
|
|
is_premium: false,
|
|
min_grade: 1, // Cấp lớp tối thiểu
|
|
max_grade: 3, // Cấp lớp tối đa
|
|
difficulty_level: 'easy', // easy, medium, hard
|
|
play_count: 0,
|
|
rating: 4.5
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Cách 1: Thêm Nội Dung Giảng Dạy
|
|
|
|
### Bước 1: Tạo Subject (Môn học)
|
|
|
|
```javascript
|
|
const { Subject, Chapter, Lesson } = require('./models');
|
|
|
|
// Tạo môn Toán lớp 1
|
|
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'
|
|
});
|
|
```
|
|
|
|
### Bước 2: Tạo Chapter (Chương)
|
|
|
|
```javascript
|
|
const chapter1 = 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
|
|
});
|
|
|
|
const chapter2 = 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
|
|
});
|
|
```
|
|
|
|
### Bước 3a: Tạo Lesson với JSON Content (Tương tác)
|
|
|
|
```javascript
|
|
const lesson1 = await Lesson.create({
|
|
chapter_id: chapter1.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', // Khớp với Game có 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, sau đó chọn đáp án đúng',
|
|
pass_score: 70,
|
|
max_attempts: 3,
|
|
show_hints: true
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1,
|
|
thumbnail_url: 'https://cdn.senaai.tech/thumbnails/lesson-counting.jpg'
|
|
});
|
|
```
|
|
|
|
### Bước 3b: Tạo Lesson với URL Content (Video/PDF)
|
|
|
|
```javascript
|
|
// Video YouTube
|
|
const lesson2 = await Lesson.create({
|
|
chapter_id: chapter1.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=abc123xyz',
|
|
content_type: 'youtube',
|
|
duration_minutes: 10,
|
|
is_published: true,
|
|
is_free: false,
|
|
display_order: 2
|
|
});
|
|
|
|
// PDF Document
|
|
const lesson3 = await Lesson.create({
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 3,
|
|
lesson_title: 'Tài liệu: Bảng số từ 1-10',
|
|
lesson_type: 'url_content',
|
|
content_url: 'https://cdn.senaai.tech/docs/number-chart-1-10.pdf',
|
|
content_type: 'pdf',
|
|
duration_minutes: 5,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 3
|
|
});
|
|
|
|
// Audio
|
|
const lesson4 = await Lesson.create({
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 4,
|
|
lesson_title: 'Nghe: Phát âm các số',
|
|
lesson_type: 'url_content',
|
|
content_url: 'https://cdn.senaai.tech/audio/numbers-pronunciation.mp3',
|
|
content_type: 'audio',
|
|
duration_minutes: 8,
|
|
is_published: true,
|
|
display_order: 4
|
|
});
|
|
```
|
|
|
|
### 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
|
|
// Quiz trắc nghiệm
|
|
const quizLesson = await Lesson.create({
|
|
chapter_id: chapter2.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Bài kiểm tra: Phép cộng',
|
|
lesson_type: 'json_content',
|
|
content_json: {
|
|
type: 'multiple_choice_quiz',
|
|
time_limit: 600, // 10 phút
|
|
questions: [
|
|
{
|
|
id: 1,
|
|
question: '2 + 3 = ?',
|
|
options: ['4', '5', '6', '7'],
|
|
correct_answer: '5',
|
|
explanation: 'Hai cộng ba bằng năm'
|
|
},
|
|
{
|
|
id: 2,
|
|
question: '4 + 5 = ?',
|
|
options: ['7', '8', '9', '10'],
|
|
correct_answer: '9'
|
|
}
|
|
],
|
|
pass_score: 80
|
|
},
|
|
duration_minutes: 10,
|
|
is_published: true
|
|
});
|
|
|
|
// Bài tập tương tác
|
|
const interactiveLesson = await Lesson.create({
|
|
chapter_id: chapter2.id,
|
|
lesson_number: 2,
|
|
lesson_title: 'Thực hành: Giải toán 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,
|
|
allow_calculator: false
|
|
},
|
|
duration_minutes: 20,
|
|
is_published: true
|
|
});
|
|
|
|
// Assignment (Bài tập về nhà)
|
|
const assignmentLesson = await Lesson.create({
|
|
chapter_id: chapter2.id,
|
|
lesson_number: 3,
|
|
lesson_title: 'Bài tập về nhà: Tuần 1',
|
|
lesson_type: 'json_content',
|
|
content_json: {
|
|
type: 'assignment',
|
|
deadline_days: 7,
|
|
tasks: [
|
|
{
|
|
task_id: 1,
|
|
title: 'Làm bài tập SGK trang 15',
|
|
description: 'Hoàn thành các bài từ 1 đến 5',
|
|
points: 10
|
|
},
|
|
{
|
|
task_id: 2,
|
|
title: 'Vẽ 5 quả táo và đếm',
|
|
description: 'Vẽ hình và viết số',
|
|
points: 5,
|
|
requires_upload: true
|
|
}
|
|
],
|
|
total_points: 15,
|
|
submission_type: 'photo' // photo, pdf, text
|
|
},
|
|
duration_minutes: 30,
|
|
is_published: true
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🎮 Cách 2: Thêm Game Engine
|
|
|
|
### Tạo Game Template
|
|
|
|
```javascript
|
|
const { Game } = require('./models');
|
|
|
|
// Game 1: Đếm số
|
|
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', // Khớp với Lesson.content_json.type
|
|
config: {
|
|
engine: 'phaser3',
|
|
version: '1.0.0',
|
|
features: ['sound', 'animation', 'reward_stars'],
|
|
controls: ['touch', 'mouse', 'keyboard'],
|
|
responsive: true,
|
|
max_time_per_question: 60,
|
|
hints_enabled: true,
|
|
save_progress: true
|
|
},
|
|
is_active: true,
|
|
is_premium: false,
|
|
min_grade: 1,
|
|
max_grade: 2,
|
|
difficulty_level: 'easy',
|
|
play_count: 0,
|
|
rating: 0,
|
|
display_order: 1
|
|
});
|
|
|
|
// Game 2: Quiz trắc nghiệm
|
|
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'],
|
|
sound_effects: true,
|
|
show_correct_answer: true,
|
|
retry_allowed: true
|
|
},
|
|
is_active: true,
|
|
is_premium: false,
|
|
min_grade: 1,
|
|
max_grade: 12,
|
|
difficulty_level: 'medium',
|
|
display_order: 2
|
|
});
|
|
|
|
// Game 3: Thực hành Toán
|
|
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', 'scratch_pad'],
|
|
controls: ['touch', 'mouse'],
|
|
performance_tracking: true
|
|
},
|
|
is_active: true,
|
|
is_premium: true,
|
|
min_grade: 1,
|
|
max_grade: 6,
|
|
difficulty_level: 'medium',
|
|
display_order: 3
|
|
});
|
|
|
|
// Game 4: Word Puzzle
|
|
const wordPuzzleGame = await Game.create({
|
|
title: 'Ghép chữ',
|
|
description: 'Trò chơi ghép chữ và học từ vựng',
|
|
url: 'https://games.senaai.tech/word-puzzle/index.html',
|
|
thumbnail: 'https://cdn.senaai.tech/game-thumbs/word.jpg',
|
|
type: 'word_puzzle',
|
|
config: {
|
|
engine: 'phaser3',
|
|
features: ['drag_drop', 'text_to_speech', 'pronunciation'],
|
|
languages: ['vi', 'en'],
|
|
difficulty_levels: ['easy', 'medium', 'hard']
|
|
},
|
|
is_active: true,
|
|
is_premium: false,
|
|
min_grade: 1,
|
|
max_grade: 5,
|
|
difficulty_level: 'easy',
|
|
display_order: 4
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🔄 Luồng Hoạt Động
|
|
|
|
### Frontend Flow
|
|
|
|
```javascript
|
|
// 1. Học viên chọn môn học
|
|
GET /api/subjects/:subject_id
|
|
→ Trả về thông tin Subject
|
|
|
|
// 2. Xem danh sách chương
|
|
GET /api/subjects/:subject_id/chapters
|
|
→ Trả về danh sách Chapter
|
|
|
|
// 3. Xem danh sách bài học trong chương
|
|
GET /api/chapters/:chapter_id/lessons
|
|
→ Trả về danh sách Lesson
|
|
|
|
// 4. Học một bài cụ thể
|
|
GET /api/lessons/:lesson_id
|
|
→ Trả về chi tiết Lesson
|
|
|
|
// 5. Render lesson theo type
|
|
if (lesson.lesson_type === 'json_content') {
|
|
const lessonType = lesson.content_json.type;
|
|
|
|
// 5a. Single Component Lesson
|
|
if (lessonType !== 'composite_lesson') {
|
|
// Tìm game engine phù hợp
|
|
const game = await fetch(`/api/games?type=${lessonType}`);
|
|
|
|
// Render game với content
|
|
<GamePlayer
|
|
gameUrl={game.url}
|
|
content={lesson.content_json}
|
|
/>
|
|
}
|
|
|
|
// 5b. Composite Lesson (Multi Components)
|
|
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} />
|
|
} else if (lesson.content_type === 'pdf') {
|
|
<PDFViewer url={lesson.content_url} />
|
|
} else if (lesson.content_type === 'audio') {
|
|
<AudioPlayer url={lesson.content_url} />
|
|
}
|
|
}
|
|
```
|
|
|
|
### Game Engine Integration
|
|
|
|
```javascript
|
|
// Trong game engine (ví dụ: counting-game/index.html)
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data.type === 'LOAD_CONTENT') {
|
|
const content = event.data.content; // Lesson.content_json
|
|
|
|
// Render game với nội dung từ lesson
|
|
renderGame({
|
|
questions: content.questions,
|
|
theme: content.theme,
|
|
instructions: content.instructions,
|
|
passScore: content.pass_score
|
|
});
|
|
}
|
|
});
|
|
|
|
// Khi học viên hoàn thành
|
|
window.parent.postMessage({
|
|
type: 'GAME_COMPLETE',
|
|
score: 85,
|
|
time_spent: 300,
|
|
answers: [...]
|
|
}, '*');
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 API Endpoints Cần Thiết
|
|
|
|
### Subject Routes (Đã có: `routes/subjectRoutes.js`)
|
|
```
|
|
GET /api/subjects - Danh sách môn học
|
|
GET /api/subjects/:id - Chi tiết môn học
|
|
POST /api/subjects - Tạo môn học mới
|
|
PUT /api/subjects/:id - Cập nhật môn học
|
|
DELETE /api/subjects/:id - Xóa môn học
|
|
```
|
|
|
|
### Chapter Routes (Đã có: `routes/chapterRoutes.js`)
|
|
```
|
|
GET /api/chapters - Danh sách chương
|
|
GET /api/chapters/:id - Chi tiết chương
|
|
GET /api/subjects/:id/chapters - Chương của môn học
|
|
POST /api/chapters - Tạo chương mới
|
|
PUT /api/chapters/:id - Cập nhật chương
|
|
DELETE /api/chapters/:id - Xóa chương
|
|
```
|
|
|
|
### Lesson Routes (CẦN TẠO)
|
|
```
|
|
GET /api/lessons - Danh sách bài học
|
|
GET /api/lessons/:id - Chi tiết bài học
|
|
GET /api/chapters/:id/lessons - Bài học của chương
|
|
POST /api/lessons - Tạo bài học mới
|
|
PUT /api/lessons/:id - Cập nhật bài học
|
|
DELETE /api/lessons/:id - Xóa bài học
|
|
POST /api/lessons/:id/complete - Đánh dấu hoàn thành
|
|
```
|
|
|
|
### Game Routes (Đã có: `routes/gameRoutes.js`)
|
|
```
|
|
GET /api/games - Danh sách game
|
|
GET /api/games/:id - Chi tiết game
|
|
GET /api/games?type=xxx - Lọc theo type
|
|
POST /api/games - Tạo game mới
|
|
PUT /api/games/:id - Cập nhật game
|
|
DELETE /api/games/:id - Xóa game
|
|
POST /api/games/:id/play - Tăng play_count
|
|
POST /api/games/:id/rate - Đánh giá game
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Checklist Triển Khai
|
|
|
|
### Bước 1: Tạo Lesson Routes & Controller
|
|
- [ ] Tạo `routes/lessonRoutes.js`
|
|
- [ ] Tạo `controllers/lessonController.js`
|
|
- [ ] Đăng ký route trong `app.js`
|
|
|
|
### Bước 2: Tạo Sample Data
|
|
- [ ] Tạo script `scripts/seed-sample-content.js`
|
|
- [ ] Tạo Subject mẫu (Toán, Tiếng Việt, Tiếng Anh)
|
|
- [ ] Tạo Chapter mẫu (3-5 chương mỗi môn)
|
|
- [ ] Tạo Lesson mẫu (5-10 bài mỗi chương)
|
|
- [ ] Tạo Game template mẫu (3-5 game)
|
|
|
|
### Bước 3: Update Relations
|
|
```javascript
|
|
// Trong models/index.js
|
|
Subject.hasMany(Chapter, { foreignKey: 'subject_id' });
|
|
Chapter.belongsTo(Subject, { foreignKey: 'subject_id' });
|
|
|
|
Chapter.hasMany(Lesson, { foreignKey: 'chapter_id' });
|
|
Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id' });
|
|
```
|
|
|
|
### Bước 4: Thêm Swagger Documentation
|
|
- [ ] Thêm Swagger cho Lesson routes
|
|
- [ ] Cập nhật schemas trong `config/swagger.js`
|
|
|
|
### Bước 5: Test
|
|
- [ ] Test tạo Subject → Chapter → Lesson
|
|
- [ ] Test query lessons theo chapter
|
|
- [ ] Test match Game với Lesson.content_json.type
|
|
- [ ] Test các loại content khác nhau
|
|
|
|
---
|
|
|
|
## 💡 Best Practices
|
|
|
|
### 1. Lesson Type Naming Convention
|
|
```javascript
|
|
// Đặt tên theo format: <category>_<action>
|
|
|
|
// Single Component Lessons
|
|
'counting_quiz' // Đếm số - Quiz
|
|
'multiple_choice_quiz' // Trắc nghiệm
|
|
'math_practice' // Luyện toán
|
|
'word_puzzle' // Ghép chữ
|
|
'reading_comprehension' // Đọc hiểu
|
|
'listening_exercise' // Nghe
|
|
'speaking_practice' // Luyện nói
|
|
'writing_assignment' // Bài tập viết
|
|
'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
|
|
```javascript
|
|
{
|
|
type: 'xxx', // 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.)
|
|
}
|
|
```
|
|
|
|
### 3. Game Config Structure
|
|
```javascript
|
|
{
|
|
engine: 'phaser3', // Game engine sử dụng
|
|
version: '1.0.0',
|
|
features: [], // Các tính năng: sound, animation, etc.
|
|
controls: [], // touch, mouse, keyboard
|
|
responsive: true,
|
|
settings: {
|
|
max_time: 300,
|
|
hints_allowed: true,
|
|
retry_count: 3
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Error Handling
|
|
```javascript
|
|
// Khi student học lesson
|
|
if (lesson.lesson_type === 'json_content') {
|
|
const lessonType = lesson.content_json.type;
|
|
const game = await Game.findOne({
|
|
where: {
|
|
type: lessonType,
|
|
is_active: true
|
|
}
|
|
});
|
|
|
|
if (!game) {
|
|
// Fallback: Hiển thị content JSON dạng text/list
|
|
console.warn(`No game found for type: ${lessonType}`);
|
|
return renderStaticContent(lesson.content_json);
|
|
}
|
|
|
|
return renderGameWithContent(game, lesson.content_json);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start Script
|
|
|
|
Chạy script này để tạo sample data:
|
|
|
|
```javascript
|
|
// scripts/seed-sample-content.js
|
|
const { Subject, Chapter, Lesson, Game } = require('../models');
|
|
|
|
async function seedSampleContent() {
|
|
// 1. Tạo Subject
|
|
const math = await Subject.create({
|
|
subject_code: 'MATH_G1',
|
|
subject_name: 'Toán lớp 1',
|
|
is_active: true,
|
|
is_public: true
|
|
});
|
|
|
|
// 2. Tạo Chapter
|
|
const chapter = await Chapter.create({
|
|
subject_id: math.id,
|
|
chapter_number: 1,
|
|
chapter_title: 'Số và chữ số',
|
|
is_published: true
|
|
});
|
|
|
|
// 3. Tạo Lesson
|
|
const lesson = await Lesson.create({
|
|
chapter_id: chapter.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Đếm từ 1 đến 5',
|
|
lesson_type: 'json_content',
|
|
content_json: {
|
|
type: 'counting_quiz',
|
|
questions: [
|
|
{
|
|
question: 'Có bao nhiêu quả táo?',
|
|
image: '/images/apples-3.png',
|
|
answer: 3,
|
|
options: [2, 3, 4, 5]
|
|
}
|
|
]
|
|
},
|
|
is_published: true,
|
|
is_free: true
|
|
});
|
|
|
|
// 4. Tạo Game
|
|
const game = await Game.create({
|
|
title: 'Trò chơi đếm số',
|
|
url: 'https://games.senaai.tech/counting/',
|
|
type: 'counting_quiz',
|
|
is_active: true
|
|
});
|
|
|
|
console.log('✅ Sample content created successfully!');
|
|
}
|
|
|
|
seedSampleContent();
|
|
```
|
|
|
|
Chạy: `node scripts/seed-sample-content.js`
|
|
|
|
---
|
|
|
|
## 📞 Support
|
|
|
|
Nếu cần hỗ trợ thêm:
|
|
1. Tạo Lesson Routes & Controller
|
|
2. Tạo script seed data mẫu
|
|
3. Cập nhật Swagger documentation
|
|
4. Tạo frontend component để render lesson
|
|
|
|
Hãy cho tôi biết bạn muốn tôi implement phần nào! 🚀
|