update
This commit is contained in:
@@ -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
|
||||
// Quiz trắc nghiệm
|
||||
@@ -455,26 +773,66 @@ GET /api/chapters/:chapter_id/lessons
|
||||
GET /api/lessons/:lesson_id
|
||||
→ Trả về chi tiết Lesson
|
||||
|
||||
// 5a. Nếu lesson_type = 'json_content'
|
||||
const lessonType = lesson.content_json.type; // vd: 'counting_quiz'
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tìm game engine phù hợp
|
||||
GET /api/games?type=counting_quiz
|
||||
→ Trả về Game có type khớp
|
||||
|
||||
// Load game và truyền content vào
|
||||
<iframe
|
||||
src={game.url}
|
||||
data-content={JSON.stringify(lesson.content_json)}
|
||||
/>
|
||||
|
||||
// 5b. Nếu 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} />
|
||||
// 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} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -594,6 +952,8 @@ Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id' });
|
||||
### 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
|
||||
@@ -603,8 +963,279 @@ Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id' });
|
||||
'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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user