# 📚 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 } // 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 ; case 'game': // Tìm game engine theo comp.config.game_type const game = await fetch(`/api/games?type=${comp.config.game_type}`); return ; case 'results_board': return ; case 'leaderboard': return ; default: return null; } }); } } // 5c. URL Content else if (lesson.lesson_type === 'url_content') { if (lesson.content_type === 'youtube') { } else if (lesson.content_type === 'pdf') { } else if (lesson.content_type === 'audio') { } } ``` ### 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: _ // 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 (
{/* Navigation tabs */}
{components.map((comp, index) => ( ))}
{/* Current component */}
{renderComponent( components[currentComponent], handleComponentComplete )}
); }; function renderComponent(component, onComplete) { switch(component.type) { case 'story_game': return ( onComplete(component.id)} /> ); case 'game': return ( { // Save results to API saveGameResults(component.id, results); onComplete(component.id); }} /> ); case 'results_board': return ( ); case 'leaderboard': return ( ); default: return
Unknown component type: {component.type}
; } } 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! 🚀