diff --git a/add-knowledge-column.js b/add-knowledge-column.js deleted file mode 100644 index c0fe206..0000000 --- a/add-knowledge-column.js +++ /dev/null @@ -1,50 +0,0 @@ -const {sequelize} = require('./config/database'); - -(async () => { - try { - await sequelize.authenticate(); - console.log('✅ Database connected'); - - // Check current columns - const [cols] = await sequelize.query('DESCRIBE context'); - console.log('\n📋 Current columns:'); - cols.forEach(c => console.log(` - ${c.Field}`)); - - const columnNames = cols.map(c => c.Field); - - // Add knowledge column if not exists - if (!columnNames.includes('knowledge')) { - await sequelize.query(` - ALTER TABLE context - ADD COLUMN knowledge TEXT NULL - COMMENT 'Additional knowledge or information' - `); - console.log('\n✅ Added knowledge column'); - } else { - console.log('\n✅ knowledge column already exists'); - } - - // Add status column if not exists - if (!columnNames.includes('status')) { - await sequelize.query(` - ALTER TABLE context - ADD COLUMN status INT DEFAULT 0 - COMMENT '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved' - `); - console.log('✅ Added status column'); - } else { - console.log('✅ status column already exists'); - } - - // Show final structure - const [finalCols] = await sequelize.query('DESCRIBE context'); - console.log('\n📊 Final Context table structure:'); - finalCols.forEach((c, i) => console.log(` ${i+1}. ${c.Field} (${c.Type})`)); - - process.exit(0); - } catch (error) { - console.error('❌ Error:', error.message); - console.error(error.stack); - process.exit(1); - } -})(); diff --git a/app.js b/app.js index 9d08327..e94ebaf 100644 --- a/app.js +++ b/app.js @@ -20,6 +20,7 @@ const authRoutes = require('./routes/authRoutes'); const schoolRoutes = require('./routes/schoolRoutes'); const classRoutes = require('./routes/classRoutes'); const academicYearRoutes = require('./routes/academicYearRoutes'); +const categoryRoutes = require('./routes/categoryRoutes'); const subjectRoutes = require('./routes/subjectRoutes'); const userRoutes = require('./routes/userRoutes'); const studentRoutes = require('./routes/studentRoutes'); @@ -217,6 +218,7 @@ app.use('/api/auth', authRoutes); app.use('/api/schools', schoolRoutes); app.use('/api/classes', classRoutes); app.use('/api/academic-years', academicYearRoutes); +app.use('/api/categories', categoryRoutes); app.use('/api/subjects', subjectRoutes); app.use('/api/users', userRoutes); app.use('/api/students', studentRoutes); diff --git a/controllers/categoryController.js b/controllers/categoryController.js new file mode 100644 index 0000000..3e90e42 --- /dev/null +++ b/controllers/categoryController.js @@ -0,0 +1,354 @@ +const { Categories, Subject } = require('../models'); +const { cacheUtils } = require('../config/redis'); + +/** + * Categories Controller - Quản lý danh mục + */ +class CategoryController { + /** + * Get all categories with pagination and caching + */ + async getAllCategories(req, res, next) { + try { + const { page = 1, limit = 50, is_active } = req.query; + const offset = (page - 1) * limit; + + // Generate cache key + const cacheKey = `categories:list:${page}:${limit}:${is_active || 'all'}`; + + // Try to get from cache first + const cached = await cacheUtils.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + cached: true, + }); + } + + // Build query conditions + const where = {}; + if (is_active !== undefined) where.is_active = is_active === 'true'; + + // Query from database (through ProxySQL) + const { count, rows } = await Categories.findAndCountAll({ + where, + limit: parseInt(limit), + offset: parseInt(offset), + order: [['category_code', 'ASC']], + }); + + const result = { + categories: rows, + pagination: { + total: count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(count / limit), + }, + }; + + // Cache the result + await cacheUtils.set(cacheKey, result, 7200); // Cache for 2 hours + + res.json({ + success: true, + data: result, + cached: false, + }); + } catch (error) { + next(error); + } + } + + /** + * Get category by ID with caching + */ + async getCategoryById(req, res, next) { + try { + const { id } = req.params; + const cacheKey = `category:${id}`; + + // Try cache first + const cached = await cacheUtils.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + cached: true, + }); + } + + // Query from database + const category = await Categories.findByPk(id); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + // Cache the result + await cacheUtils.set(cacheKey, category, 7200); // Cache for 2 hours + + res.json({ + success: true, + data: category, + cached: false, + }); + } catch (error) { + next(error); + } + } + + /** + * Get category by code with caching + */ + async getCategoryByCode(req, res, next) { + try { + const { code } = req.params; + const cacheKey = `category:code:${code}`; + + // Try cache first + const cached = await cacheUtils.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + cached: true, + }); + } + + // Query from database + const category = await Categories.findOne({ + where: { category_code: code }, + }); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + // Cache the result + await cacheUtils.set(cacheKey, category, 7200); // Cache for 2 hours + + res.json({ + success: true, + data: category, + cached: false, + }); + } catch (error) { + next(error); + } + } + + /** + * Create new category + */ + async createCategory(req, res, next) { + try { + const categoryData = req.body; + + const category = await Categories.create(categoryData); + + await cacheUtils.deletePattern('categories:list:*'); + + res.status(201).json({ + success: true, + message: 'Category created successfully', + data: category, + }); + } catch (error) { + next(error); + } + } + + /** + * Update category + */ + async updateCategory(req, res, next) { + try { + const { id } = req.params; + const updates = req.body; + + const category = await Categories.findByPk(id); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + await category.update(updates); + + await cacheUtils.delete(`category:${id}`); + await cacheUtils.deletePattern('categories:list:*'); + + res.json({ + success: true, + message: 'Category updated successfully', + data: category, + }); + } catch (error) { + next(error); + } + } + + /** + * Delete category + */ + async deleteCategory(req, res, next) { + try { + const { id } = req.params; + + const category = await Categories.findByPk(id); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + await category.destroy(); + + await cacheUtils.delete(`category:${id}`); + await cacheUtils.deletePattern('categories:list:*'); + + res.json({ + success: true, + message: 'Category deleted successfully', + }); + } catch (error) { + next(error); + } + } + + /** + * Get active categories (frequently used) + */ + async getActiveCategories(req, res, next) { + try { + const cacheKey = 'categories:active'; + + // Try cache first + const cached = await cacheUtils.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + cached: true, + }); + } + + // Query from database + const categories = await Categories.findAll({ + where: { is_active: true }, + order: [['category_code', 'ASC']], + }); + + // Cache for 4 hours (this data changes infrequently) + await cacheUtils.set(cacheKey, categories, 14400); + + res.json({ + success: true, + data: categories, + cached: false, + }); + } catch (error) { + next(error); + } + } + + /** + * Get category datatypes + */ + async getCategoryDatatypes(req, res, next) { + try { + const datatypes = Categories.rawAttributes; + res.json({ + success: true, + data: datatypes, + }); + } catch (error) { + next(error); + } + } + + /** + * Get subjects by category ID + */ + async getSubjectsByCategory(req, res, next) { + try { + const { id } = req.params; + const { page = 1, limit = 50, is_active } = req.query; + const offset = (page - 1) * limit; + + // Generate cache key + const cacheKey = `category:${id}:subjects:${page}:${limit}:${is_active || 'all'}`; + + // Try cache first + const cached = await cacheUtils.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + cached: true, + }); + } + + // Check if category exists + const category = await Categories.findByPk(id); + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + // Build query conditions + const where = { category_id: id }; + if (is_active !== undefined) where.is_active = is_active === 'true'; + + // Query subjects + const { count, rows } = await Subject.findAndCountAll({ + where, + limit: parseInt(limit), + offset: parseInt(offset), + order: [['subject_code', 'ASC']], + }); + + const result = { + category: { + id: category.id, + category_code: category.category_code, + category_name: category.category_name, + }, + subjects: rows, + pagination: { + total: count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(count / limit), + }, + }; + + // Cache the result + await cacheUtils.set(cacheKey, result, 3600); // Cache for 1 hour + + res.json({ + success: true, + data: result, + cached: false, + }); + } catch (error) { + next(error); + } + } +} + +module.exports = new CategoryController(); diff --git a/models/Categories.js b/models/Categories.js new file mode 100644 index 0000000..e254064 --- /dev/null +++ b/models/Categories.js @@ -0,0 +1,30 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const Categories = sequelize.define('Categories', { + id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, + category_code: { type: DataTypes.STRING(20), unique: true, allowNull: false }, + category_name: { type: DataTypes.STRING(100), allowNull: false }, + category_thumbnail: { type: DataTypes.STRING(255) }, + category_name_en: { type: DataTypes.STRING(100) }, + description: { type: DataTypes.TEXT }, + is_active: { type: DataTypes.BOOLEAN, defaultValue: true }, + is_premium: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung premium' }, + is_training: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung đào tạo nhân sự' }, + is_public: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung tự học công khai' }, + required_role: { type: DataTypes.STRING(50), comment: 'Role yêu cầu' }, + min_subscription_tier: { type: DataTypes.STRING(50), comment: 'Gói tối thiểu: basic, premium, vip' }, + created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, + updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, +}, { + tableName: 'Categories', + timestamps: true, + underscored: true, + indexes: [ + { fields: ['is_premium'] }, + { fields: ['is_training'] }, + { fields: ['is_public'] }, + ], +}); + +module.exports = Categories; diff --git a/models/Subject.js b/models/Subject.js index d594f5d..0a8d422 100644 --- a/models/Subject.js +++ b/models/Subject.js @@ -3,6 +3,7 @@ const { sequelize } = require('../config/database'); const Subject = sequelize.define('subjects', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, + category_id: { type: DataTypes.UUID, allowNull: false, comment: 'Danh mục chủ đề' }, subject_code: { type: DataTypes.STRING(20), unique: true, allowNull: false }, subject_name: { type: DataTypes.STRING(100), allowNull: false }, subject_name_en: { type: DataTypes.STRING(100) }, diff --git a/models/index.js b/models/index.js index b390b2a..834a2da 100644 --- a/models/index.js +++ b/models/index.js @@ -20,6 +20,7 @@ const StaffContract = require('./StaffContract'); // Group 3: Academic Structure const AcademicYear = require('./AcademicYear'); +const Categories = require('./Categories'); const Subject = require('./Subject'); const Class = require('./Class'); const ClassSchedule = require('./ClassSchedule'); @@ -149,6 +150,10 @@ const setupRelationships = () => { ClassSchedule.belongsTo(TeacherDetail, { foreignKey: 'teacher_id', as: 'teacher' }); // Learning Content relationships (NEW) + // Categories -> Subject (1:N) + Categories.hasMany(Subject, { foreignKey: 'category_id', as: 'subjects' }); + Subject.belongsTo(Categories, { foreignKey: 'category_id', as: 'category' }); + // Subject -> Chapter (1:N) Subject.hasMany(Chapter, { foreignKey: 'subject_id', as: 'chapters' }); Chapter.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' }); @@ -277,6 +282,7 @@ module.exports = { // Group 3: Academic Structure AcademicYear, + Categories, Subject, Class, ClassSchedule, diff --git a/quick-fix-bcrypt.bat b/quick-fix-bcrypt.bat deleted file mode 100644 index 242a772..0000000 --- a/quick-fix-bcrypt.bat +++ /dev/null @@ -1,23 +0,0 @@ -@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 diff --git a/quick-fix-bcrypt.sh b/quick-fix-bcrypt.sh deleted file mode 100644 index 673fb8f..0000000 --- a/quick-fix-bcrypt.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/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 diff --git a/rename-img-prompt.js b/rename-img-prompt.js deleted file mode 100644 index 9ab1315..0000000 --- a/rename-img-prompt.js +++ /dev/null @@ -1,27 +0,0 @@ -const {sequelize} = require('./config/database'); - -(async () => { - try { - await sequelize.authenticate(); - console.log('✅ Database connected'); - - // Rename promptForImage to img_prompt - await sequelize.query(` - ALTER TABLE context - CHANGE COLUMN promptForImage img_prompt JSON - COMMENT 'Prompt configuration object' - `); - console.log('✅ Renamed promptForImage to img_prompt'); - - // Show final structure - const [cols] = await sequelize.query('DESCRIBE context'); - console.log('\n📊 Final Context table structure:'); - cols.forEach((c, i) => console.log(` ${i+1}. ${c.Field} (${c.Type})`)); - - process.exit(0); - } catch (error) { - console.error('❌ Error:', error.message); - console.error(error.stack); - process.exit(1); - } -})(); diff --git a/routes/categoryRoutes.js b/routes/categoryRoutes.js new file mode 100644 index 0000000..f034220 --- /dev/null +++ b/routes/categoryRoutes.js @@ -0,0 +1,36 @@ +const express = require('express'); +const router = express.Router(); +const categoryController = require('../controllers/categoryController'); + +/** + * Category Routes + */ + +// GET /api/categories - Get all categories with pagination +router.get('/', categoryController.getAllCategories); + +// GET /api/categories/active - Get all active categories +router.get('/active', categoryController.getActiveCategories); + +// GET /api/categories/datatypes/schema - Get category datatypes +router.get('/datatypes/schema', categoryController.getCategoryDatatypes); + +// GET /api/categories/code/:code - Get category by code +router.get('/code/:code', categoryController.getCategoryByCode); + +// GET /api/categories/:id - Get category by ID +router.get('/:id', categoryController.getCategoryById); + +// GET /api/categories/:id/subjects - Get subjects by category +router.get('/:id/subjects', categoryController.getSubjectsByCategory); + +// POST /api/categories - Create new category +router.post('/', categoryController.createCategory); + +// PUT /api/categories/:id - Update category +router.put('/:id', categoryController.updateCategory); + +// DELETE /api/categories/:id - Delete category +router.delete('/:id', categoryController.deleteCategory); + +module.exports = router;