update
All checks were successful
Deploy to Production / deploy (push) Successful in 21s

This commit is contained in:
silverpro89
2026-01-28 11:21:21 +07:00
parent 57c45d27a3
commit 3791b7cae1
23 changed files with 1033 additions and 317 deletions

3
app.js
View File

@@ -32,6 +32,7 @@ const trainingRoutes = require('./routes/trainingRoutes');
const parentTaskRoutes = require('./routes/parentTaskRoutes'); const parentTaskRoutes = require('./routes/parentTaskRoutes');
const chapterRoutes = require('./routes/chapterRoutes'); const chapterRoutes = require('./routes/chapterRoutes');
const gameRoutes = require('./routes/gameRoutes'); const gameRoutes = require('./routes/gameRoutes');
const gameTypeRoutes = require('./routes/gameTypeRoutes');
const lessonRoutes = require('./routes/lessonRoutes'); const lessonRoutes = require('./routes/lessonRoutes');
const chapterLessonRoutes = require('./routes/chapterLessonRoutes'); const chapterLessonRoutes = require('./routes/chapterLessonRoutes');
const vocabRoutes = require('./routes/vocabRoutes'); const vocabRoutes = require('./routes/vocabRoutes');
@@ -150,6 +151,7 @@ app.get('/api', (req, res) => {
chapters: '/api/chapters', chapters: '/api/chapters',
lessons: '/api/lessons', lessons: '/api/lessons',
games: '/api/games', games: '/api/games',
gameTypes: '/api/game-types',
vocab: '/api/vocab', vocab: '/api/vocab',
}, },
documentation: '/api-docs', documentation: '/api-docs',
@@ -202,6 +204,7 @@ app.use('/api/parent-tasks', parentTaskRoutes);
app.use('/api/chapters', chapterRoutes); app.use('/api/chapters', chapterRoutes);
app.use('/api/chapters', chapterLessonRoutes); // Nested route: /api/chapters/:id/lessons app.use('/api/chapters', chapterLessonRoutes); // Nested route: /api/chapters/:id/lessons
app.use('/api/games', gameRoutes); app.use('/api/games', gameRoutes);
app.use('/api/game-types', gameTypeRoutes);
app.use('/api/lessons', lessonRoutes); app.use('/api/lessons', lessonRoutes);
app.use('/api/vocab', vocabRoutes); app.use('/api/vocab', vocabRoutes);
app.use('/api/grammar', grammarRoutes); app.use('/api/grammar', grammarRoutes);

View File

@@ -1,6 +1,6 @@
const { AcademicYear } = require('../models'); const { AcademicYear } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* Academic Year Controller - Quản lý năm học * Academic Year Controller - Quản lý năm học
@@ -146,24 +146,23 @@ class AcademicYearController {
} }
/** /**
* Create new academic year (async via BullMQ) * Create new academic year
*/ */
async createAcademicYear(req, res, next) { async createAcademicYear(req, res, next) {
try { try {
const academicYearData = req.body; const academicYearData = req.body;
// Add to job queue for async processing // Create academic year directly
const job = await addDatabaseWriteJob('create', 'AcademicYear', academicYearData); const academicYear = await AcademicYear.create(academicYearData);
// Invalidate related caches // Invalidate related caches
await cacheUtils.deletePattern('academic_years:list:*'); await cacheUtils.deletePattern('academic_years:list:*');
await cacheUtils.delete('academic_year:current'); await cacheUtils.delete('academic_year:current');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Academic year creation job queued', message: 'Academic year created successfully',
jobId: job.id, data: academicYear,
data: academicYearData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -171,28 +170,34 @@ class AcademicYearController {
} }
/** /**
* Update academic year (async via BullMQ) * Update academic year
*/ */
async updateAcademicYear(req, res, next) { async updateAcademicYear(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
// Add to job queue for async processing const academicYear = await AcademicYear.findByPk(id);
const job = await addDatabaseWriteJob('update', 'AcademicYear', {
id, if (!academicYear) {
updates, return res.status(404).json({
}); success: false,
message: 'Academic year not found',
});
}
// Update academic year directly
await academicYear.update(updates);
// Invalidate related caches // Invalidate related caches
await cacheUtils.delete(`academic_year:${id}`); await cacheUtils.delete(`academic_year:${id}`);
await cacheUtils.deletePattern('academic_years:list:*'); await cacheUtils.deletePattern('academic_years:list:*');
await cacheUtils.delete('academic_year:current'); await cacheUtils.delete('academic_year:current');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Academic year update job queued', message: 'Academic year updated successfully',
jobId: job.id, data: academicYear,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -200,24 +205,32 @@ class AcademicYearController {
} }
/** /**
* Delete academic year (async via BullMQ) * Delete academic year
*/ */
async deleteAcademicYear(req, res, next) { async deleteAcademicYear(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
// Add to job queue for async processing const academicYear = await AcademicYear.findByPk(id);
const job = await addDatabaseWriteJob('delete', 'AcademicYear', { id });
if (!academicYear) {
return res.status(404).json({
success: false,
message: 'Academic year not found',
});
}
// Delete academic year directly
await academicYear.destroy();
// Invalidate related caches // Invalidate related caches
await cacheUtils.delete(`academic_year:${id}`); await cacheUtils.delete(`academic_year:${id}`);
await cacheUtils.deletePattern('academic_years:list:*'); await cacheUtils.deletePattern('academic_years:list:*');
await cacheUtils.delete('academic_year:current'); await cacheUtils.delete('academic_year:current');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Academic year deletion job queued', message: 'Academic year deleted successfully',
jobId: job.id,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -231,20 +244,31 @@ class AcademicYearController {
try { try {
const { id } = req.params; const { id } = req.params;
// Add to job queue to update current year const academicYear = await AcademicYear.findByPk(id);
const job = await addDatabaseWriteJob('update', 'AcademicYear', {
id, if (!academicYear) {
updates: { is_current: true }, return res.status(404).json({
// Will also set all other years to is_current: false success: false,
}); message: 'Academic year not found',
});
}
// Set all other years to not current
await AcademicYear.update(
{ is_current: false },
{ where: {} }
);
// Set this year as current
await academicYear.update({ is_current: true });
// Invalidate all academic year caches // Invalidate all academic year caches
await cacheUtils.deletePattern('academic_year*'); await cacheUtils.deletePattern('academic_year*');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Set current academic year job queued', message: 'Academic year set as current successfully',
jobId: job.id, data: academicYear,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,6 @@
const { AttendanceLog, AttendanceDaily, UsersAuth, School } = require('../models'); const { AttendanceLog, AttendanceDaily, UsersAuth, School } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob, addAttendanceProcessJob } = require('../config/bullmq'); const { addAttendanceProcessJob } = require('../config/bullmq');
/** /**
* Attendance Controller - Quản lý điểm danh * Attendance Controller - Quản lý điểm danh
@@ -82,15 +82,15 @@ class AttendanceController {
try { try {
const logData = req.body; const logData = req.body;
const job = await addDatabaseWriteJob('create', 'AttendanceLog', logData); const newLog = await AttendanceLog.create(logData);
await cacheUtils.deletePattern('attendance:logs:*'); await cacheUtils.deletePattern('attendance:logs:*');
await cacheUtils.deletePattern('attendance:daily:*'); await cacheUtils.deletePattern('attendance:daily:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Attendance log created', message: 'Attendance log created successfully',
jobId: job.id, data: newLog,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,7 +1,6 @@
const { Class } = require('../models'); const { Class } = require('../models');
const { StudentDetail, UserProfile, UsersAuth, School } = require('../models'); const { StudentDetail, UserProfile, UsersAuth, School } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* Class Controller - Quản lý thông tin lớp học * Class Controller - Quản lý thông tin lớp học
@@ -107,22 +106,55 @@ class ClassController {
} }
/** /**
* Create new class (async via BullMQ) * Create new class
*/ */
async createClass(req, res, next) { async createClass(req, res, next) {
try { try {
const classData = req.body; const classData = req.body;
// Add to job queue for async processing // Create class directly
const job = await addDatabaseWriteJob('create', 'Class', classData); const newClass = await Class.create(classData);
// Invalidate related caches // Invalidate related caches
await cacheUtils.deletePattern('classes:list:*'); await cacheUtils.deletePattern('classes:list:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Class creation job queued', message: 'Class created successfully',
jobId: job.id, data: newClass,
});
} catch (error) {
next(error);
}
}
/**
* Update class
*/
async updateClass(req, res, next) {
try {
const { id } = req.params;
const updates = req.body;
const classData = await Class.findByPk(id);
if (!classData) {
return res.status(404).json({
success: false,
message: 'Class not found',
});
}
// Update class directly
await classData.update(updates);
// Invalidate related caches
await cacheUtils.delete(`class:${id}`);
await cacheUtils.deletePattern('classes:list:*');
res.json({
success: true,
message: 'Class updated successfully',
data: classData, data: classData,
}); });
} catch (error) { } catch (error) {
@@ -131,51 +163,31 @@ class ClassController {
} }
/** /**
* Update class (async via BullMQ) * Delete class
*/
async updateClass(req, res, next) {
try {
const { id } = req.params;
const updates = req.body;
// Add to job queue for async processing
const job = await addDatabaseWriteJob('update', 'Class', {
id,
updates,
});
// Invalidate related caches
await cacheUtils.delete(`class:${id}`);
await cacheUtils.deletePattern('classes:list:*');
res.status(202).json({
success: true,
message: 'Class update job queued',
jobId: job.id,
});
} catch (error) {
next(error);
}
}
/**
* Delete class (async via BullMQ)
*/ */
async deleteClass(req, res, next) { async deleteClass(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
// Add to job queue for async processing const classData = await Class.findByPk(id);
const job = await addDatabaseWriteJob('delete', 'Class', { id });
if (!classData) {
return res.status(404).json({
success: false,
message: 'Class not found',
});
}
// Delete class directly
await classData.destroy();
// Invalidate related caches // Invalidate related caches
await cacheUtils.delete(`class:${id}`); await cacheUtils.delete(`class:${id}`);
await cacheUtils.deletePattern('classes:list:*'); await cacheUtils.deletePattern('classes:list:*');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Class deletion job queued', message: 'Class deleted successfully',
jobId: job.id,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -0,0 +1,343 @@
const { GameType } = require('../models');
const { cacheUtils } = require('../config/redis');
/**
* GameType Controller - Quản lý loại trò chơi giáo dục
*/
class GameTypeController {
/**
* Get all game types with pagination
*/
async getAllGameTypes(req, res, next) {
try {
const { page = 1, limit = 20, is_active, is_premium, difficulty_level } = req.query;
const offset = (page - 1) * limit;
const cacheKey = `game_types:list:${page}:${limit}:${is_active || 'all'}:${is_premium || 'all'}:${difficulty_level || 'all'}`;
const cached = await cacheUtils.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
cached: true,
});
}
const where = {};
if (is_active !== undefined) where.is_active = is_active === 'true';
if (is_premium !== undefined) where.is_premium = is_premium === 'true';
if (difficulty_level) where.difficulty_level = difficulty_level;
const { count, rows } = await GameType.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['display_order', 'ASC'], ['rating', 'DESC']],
});
const result = {
gameTypes: rows,
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);
}
}
/**
* Get game type by ID
*/
async getGameTypeById(req, res, next) {
try {
const { id } = req.params;
const cacheKey = `game_type:${id}`;
const cached = await cacheUtils.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
cached: true,
});
}
const gameType = await GameType.findByPk(id);
if (!gameType) {
return res.status(404).json({
success: false,
error: 'Game type not found',
});
}
await cacheUtils.set(cacheKey, gameType, 3600);
res.json({
success: true,
data: gameType,
cached: false,
});
} catch (error) {
next(error);
}
}
/**
* Get game type by type code
*/
async getGameTypeByType(req, res, next) {
try {
const { type } = req.params;
const cacheKey = `game_type:type:${type}`;
const cached = await cacheUtils.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
cached: true,
});
}
const gameType = await GameType.findOne({
where: { type },
});
if (!gameType) {
return res.status(404).json({
success: false,
error: 'Game type not found',
});
}
await cacheUtils.set(cacheKey, gameType, 3600);
res.json({
success: true,
data: gameType,
cached: false,
});
} catch (error) {
next(error);
}
}
/**
* Create new game type
*/
async createGameType(req, res, next) {
try {
const gameTypeData = req.body;
// Check if type already exists
const existingType = await GameType.findOne({
where: { type: gameTypeData.type },
});
if (existingType) {
return res.status(400).json({
success: false,
error: 'Game type with this type code already exists',
});
}
const gameType = await GameType.create(gameTypeData);
// Invalidate cache
await cacheUtils.deletePattern('game_types:*');
res.status(201).json({
success: true,
data: gameType,
message: 'Game type created successfully',
});
} catch (error) {
next(error);
}
}
/**
* Update game type
*/
async updateGameType(req, res, next) {
try {
const { id } = req.params;
const updateData = req.body;
const gameType = await GameType.findByPk(id);
if (!gameType) {
return res.status(404).json({
success: false,
error: 'Game type not found',
});
}
// If updating type code, check uniqueness
if (updateData.type && updateData.type !== gameType.type) {
const existingType = await GameType.findOne({
where: { type: updateData.type },
});
if (existingType) {
return res.status(400).json({
success: false,
error: 'Game type with this type code already exists',
});
}
}
await gameType.update(updateData);
// Invalidate cache
await cacheUtils.del(`game_type:${id}`);
await cacheUtils.del(`game_type:type:${gameType.type}`);
await cacheUtils.deletePattern('game_types:*');
res.json({
success: true,
data: gameType,
message: 'Game type updated successfully',
});
} catch (error) {
next(error);
}
}
/**
* Delete game type
*/
async deleteGameType(req, res, next) {
try {
const { id } = req.params;
const gameType = await GameType.findByPk(id);
if (!gameType) {
return res.status(404).json({
success: false,
error: 'Game type not found',
});
}
const typeCode = gameType.type;
await gameType.destroy();
// Invalidate cache
await cacheUtils.del(`game_type:${id}`);
await cacheUtils.del(`game_type:type:${typeCode}`);
await cacheUtils.deletePattern('game_types:*');
res.json({
success: true,
message: 'Game type deleted successfully',
});
} catch (error) {
next(error);
}
}
/**
* Get active game types only
*/
async getActiveGameTypes(req, res, next) {
try {
const cacheKey = 'game_types:active';
const cached = await cacheUtils.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
cached: true,
});
}
const gameTypes = await GameType.findAll({
where: { is_active: true },
order: [['display_order', 'ASC'], ['rating', 'DESC']],
});
await cacheUtils.set(cacheKey, gameTypes, 3600);
res.json({
success: true,
data: gameTypes,
cached: false,
});
} catch (error) {
next(error);
}
}
/**
* Get game types statistics
*/
async getGameTypeStats(req, res, next) {
try {
const cacheKey = 'game_types:stats';
const cached = await cacheUtils.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
cached: true,
});
}
const [total, active, premium, byDifficulty] = await Promise.all([
GameType.count(),
GameType.count({ where: { is_active: true } }),
GameType.count({ where: { is_premium: true } }),
GameType.findAll({
attributes: [
'difficulty_level',
[GameType.sequelize.fn('COUNT', GameType.sequelize.col('id')), 'count'],
],
group: ['difficulty_level'],
}),
]);
const stats = {
total,
active,
premium,
inactive: total - active,
byDifficulty: byDifficulty.reduce((acc, item) => {
if (item.difficulty_level) {
acc[item.difficulty_level] = parseInt(item.dataValues.count);
}
return acc;
}, {}),
};
await cacheUtils.set(cacheKey, stats, 1800);
res.json({
success: true,
data: stats,
cached: false,
});
} catch (error) {
next(error);
}
}
}
module.exports = new GameTypeController();

View File

@@ -1,6 +1,6 @@
const { Grade, GradeItem, GradeSummary, StudentDetail, Subject, AcademicYear } = require('../models'); const { Grade, GradeItem, GradeSummary, StudentDetail, Subject, AcademicYear } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob, addGradeCalculationJob } = require('../config/bullmq'); const { addGradeCalculationJob } = require('../config/bullmq');
/** /**
* Grade Controller - Quản lý điểm số * Grade Controller - Quản lý điểm số
@@ -121,22 +121,21 @@ class GradeController {
} }
/** /**
* Create new grade (async via BullMQ) * Create new grade
*/ */
async createGrade(req, res, next) { async createGrade(req, res, next) {
try { try {
const gradeData = req.body; const gradeData = req.body;
const job = await addDatabaseWriteJob('create', 'Grade', gradeData); const grade = await Grade.create(gradeData);
await cacheUtils.deletePattern('grades:list:*'); await cacheUtils.deletePattern('grades:list:*');
await cacheUtils.deletePattern(`grade:student:${gradeData.student_id}:*`); await cacheUtils.deletePattern(`grade:student:${gradeData.student_id}:*`);
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Grade creation job queued', message: 'Grade created successfully',
jobId: job.id, data: grade,
data: gradeData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -144,25 +143,31 @@ class GradeController {
} }
/** /**
* Update grade (async via BullMQ) * Update grade
*/ */
async updateGrade(req, res, next) { async updateGrade(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
const job = await addDatabaseWriteJob('update', 'Grade', { const grade = await Grade.findByPk(id);
id,
updates, if (!grade) {
}); return res.status(404).json({
success: false,
message: 'Grade not found',
});
}
await grade.update(updates);
await cacheUtils.delete(`grade:${id}`); await cacheUtils.delete(`grade:${id}`);
await cacheUtils.deletePattern('grades:list:*'); await cacheUtils.deletePattern('grades:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Grade update job queued', message: 'Grade updated successfully',
jobId: job.id, data: grade,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -170,21 +175,29 @@ class GradeController {
} }
/** /**
* Delete grade (async via BullMQ) * Delete grade
*/ */
async deleteGrade(req, res, next) { async deleteGrade(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const job = await addDatabaseWriteJob('delete', 'Grade', { id }); const grade = await Grade.findByPk(id);
if (!grade) {
return res.status(404).json({
success: false,
message: 'Grade not found',
});
}
await grade.destroy();
await cacheUtils.delete(`grade:${id}`); await cacheUtils.delete(`grade:${id}`);
await cacheUtils.deletePattern('grades:list:*'); await cacheUtils.deletePattern('grades:list:*');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Grade deletion job queued', message: 'Grade deleted successfully',
jobId: job.id,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,6 @@
const { ParentAssignedTask, GradeItem, StudentDetail, UsersAuth, ParentStudentMap } = require('../models'); const { ParentAssignedTask, GradeItem, StudentDetail, UsersAuth, ParentStudentMap } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob, addNotificationJob } = require('../config/bullmq'); const { addNotificationJob } = require('../config/bullmq');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
/** /**
@@ -64,8 +64,8 @@ class ParentTaskController {
priority: priority || 'normal', priority: priority || 'normal',
}; };
// Async write to DB via BullMQ // Create task directly in database
await addDatabaseWriteJob('create', 'ParentAssignedTask', taskData); const task = await ParentAssignedTask.create(taskData);
// Gửi notification cho học sinh // Gửi notification cho học sinh
await addNotificationJob({ await addNotificationJob({
@@ -80,10 +80,10 @@ class ParentTaskController {
await cacheUtils.del(`parent:tasks:${parent_id}`); await cacheUtils.del(`parent:tasks:${parent_id}`);
await cacheUtils.del(`student:tasks:${student_id}`); await cacheUtils.del(`student:tasks:${student_id}`);
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Task assignment request queued', message: 'Task assigned successfully',
data: taskData, data: task,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -138,11 +138,7 @@ class ParentTaskController {
for (const task of tasks) { for (const task of tasks) {
if (task.status !== 'completed' && new Date(task.due_date) < now) { if (task.status !== 'completed' && new Date(task.due_date) < now) {
if (task.status !== 'overdue') { if (task.status !== 'overdue') {
await addDatabaseWriteJob('update', 'ParentAssignedTask', { await task.update({ status: 'overdue' });
id: task.id,
status: 'overdue',
});
task.status = 'overdue';
} }
} }
} }
@@ -235,8 +231,7 @@ class ParentTaskController {
}); });
} }
await addDatabaseWriteJob('update', 'ParentAssignedTask', { await task.update({
id,
status: 'completed', status: 'completed',
completion_date: new Date(), completion_date: new Date(),
student_notes: student_notes || null, student_notes: student_notes || null,
@@ -255,9 +250,10 @@ class ParentTaskController {
await cacheUtils.del(`parent:tasks:${task.parent_id}:*`); await cacheUtils.del(`parent:tasks:${task.parent_id}:*`);
await cacheUtils.del(`student:tasks:${task.student_id}:*`); await cacheUtils.del(`student:tasks:${task.student_id}:*`);
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Task completion request queued', message: 'Task completed successfully',
data: task,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -288,18 +284,16 @@ class ParentTaskController {
}); });
} }
await addDatabaseWriteJob('update', 'ParentAssignedTask', { await task.update({ status });
id,
status,
});
// Clear cache // Clear cache
await cacheUtils.del(`parent:tasks:${task.parent_id}:*`); await cacheUtils.del(`parent:tasks:${task.parent_id}:*`);
await cacheUtils.del(`student:tasks:${task.student_id}:*`); await cacheUtils.del(`student:tasks:${task.student_id}:*`);
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Task status update queued', message: 'Task status updated successfully',
data: task,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -330,15 +324,15 @@ class ParentTaskController {
}); });
} }
await addDatabaseWriteJob('delete', 'ParentAssignedTask', { id }); await task.destroy();
// Clear cache // Clear cache
await cacheUtils.del(`parent:tasks:${task.parent_id}:*`); await cacheUtils.del(`parent:tasks:${task.parent_id}:*`);
await cacheUtils.del(`student:tasks:${task.student_id}:*`); await cacheUtils.del(`student:tasks:${task.student_id}:*`);
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Task deletion request queued', message: 'Task deleted successfully',
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { Room, School } = require('../models'); const { Room, School } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* Room Controller - Quản lý phòng học * Room Controller - Quản lý phòng học
@@ -97,21 +96,20 @@ class RoomController {
} }
/** /**
* Create new room (async via BullMQ) * Create new room
*/ */
async createRoom(req, res, next) { async createRoom(req, res, next) {
try { try {
const roomData = req.body; const roomData = req.body;
const job = await addDatabaseWriteJob('create', 'Room', roomData); const room = await Room.create(roomData);
await cacheUtils.deletePattern('rooms:list:*'); await cacheUtils.deletePattern('rooms:list:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Room creation job queued', message: 'Room created successfully',
jobId: job.id, data: room,
data: roomData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -119,25 +117,31 @@ class RoomController {
} }
/** /**
* Update room (async via BullMQ) * Update room
*/ */
async updateRoom(req, res, next) { async updateRoom(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
const job = await addDatabaseWriteJob('update', 'Room', { const room = await Room.findByPk(id);
id,
updates, if (!room) {
}); return res.status(404).json({
success: false,
message: 'Room not found',
});
}
await room.update(updates);
await cacheUtils.delete(`room:${id}`); await cacheUtils.delete(`room:${id}`);
await cacheUtils.deletePattern('rooms:list:*'); await cacheUtils.deletePattern('rooms:list:*');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Room update job queued', message: 'Room updated successfully',
jobId: job.id, data: room,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -145,21 +149,29 @@ class RoomController {
} }
/** /**
* Delete room (async via BullMQ) * Delete room
*/ */
async deleteRoom(req, res, next) { async deleteRoom(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const job = await addDatabaseWriteJob('delete', 'Room', { id }); const room = await Room.findByPk(id);
if (!room) {
return res.status(404).json({
success: false,
message: 'Room not found',
});
}
await room.destroy();
await cacheUtils.delete(`room:${id}`); await cacheUtils.delete(`room:${id}`);
await cacheUtils.deletePattern('rooms:list:*'); await cacheUtils.deletePattern('rooms:list:*');
res.status(202).json({ res.json({
success: true, success: true,
message: 'Room deletion job queued', message: 'Room deleted successfully',
jobId: job.id,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { School } = require('../models'); const { School } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* School Controller - Quản lý thông tin trường học * School Controller - Quản lý thông tin trường học
@@ -108,12 +107,11 @@ class SchoolController {
} }
/** /**
* Create new school - Push to BullMQ * Create new school
*/ */
async createSchool(req, res, next) { async createSchool(req, res, next) {
try { try {
const schoolData = req.body; const schoolData = req.body;
const userId = req.user?.id; // From auth middleware
// Validate required fields (you can use Joi for this) // Validate required fields (you can use Joi for this)
if (!schoolData.school_code || !schoolData.school_name || !schoolData.school_type) { if (!schoolData.school_code || !schoolData.school_name || !schoolData.school_type) {
@@ -123,22 +121,14 @@ class SchoolController {
}); });
} }
// Add job to BullMQ queue const newSchool = await School.create(schoolData);
const job = await addDatabaseWriteJob(
'create',
'School',
schoolData,
{ userId, priority: 3 }
);
res.status(202).json({ await cacheUtils.deletePattern('schools:list:*');
res.status(201).json({
success: true, success: true,
message: 'School creation queued', message: 'School created successfully',
jobId: job.id, data: newSchool,
data: {
school_code: schoolData.school_code,
school_name: schoolData.school_name,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -146,44 +136,31 @@ class SchoolController {
} }
/** /**
* Update school - Push to BullMQ * Update school
*/ */
async updateSchool(req, res, next) { async updateSchool(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updateData = req.body; const updateData = req.body;
const userId = req.user?.id;
// Check if school exists (from cache or DB)
const cacheKey = `school:${id}`;
let school = await cacheUtils.get(cacheKey);
const school = await School.findByPk(id);
if (!school) { if (!school) {
school = await School.findByPk(id); return res.status(404).json({
if (!school) { success: false,
return res.status(404).json({ message: 'School not found',
success: false, });
message: 'School not found',
});
}
} }
// Add update job to BullMQ await school.update(updateData);
const job = await addDatabaseWriteJob(
'update',
'School',
{ id, ...updateData },
{ userId, priority: 3 }
);
// Invalidate cache // Invalidate cache
await cacheUtils.delete(cacheKey); await cacheUtils.delete(`school:${id}`);
await cacheUtils.deletePattern('schools:list:*'); await cacheUtils.deletePattern('schools:list:*');
res.json({ res.status(200).json({
success: true, success: true,
message: 'School update queued', message: 'School updated successfully',
jobId: job.id, data: school,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -191,12 +168,11 @@ class SchoolController {
} }
/** /**
* Delete school - Push to BullMQ * Delete school (soft delete)
*/ */
async deleteSchool(req, res, next) { async deleteSchool(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const userId = req.user?.id;
// Check if school exists // Check if school exists
const school = await School.findByPk(id); const school = await School.findByPk(id);
@@ -208,21 +184,16 @@ class SchoolController {
} }
// Soft delete - just mark as inactive // Soft delete - just mark as inactive
const job = await addDatabaseWriteJob( await school.update({ is_active: false });
'update',
'School',
{ id, is_active: false },
{ userId, priority: 3 }
);
// Invalidate cache // Invalidate cache
await cacheUtils.delete(`school:${id}`); await cacheUtils.delete(`school:${id}`);
await cacheUtils.deletePattern('schools:list:*'); await cacheUtils.deletePattern('schools:list:*');
res.json({ res.status(200).json({
success: true, success: true,
message: 'School deletion queued', message: 'School deactivated successfully',
jobId: job.id, data: school,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { StudentDetail, UserProfile, UsersAuth, Class } = require('../models'); const { StudentDetail, UserProfile, UsersAuth, Class } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* Student Controller - Quản lý học sinh * Student Controller - Quản lý học sinh
@@ -120,21 +119,20 @@ class StudentController {
} }
/** /**
* Create new student (async via BullMQ) * Create new student
*/ */
async createStudent(req, res, next) { async createStudent(req, res, next) {
try { try {
const studentData = req.body; const studentData = req.body;
const job = await addDatabaseWriteJob('create', 'StudentDetail', studentData); const newStudent = await StudentDetail.create(studentData);
await cacheUtils.deletePattern('students:list:*'); await cacheUtils.deletePattern('students:list:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Student creation job queued', message: 'Student created successfully',
jobId: job.id, data: newStudent,
data: studentData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -142,25 +140,30 @@ class StudentController {
} }
/** /**
* Update student (async via BullMQ) * Update student
*/ */
async updateStudent(req, res, next) { async updateStudent(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
const job = await addDatabaseWriteJob('update', 'StudentDetail', { const student = await StudentDetail.findByPk(id);
id, if (!student) {
updates, return res.status(404).json({
}); success: false,
message: 'Student not found',
});
}
await student.update(updates);
await cacheUtils.delete(`student:${id}`); await cacheUtils.delete(`student:${id}`);
await cacheUtils.deletePattern('students:list:*'); await cacheUtils.deletePattern('students:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Student update job queued', message: 'Student updated successfully',
jobId: job.id, data: student,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -174,18 +177,23 @@ class StudentController {
try { try {
const { id } = req.params; const { id } = req.params;
const job = await addDatabaseWriteJob('update', 'StudentDetail', { const student = await StudentDetail.findByPk(id);
id, if (!student) {
updates: { status: 'dropped' }, return res.status(404).json({
}); success: false,
message: 'Student not found',
});
}
await student.update({ status: 'dropped' });
await cacheUtils.delete(`student:${id}`); await cacheUtils.delete(`student:${id}`);
await cacheUtils.deletePattern('students:list:*'); await cacheUtils.deletePattern('students:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Student deletion job queued', message: 'Student status updated to dropped',
jobId: job.id, data: student,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { Subject, Chapter } = require('../models'); const { Subject, Chapter } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
/** /**
* Subject Controller - Quản lý môn học * Subject Controller - Quản lý môn học
@@ -147,22 +146,20 @@ class SubjectController {
} }
/** /**
* Create new subject (async via BullMQ) * Create new subject
*/ */
async createSubject(req, res, next) { async createSubject(req, res, next) {
try { try {
const subjectData = req.body; const subjectData = req.body;
// Add to job queue for async processing const subject = await Subject.create(subjectData);
const job = await addDatabaseWriteJob('create', 'Subject', subjectData);
// Note: Cache will be invalidated by worker after successful creation await cacheUtils.deletePattern('subjects:list:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Subject creation job queued', message: 'Subject created successfully',
jobId: job.id, data: subject,
data: subjectData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -170,25 +167,31 @@ class SubjectController {
} }
/** /**
* Update subject (async via BullMQ) * Update subject
*/ */
async updateSubject(req, res, next) { async updateSubject(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
// Add to job queue for async processing const subject = await Subject.findByPk(id);
const job = await addDatabaseWriteJob('update', 'Subject', {
id,
updates,
});
// Note: Cache will be invalidated by worker after successful update if (!subject) {
return res.status(404).json({
success: false,
message: 'Subject not found',
});
}
res.status(202).json({ await subject.update(updates);
await cacheUtils.delete(`subject:${id}`);
await cacheUtils.deletePattern('subjects:list:*');
res.json({
success: true, success: true,
message: 'Subject update job queued', message: 'Subject updated successfully',
jobId: job.id, data: subject,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -196,21 +199,29 @@ class SubjectController {
} }
/** /**
* Delete subject (async via BullMQ) * Delete subject
*/ */
async deleteSubject(req, res, next) { async deleteSubject(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
// Add to job queue for async processing const subject = await Subject.findByPk(id);
const job = await addDatabaseWriteJob('delete', 'Subject', { id });
// Note: Cache will be invalidated by worker after successful deletion if (!subject) {
return res.status(404).json({
success: false,
message: 'Subject not found',
});
}
res.status(202).json({ await subject.destroy();
await cacheUtils.delete(`subject:${id}`);
await cacheUtils.deletePattern('subjects:list:*');
res.json({
success: true, success: true,
message: 'Subject deletion job queued', message: 'Subject deleted successfully',
jobId: job.id,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { SubscriptionPlan, UserSubscription, UsersAuth } = require('../models'); const { SubscriptionPlan, UserSubscription, UsersAuth } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
/** /**
@@ -98,16 +97,15 @@ class SubscriptionController {
auto_renew: req.body.auto_renew || false, auto_renew: req.body.auto_renew || false,
}; };
// Async write to DB via BullMQ const newSubscription = await UserSubscription.create(subscriptionData);
await addDatabaseWriteJob('create', 'UserSubscription', subscriptionData);
// Clear cache // Clear cache
await cacheUtils.del(`subscription:user:${user_id}`); await cacheUtils.del(`subscription:user:${user_id}`);
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Subscription purchase request queued', message: 'Subscription purchased successfully',
data: subscriptionData, data: newSubscription,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -153,11 +151,7 @@ class SubscriptionController {
// Check nếu subscription đã hết hạn // Check nếu subscription đã hết hạn
const now = new Date(); const now = new Date();
if (subscription.status === 'active' && new Date(subscription.end_date) < now) { if (subscription.status === 'active' && new Date(subscription.end_date) < now) {
subscription.status = 'expired'; await subscription.update({ status: 'expired' });
await addDatabaseWriteJob('update', 'UserSubscription', {
id: subscription.id,
status: 'expired',
});
} }
await cacheUtils.set(cacheKey, subscription, 900); // Cache 15 min await cacheUtils.set(cacheKey, subscription, 900); // Cache 15 min
@@ -195,9 +189,8 @@ class SubscriptionController {
}); });
} }
// Async update via BullMQ // Update subscription
await addDatabaseWriteJob('update', 'UserSubscription', { await subscription.update({
id,
status: 'cancelled', status: 'cancelled',
auto_renew: false, auto_renew: false,
}); });
@@ -205,9 +198,10 @@ class SubscriptionController {
// Clear cache // Clear cache
await cacheUtils.del(`subscription:user:${subscription.user_id}`); await cacheUtils.del(`subscription:user:${subscription.user_id}`);
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Subscription cancellation request queued', message: 'Subscription cancelled successfully',
data: subscription,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { TeacherDetail, UserProfile, UsersAuth } = require('../models'); const { TeacherDetail, UserProfile, UsersAuth } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
const teacherProfileService = require('../services/teacherProfileService'); const teacherProfileService = require('../services/teacherProfileService');
/** /**
@@ -205,25 +204,30 @@ class TeacherController {
} }
/** /**
* Update teacher (async via BullMQ) * Update teacher
*/ */
async updateTeacher(req, res, next) { async updateTeacher(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
const job = await addDatabaseWriteJob('update', 'TeacherDetail', { const teacher = await TeacherDetail.findByPk(id);
id, if (!teacher) {
updates, return res.status(404).json({
}); success: false,
message: 'Teacher not found',
});
}
await teacher.update(updates);
await cacheUtils.delete(`teacher:${id}`); await cacheUtils.delete(`teacher:${id}`);
await cacheUtils.deletePattern('teachers:list:*'); await cacheUtils.deletePattern('teachers:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Teacher update job queued', message: 'Teacher updated successfully',
jobId: job.id, data: teacher,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -237,17 +241,28 @@ class TeacherController {
try { try {
const { id } = req.params; const { id } = req.params;
const job = await addDatabaseWriteJob('update', 'TeacherDetail', { const teacher = await TeacherDetail.findByPk(id);
id, if (!teacher) {
updates: { status: 'resigned' }, return res.status(404).json({
}); success: false,
message: 'Teacher not found',
});
}
await teacher.update({ status: 'resigned' });
await cacheUtils.delete(`teacher:${id}`); await cacheUtils.delete(`teacher:${id}`);
await cacheUtils.deletePattern('teachers:list:*'); await cacheUtils.deletePattern('teachers:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Teacher deletion job queued', message: 'Teacher status updated to resigned',
data: teacher,
});
} catch (error) {
next(error);
}
}
jobId: job.id, jobId: job.id,
}); });
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,5 @@
const { StaffTrainingAssignment, StaffAchievement, UsersAuth, Subject } = require('../models'); const { StaffTrainingAssignment, StaffAchievement, UsersAuth, Subject } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
/** /**
@@ -55,11 +54,7 @@ class TrainingController {
for (const assignment of assignments) { for (const assignment of assignments) {
if (assignment.status !== 'completed' && new Date(assignment.deadline) < now) { if (assignment.status !== 'completed' && new Date(assignment.deadline) < now) {
if (assignment.status !== 'overdue') { if (assignment.status !== 'overdue') {
await addDatabaseWriteJob('update', 'StaffTrainingAssignment', { await assignment.update({ status: 'overdue' });
id: assignment.id,
status: 'overdue',
});
assignment.status = 'overdue';
} }
} }
} }
@@ -102,16 +97,15 @@ class TrainingController {
notes: notes || null, notes: notes || null,
}; };
// Async write to DB via BullMQ const newAssignment = await StaffTrainingAssignment.create(assignmentData);
await addDatabaseWriteJob('create', 'StaffTrainingAssignment', assignmentData);
// Clear cache // Clear cache
await cacheUtils.del(`training:assignments:${staff_id}:*`); await cacheUtils.del(`training:assignments:${staff_id}:*`);
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Training assignment request queued', message: 'Training assignment created successfully',
data: assignmentData, data: newAssignment,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -142,17 +136,15 @@ class TrainingController {
}); });
} }
await addDatabaseWriteJob('update', 'StaffTrainingAssignment', { await assignment.update({ status });
id,
status,
});
// Clear cache // Clear cache
await cacheUtils.del(`training:assignments:${assignment.staff_id}:*`); await cacheUtils.del(`training:assignments:${assignment.staff_id}:*`);
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'Assignment status update queued', message: 'Assignment status updated successfully',
data: assignment,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -199,16 +191,15 @@ class TrainingController {
verified_at: verified_by ? new Date() : null, verified_at: verified_by ? new Date() : null,
}; };
// Async write to DB via BullMQ const newAchievement = await StaffAchievement.create(achievementData);
await addDatabaseWriteJob('create', 'StaffAchievement', achievementData);
// Clear cache // Clear cache
await cacheUtils.del(`training:achievements:${staff_id}`); await cacheUtils.del(`training:achievements:${staff_id}`);
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'Achievement creation request queued', message: 'Achievement created successfully',
data: achievementData, data: newAchievement,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,6 +1,5 @@
const { UsersAuth, UserProfile } = require('../models'); const { UsersAuth, UserProfile } = require('../models');
const { cacheUtils } = require('../config/redis'); const { cacheUtils } = require('../config/redis');
const { addDatabaseWriteJob } = require('../config/bullmq');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const crypto = require('crypto'); const crypto = require('crypto');
@@ -404,21 +403,20 @@ class UserController {
} }
/** /**
* Create new user (async via BullMQ) * Create new user
*/ */
async createUser(req, res, next) { async createUser(req, res, next) {
try { try {
const userData = req.body; const userData = req.body;
const job = await addDatabaseWriteJob('create', 'UsersAuth', userData); const newUser = await UsersAuth.create(userData);
await cacheUtils.deletePattern('users:list:*'); await cacheUtils.deletePattern('users:list:*');
res.status(202).json({ res.status(201).json({
success: true, success: true,
message: 'User creation job queued', message: 'User created successfully',
jobId: job.id, data: newUser,
data: userData,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -426,25 +424,30 @@ class UserController {
} }
/** /**
* Update user (async via BullMQ) * Update user
*/ */
async updateUser(req, res, next) { async updateUser(req, res, next) {
try { try {
const { id } = req.params; const { id } = req.params;
const updates = req.body; const updates = req.body;
const job = await addDatabaseWriteJob('update', 'UsersAuth', { const user = await UsersAuth.findByPk(id);
id, if (!user) {
updates, return res.status(404).json({
}); success: false,
message: 'User not found',
});
}
await user.update(updates);
await cacheUtils.delete(`user:${id}`); await cacheUtils.delete(`user:${id}`);
await cacheUtils.deletePattern('users:list:*'); await cacheUtils.deletePattern('users:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'User update job queued', message: 'User updated successfully',
jobId: job.id, data: user,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -458,18 +461,23 @@ class UserController {
try { try {
const { id } = req.params; const { id } = req.params;
const job = await addDatabaseWriteJob('update', 'UsersAuth', { const user = await UsersAuth.findByPk(id);
id, if (!user) {
updates: { is_active: false }, return res.status(404).json({
}); success: false,
message: 'User not found',
});
}
await user.update({ is_active: false });
await cacheUtils.delete(`user:${id}`); await cacheUtils.delete(`user:${id}`);
await cacheUtils.deletePattern('users:list:*'); await cacheUtils.deletePattern('users:list:*');
res.status(202).json({ res.status(200).json({
success: true, success: true,
message: 'User deletion job queued', message: 'User deactivated successfully',
jobId: job.id, data: user,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

67
deploy.sh Normal file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
# Script tự động deploy và fix bcrypt trên server
# Usage: ./deploy.sh
SERVER="root@senaai.tech"
PROJECT_PATH="/var/www/services/sena_db_api"
echo "════════════════════════════════════════════════════════"
echo " 🚀 Auto Deploy & Fix Bcrypt Script"
echo " Server: $SERVER"
echo " Path: $PROJECT_PATH"
echo "════════════════════════════════════════════════════════"
echo ""
# Execute commands on remote server
ssh $SERVER << 'ENDSSH'
set -e # Exit on error
echo "📁 Navigating to project directory..."
cd /var/www/services/sena_db_api
echo ""
echo "🔧 Rebuilding bcrypt module..."
npm rebuild bcrypt --build-from-source
echo ""
echo "🔄 Rebuilding all native modules..."
npm rebuild
echo ""
echo "📂 Creating logs directory..."
mkdir -p logs
echo ""
echo "✅ Build completed successfully!"
echo ""
echo "════════════════════════════════════════════════════════"
echo " PM2 Status:"
echo "════════════════════════════════════════════════════════"
pm2 list
echo ""
echo "Do you want to restart PM2 processes? (y/n)"
read -r response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
echo ""
echo "🔄 Restarting PM2 processes..."
pm2 restart all 2>/dev/null || pm2 start start.json
echo ""
echo "✅ PM2 processes restarted!"
echo ""
pm2 list
else
echo ""
echo "⏭️ Skipped PM2 restart"
fi
echo ""
echo "════════════════════════════════════════════════════════"
echo " ✅ Deployment completed!"
echo "════════════════════════════════════════════════════════"
ENDSSH
echo ""
echo "Done! Check the output above for any errors."

17
fix-bcrypt-complete.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
REM Complete bcrypt fix for Windows
REM This will remove node_modules and reinstall everything
echo ================================================================
echo Complete Bcrypt Fix Script
echo Server: root@senaai.tech
echo ================================================================
echo.
ssh root@senaai.tech "cd /var/www/services/sena_db_api && echo 'Removing node_modules...' && rm -rf node_modules package-lock.json pnpm-lock.yaml yarn.lock && echo 'Installing with npm...' && npm install && echo 'Rebuilding bcrypt...' && npm rebuild bcrypt --build-from-source && echo 'Restarting PM2...' && pm2 restart all && pm2 list"
echo.
echo ================================================================
echo Done! Check the output above.
echo ================================================================
pause

49
fix-bcrypt-complete.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Fix bcrypt on server - Complete reinstall
# Run this script: bash fix-bcrypt-complete.sh
echo "════════════════════════════════════════════════════════"
echo " 🔧 Complete Bcrypt Fix Script"
echo " Server: root@senaai.tech"
echo "════════════════════════════════════════════════════════"
echo ""
ssh root@senaai.tech << 'ENDSSH'
set -e
echo "📁 Navigating to project..."
cd /var/www/services/sena_db_api
echo ""
echo "🗑️ Step 1: Removing old node_modules and lock files..."
rm -rf node_modules package-lock.json pnpm-lock.yaml yarn.lock
echo ""
echo "📦 Step 2: Installing dependencies with npm..."
npm install
echo ""
echo "🔨 Step 3: Rebuilding bcrypt from source..."
npm rebuild bcrypt --build-from-source
echo ""
echo "✅ Step 4: Build completed!"
echo ""
echo "🔄 Step 5: Restarting PM2 processes..."
pm2 restart sena-api
pm2 restart worker-db-write
pm2 restart worker-lesson-fill
pm2 restart worker-process-data
echo ""
echo "📊 PM2 Status:"
pm2 list
echo ""
echo "════════════════════════════════════════════════════════"
echo " ✅ All done! Check logs with: pm2 logs sena-api"
echo "════════════════════════════════════════════════════════"
ENDSSH

97
models/GameType.js Normal file
View File

@@ -0,0 +1,97 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* GameType Model - Loại trò chơi giáo dục
* Định nghĩa các loại game template/engine khác nhau
* Một GameType có thể có nhiều Game instances
*/
const GameType = sequelize.define('game_types', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
en_title: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Tên loại game (tiếng Anh)'
},
vi_title: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Tên loại game (tiếng Việt)'
},
en_desc: {
type: DataTypes.TEXT,
comment: 'Mô tả loại game (tiếng Anh)'
},
vi_desc: {
type: DataTypes.TEXT,
comment: 'Mô tả loại game (tiếng Việt)'
},
type: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
comment: 'Mã định danh loại game: counting_quiz, math_game, word_puzzle, etc.'
},
thumb: {
type: DataTypes.TEXT,
comment: 'URL ảnh thumbnail đại diện cho loại game'
},
data: {
type: DataTypes.JSON,
comment: 'Dữ liệu mẫu hoặc template data cho loại game này'
},
config: {
type: DataTypes.JSON,
comment: 'Cấu hình mặc định cho loại game: controls, settings, features, etc.'
},
is_premium: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Yêu cầu premium để sử dụng loại game này'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Loại game có đang hoạt động không'
},
difficulty_level: {
type: DataTypes.ENUM('easy', 'medium', 'hard'),
comment: 'Độ khó mặc định'
},
rating: {
type: DataTypes.DECIMAL(3, 2),
comment: 'Đánh giá trung bình (0.00 - 5.00)'
},
display_order: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Thứ tự hiển thị'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
}, {
tableName: 'game_types',
timestamps: true,
underscored: true,
indexes: [
{ fields: ['type'], unique: true },
{ fields: ['is_active'] },
{ fields: ['is_premium'] },
{ fields: ['display_order'] },
{ fields: ['difficulty_level'] },
],
});
module.exports = GameType;

View File

@@ -29,6 +29,7 @@ const Room = require('./Room');
const Chapter = require('./Chapter'); const Chapter = require('./Chapter');
const Lesson = require('./Lesson'); const Lesson = require('./Lesson');
const Game = require('./Game'); const Game = require('./Game');
const GameType = require('./GameType');
const LessonComponentProgress = require('./LessonComponentProgress'); const LessonComponentProgress = require('./LessonComponentProgress');
const LessonLeaderboard = require('./LessonLeaderboard'); const LessonLeaderboard = require('./LessonLeaderboard');
@@ -292,6 +293,7 @@ module.exports = {
Chapter, Chapter,
Lesson, Lesson,
Game, Game,
GameType,
LessonComponentProgress, LessonComponentProgress,
LessonLeaderboard, LessonLeaderboard,

23
quick-fix-bcrypt.bat Normal file
View File

@@ -0,0 +1,23 @@
@echo off
REM Windows batch script to fix bcrypt on remote server
REM Usage: quick-fix-bcrypt.bat
set SERVER=root@senaai.tech
set PROJECT_PATH=/var/www/services/sena_db_api
echo ================================================================
echo Quick Fix Bcrypt Script
echo Server: %SERVER%
echo ================================================================
echo.
echo Connecting to server and fixing bcrypt...
echo.
ssh %SERVER% "cd %PROJECT_PATH% && npm rebuild bcrypt --build-from-source && npm rebuild && mkdir -p logs && echo. && echo [SUCCESS] Bcrypt rebuilt! && echo. && pm2 restart all 2>nul || pm2 start start.json && echo. && pm2 list"
echo.
echo ================================================================
echo Done! Check the output above.
echo ================================================================
pause

28
quick-fix-bcrypt.sh Normal file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Script tự động rebuild bcrypt và restart PM2
# Usage: ./quick-fix-bcrypt.sh
SERVER="root@senaai.tech"
PROJECT_PATH="/var/www/services/sena_db_api"
echo "🔧 Quick fix bcrypt on $SERVER..."
echo ""
ssh $SERVER << ENDSSH
cd $PROJECT_PATH && \
echo "🔨 Rebuilding bcrypt..." && \
npm rebuild bcrypt --build-from-source && \
npm rebuild && \
mkdir -p logs && \
echo "" && \
echo "✅ Bcrypt rebuilt successfully!" && \
echo "" && \
echo "🔄 Restarting PM2..." && \
pm2 restart all 2>/dev/null || pm2 start start.json && \
echo "" && \
echo "📊 PM2 Status:" && \
pm2 list && \
echo "" && \
echo "✅ All done!"
ENDSSH

34
routes/gameTypeRoutes.js Normal file
View File

@@ -0,0 +1,34 @@
const express = require('express');
const router = express.Router();
const gameTypeController = require('../controllers/gameTypeController');
/**
* Game Type Routes
* Base path: /api/game-types
*/
// Get all game types
router.get('/', gameTypeController.getAllGameTypes);
// Get active game types only
router.get('/active', gameTypeController.getActiveGameTypes);
// Get game type statistics
router.get('/stats', gameTypeController.getGameTypeStats);
// Get game type by type code
router.get('/type/:type', gameTypeController.getGameTypeByType);
// Get game type by ID
router.get('/:id', gameTypeController.getGameTypeById);
// Create new game type
router.post('/', gameTypeController.createGameType);
// Update game type
router.put('/:id', gameTypeController.updateGameType);
// Delete game type
router.delete('/:id', gameTypeController.deleteGameType);
module.exports = router;