const { Lesson, Chapter, Subject, Game } = require('../models'); const { Op } = require('sequelize'); const { Story, LessonStory } = require('../models'); const { cacheUtils } = require('../config/redis'); /** * 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, lesson_content_type, chapter_id, search } = req.query; const offset = (page - 1) * limit; const where = {}; // Filters if (chapter_id) { where.chapter_id = chapter_id; } 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 (lesson_content_type) { where.lesson_content_type = lesson_content_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); } } /** * Lấy danh sách stories trong một lesson */ async getStoriesByLesson(req, res, next) { try { const { lessonId } = req.params; const { page = 1, limit = 20 } = req.query; const offset = (page - 1) * limit; const cacheKey = `lesson:${lessonId}:stories:${page}:${limit}`; const cached = await cacheUtils.get(cacheKey); if (cached) { return res.json({ success: true, data: cached, cached: true, }); } // Check if lesson exists const lesson = await Lesson.findByPk(lessonId); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học', }); } // Get stories with pivot data const { count, rows } = await Story.findAndCountAll({ include: [ { model: Lesson, as: 'lessons', where: { id: lessonId }, through: { attributes: ['display_order', 'is_required'], }, attributes: [], }, ], limit: parseInt(limit), offset: parseInt(offset), order: [[{ model: Lesson, as: 'lessons' }, LessonStory, 'display_order', 'ASC']], }); const result = { lesson: { id: lesson.id, lesson_title: lesson.lesson_title, lesson_number: lesson.lesson_number, }, stories: rows.map(story => ({ ...story.toJSON(), display_order: story.lessons[0]?.LessonStory?.display_order, is_required: story.lessons[0]?.LessonStory?.is_required, })), pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit), }, }; await cacheUtils.set(cacheKey, result, 1800); res.json({ success: true, data: result, cached: false, }); } catch (error) { next(error); } } /** * Thêm story vào lesson */ async addStoryToLesson(req, res, next) { try { const { lessonId } = req.params; const { story_id, display_order = 0, is_required = true } = req.body; // Validate required fields if (!story_id) { return res.status(400).json({ success: false, message: 'story_id is required', }); } // Check if lesson exists const lesson = await Lesson.findByPk(lessonId); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học', }); } // Check if story exists const story = await Story.findByPk(story_id); if (!story) { return res.status(404).json({ success: false, message: 'Không tìm thấy story', }); } // Check if relationship already exists const existing = await LessonStory.findOne({ where: { lesson_id: lessonId, story_id: story_id, }, }); if (existing) { return res.status(400).json({ success: false, message: 'Story đã tồn tại trong bài học này', }); } // Create relationship const lessonStory = await LessonStory.create({ lesson_id: lessonId, story_id: story_id, display_order, is_required, }); // Clear cache await cacheUtils.deletePattern(`lesson:${lessonId}:stories:*`); await cacheUtils.deletePattern(`story:${story_id}:lessons:*`); res.status(201).json({ success: true, message: 'Story đã được thêm vào bài học', data: lessonStory, }); } catch (error) { next(error); } } /** * Xóa story khỏi lesson */ async removeStoryFromLesson(req, res, next) { try { const { lessonId, storyId } = req.params; // Check if lesson exists const lesson = await Lesson.findByPk(lessonId); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học', }); } // Find relationship const lessonStory = await LessonStory.findOne({ where: { lesson_id: lessonId, story_id: storyId, }, }); if (!lessonStory) { return res.status(404).json({ success: false, message: 'Story không tồn tại trong bài học này', }); } await lessonStory.destroy(); // Clear cache await cacheUtils.deletePattern(`lesson:${lessonId}:stories:*`); await cacheUtils.deletePattern(`story:${storyId}:lessons:*`); res.json({ success: true, message: 'Story đã được xóa khỏi bài học', }); } catch (error) { next(error); } } /** * Cập nhật thông tin story trong lesson (display_order, is_required) */ async updateStoryInLesson(req, res, next) { try { const { lessonId, storyId } = req.params; const { display_order, is_required } = req.body; // Find relationship const lessonStory = await LessonStory.findOne({ where: { lesson_id: lessonId, story_id: storyId, }, }); if (!lessonStory) { return res.status(404).json({ success: false, message: 'Story không tồn tại trong bài học này', }); } // Update await lessonStory.update({ ...(display_order !== undefined && { display_order }), ...(is_required !== undefined && { is_required }), }); // Clear cache await cacheUtils.deletePattern(`lesson:${lessonId}:stories:*`); res.json({ success: true, message: 'Cập nhật thành công', data: lessonStory, }); } catch (error) { next(error); } } } module.exports = new LessonController();