Files
sena_db_api_layer/controllers/lessonController.js
Ken 6287a019e3
All checks were successful
Deploy to Production / deploy (push) Successful in 20s
update
2026-02-27 09:38:39 +07:00

629 lines
15 KiB
JavaScript

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();