This commit is contained in:
3
app.js
3
app.js
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
343
controllers/gameTypeController.js
Normal file
343
controllers/gameTypeController.js
Normal 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();
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
67
deploy.sh
Normal 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
17
fix-bcrypt-complete.bat
Normal 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
49
fix-bcrypt-complete.sh
Normal 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
97
models/GameType.js
Normal 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;
|
||||||
@@ -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
23
quick-fix-bcrypt.bat
Normal 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
28
quick-fix-bcrypt.sh
Normal 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
34
routes/gameTypeRoutes.js
Normal 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;
|
||||||
Reference in New Issue
Block a user