diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..2798136 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,77 @@ +name: Deploy to Production + +on: + push: + branches: + - main + - master + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Deploy to server + run: | + echo "🚀 Deploying to /var/www/services/sena_db_api" + echo "📁 Current directory: $(pwd)" + echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE" + ls -la + + # Create directory if it doesn't exist + mkdir -p /var/www/services/sena_db_api + + # Copy files to destination (runner đã mount /var/www) + rsync -av --delete \ + --exclude 'node_modules' \ + --exclude '.git' \ + --exclude '.gitea' \ + --exclude 'logs' \ + --exclude 'uploads' \ + --exclude 'runner-data' \ + --exclude '.env' \ + --exclude 'docker-compose.runner.yml' \ + --exclude 'setup-runner.sh' \ + $GITHUB_WORKSPACE/ /var/www/services/sena_db_api/ + + echo "✅ Files copied successfully" + + - name: Install dependencies + run: | + cd /var/www/services/sena_db_api + echo "📦 Installing dependencies with pnpm..." + pnpm install --production + echo "✅ Dependencies installed" + + - name: Create required directories + run: | + cd /var/www/services/sena_db_api + mkdir -p logs uploads data + echo "✅ Directories created" + + - name: Restart PM2 service + run: | + echo "🔄 Restarting PM2 service..." + + # Tạo trigger file để PM2 watcher restart service + touch /var/www/services/sena_db_api/.pm2-restart-trigger + + # Đợi watcher xử lý (tối đa 10s) + for i in {1..5}; do + if [ ! -f /var/www/services/sena_db_api/.pm2-restart-trigger ]; then + echo "✅ PM2 service restarted" + exit 0 + fi + sleep 2 + done + + echo "⚠️ Trigger file created, PM2 will restart shortly" + + - name: Deployment completed + run: | + echo "✅ Deployment completed successfully!" + echo "📍 Location: /var/www/services/sena_db_api" + echo "🔧 PM2 service will restart within 2 seconds" \ No newline at end of file diff --git a/PM2_GUIDE.md b/PM2_GUIDE.md new file mode 100644 index 0000000..0562bc1 --- /dev/null +++ b/PM2_GUIDE.md @@ -0,0 +1,211 @@ +# PM2 Process Management + +PM2 script để quản lý tất cả các processes của Sena School Management System. + +## Cài đặt PM2 + +```bash +npm install -g pm2 +``` + +## Processes + +System bao gồm các processes sau: + +1. **sena-api** - Main API Server (2 instances, cluster mode) +2. **worker-db-write** - Database Write Worker (1 instance) +3. **worker-lesson-fill** - Lesson Data Fill Worker (1 instance) +4. **worker-process-data** - Process Data Worker (2 instances) + +## Commands + +### Quản lý tất cả processes + +```bash +# Start tất cả processes +npm run pm2:start + +# Stop tất cả processes +npm run pm2:stop + +# Restart tất cả processes +npm run pm2:restart + +# Delete tất cả processes +npm run pm2:delete + +# Xem status của tất cả processes +npm run pm2:status +``` + +### Quản lý từng nhóm + +```bash +# Chỉ start API server +npm run pm2:start:api + +# Chỉ start workers +npm run pm2:start:workers +``` + +### Logs & Monitoring + +```bash +# Xem tất cả logs +npm run pm2:logs + +# Xem logs của một process cụ thể +pm2 logs sena-api +pm2 logs worker-process-data + +# Xóa tất cả logs +npm run pm2:flush + +# Mở monitor dashboard +npm run pm2:monit +``` + +### Auto-start khi boot + +```bash +# Cấu hình PM2 khởi động cùng hệ thống +npm run pm2:startup + +# Sau đó chạy command mà PM2 hiển thị (với sudo nếu cần) +# Rồi save danh sách processes +npm run pm2:save +``` + +### Commands nâng cao + +```bash +# Restart một process cụ thể +pm2 restart sena-api + +# Stop một process cụ thể +pm2 stop worker-lesson-fill + +# Xem thông tin chi tiết +pm2 describe sena-api + +# Xem metrics +pm2 monit + +# Scale API instances +pm2 scale sena-api 4 +``` + +## File cấu hình + +### ecosystem.config.js + +File cấu hình PM2 chính, định nghĩa: +- Tên processes +- Script path +- Số lượng instances +- Environment variables +- Log files +- Memory limits +- Restart policies + +### Log Files + +Logs được lưu trong thư mục `logs/`: + +``` +logs/ +├── api-error.log +├── api-out.log +├── worker-db-write-error.log +├── worker-db-write-out.log +├── worker-lesson-fill-error.log +├── worker-lesson-fill-out.log +├── worker-process-data-error.log +└── worker-process-data-out.log +``` + +## Production Deployment + +### 1. Install PM2 globally +```bash +npm install -g pm2 +``` + +### 2. Start all processes +```bash +npm run pm2:start +``` + +### 3. Save process list +```bash +npm run pm2:save +``` + +### 4. Configure auto-start +```bash +npm run pm2:startup +# Chạy command được hiển thị +npm run pm2:save +``` + +### 5. Monitor +```bash +npm run pm2:status +npm run pm2:logs +``` + +## Troubleshooting + +### Process không start +```bash +# Xem logs để biết lỗi +pm2 logs + +# Xem thông tin chi tiết +pm2 describe +``` + +### Memory leak +```bash +# Processes tự động restart khi vượt quá giới hạn memory +# API: 1GB +# Workers: 512MB +``` + +### Restart processes sau khi update code +```bash +npm run pm2:restart +``` + +### Xóa và start lại từ đầu +```bash +npm run pm2:delete +npm run pm2:start +``` + +## Environment Variables + +Thay đổi environment trong [ecosystem.config.js](ecosystem.config.js): + +```javascript +env: { + NODE_ENV: 'production', + PORT: 3000, +}, +env_development: { + NODE_ENV: 'development', + PORT: 3000, +} +``` + +Chạy với environment cụ thể: +```bash +pm2 start ecosystem.config.js --env development +pm2 start ecosystem.config.js --env production +``` + +## Help + +```bash +npm run pm2:help +``` diff --git a/config/config.json b/config/config.json index 75faea2..69d4baa 100644 --- a/config/config.json +++ b/config/config.json @@ -31,7 +31,7 @@ "keyPrefix": "sena:" }, "server": { - "port" : 3000, + "port" : 10000, "env": "production" }, "cors": { diff --git a/data/family/g1/unit4.json b/data/family/g1/unit4.json new file mode 100644 index 0000000..0da6624 --- /dev/null +++ b/data/family/g1/unit4.json @@ -0,0 +1,136 @@ +[ + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 1, + "lesson_title": "Lesson 1: Animals", + "lesson_type": "json_content", + "lesson_description": "Learn animal words and use them in a chant", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to learn animal words", + "to use animal words in the form of a chant" + ], + "vocabulary": ["bird", "bear", "hippo", "crocodile", "tiger"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 2, + "lesson_title": "Lesson 2: What are they?", + "lesson_type": "json_content", + "lesson_description": "Ask and answer about animals", + "lesson_content_type": "grammar", + "content_json": { + "type": "grammar", + "learning_objectives": [ + "to ask and answer 'What are they? They're (bears)'", + "to recognize plurals with 's'", + "to sing a song" + ], + "grammar": "What are they? They're bears." + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 3, + "lesson_title": "Lesson 3: Letters J & K", + "lesson_type": "json_content", + "lesson_description": "Recognize letters J and K and their sounds", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to recognize the upper- and lowercase forms of the letters j and k", + "to associate them with the sounds /dʒ/ and /k/", + "to pronounce the sounds /dʒ/ and /k/", + "to be familiar with the names of the letters j and k" + ], + "letters": ["J", "K"], + "sounds": ["/dʒ/", "/k/"], + "vocabulary": ["jug", "juice", "kangaroo", "key"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 4, + "lesson_title": "Lesson 4: Numbers 9 and 10", + "lesson_type": "json_content", + "lesson_description": "Learn and use numbers 9 and 10", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to recognize and say the numbers 9 and 10", + "to use the numbers 9 and 10 in the context of a song" + ], + "vocabulary": ["nine", "ten"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 5, + "lesson_title": "Lesson 5: Letters L & M", + "lesson_type": "json_content", + "lesson_description": "Recognize letters L and M and their sounds", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to recognize the upper- and lowercase forms of the letters l and m", + "to associate them with the sounds /l/ and /m/", + "to pronounce the sounds /l/ and /m/", + "to be familiar with the names of the letters l and m" + ], + "letters": ["L", "M"], + "sounds": ["/l/", "/m/"], + "vocabulary": ["lion", "lollipop", "man", "mango"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 6, + "lesson_title": "Lesson 6: Story and Values", + "lesson_type": "json_content", + "lesson_description": "Understand a short story and learn values", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to recognize and identify words", + "to understand a short story", + "to review and consolidate language introduced in the unit" + ], + "values": ["Be kind to animals"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 7, + "lesson_title": "Culture: Children's Day", + "lesson_type": "json_content", + "lesson_description": "Learn about Children's Day in Japan", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": ["to learn about Children's Day in Japan"], + "vocabulary": ["fish", "bird", "flower", "frog"] + } + }, + { + "chapter_id": "uuid_chapter_4_grade1", + "lesson_number": 8, + "lesson_title": "Consolidation", + "lesson_type": "json_content", + "lesson_description": "Review and test Unit 4", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to review and consolidate language introduced in the unit", + "to do Unit Test" + ] + } + } +] \ No newline at end of file diff --git a/data/family/g1/unit5.json b/data/family/g1/unit5.json new file mode 100644 index 0000000..e924020 --- /dev/null +++ b/data/family/g1/unit5.json @@ -0,0 +1,139 @@ +[ + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 1, + "lesson_title": "Lesson 1: Body Parts", + "lesson_type": "json_content", + "lesson_description": "Identify different parts of the body", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to identify different parts of the body", + "to use the words in the context of a chant" + ], + "vocabulary": ["arms", "nose", "face", "legs", "fingers", "hands"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 2, + "lesson_title": "Lesson 2: This and These", + "lesson_type": "json_content", + "lesson_description": "Use this and these in sentences", + "lesson_content_type": "grammar", + "content_json": { + "type": "grammar", + "learning_objectives": [ + "to say sentences with 'this' and 'these'", + "to complete sentences with 'this' and 'these'", + "to recognize the difference between singular and plural forms of nouns" + ], + "grammar": "This is.... These are...." + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 3, + "lesson_title": "Lesson 3: Letters A, B, C, D", + "lesson_type": "json_content", + "lesson_description": "Learn letters A, B, C, D and their sounds", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to learn the names of the letters a, b, c, d", + "to review the upper- and lowercase forms of a, b, c, d", + "to associate them with their corresponding sounds", + "to pronounce the sounds /b/ and /d/ at the ends of words" + ], + "letters": ["A", "B", "C", "D"], + "sounds": ["/b/", "/d/"], + "vocabulary": ["a", "b", "c", "d", "e", "f", "g", "h"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 4, + "lesson_title": "Lesson 4: Numbers 1-5", + "lesson_type": "json_content", + "lesson_description": "Review numbers 1-5", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to review the numbers 1-5", + "to associate the numbers 1-5 with the words one to five" + ], + "vocabulary": ["one", "two", "three", "four", "five"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 5, + "lesson_title": "Lesson 5: Letters E, F, G, H", + "lesson_type": "json_content", + "lesson_description": "Learn letters E, F, G, H and their sounds", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to learn the names of the letters Ee, Ff, Gg, and Hh", + "to review the upper- and lowercase forms of e, f, g, and h", + "to associate them with their corresponding sounds", + "to pronounce the sounds /f/ and /g/ at the ends of words" + ], + "letters": ["E", "F", "G", "H"], + "sounds": ["/f/", "/g/"], + "vocabulary": ["b", "d", "f", "g"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 6, + "lesson_title": "Lesson 6: Story and Values", + "lesson_type": "json_content", + "lesson_description": "Understand a short story and learn values", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to recognize and identify words", + "to understand a short story", + "to review and consolidate language introduced in the unit" + ], + "values": ["Take care in the sun"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 7, + "lesson_title": "Culture: Fruits in Cambodia", + "lesson_type": "json_content", + "lesson_description": "Learn about fruits found in Cambodia", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to learn some fruit found in Cambodia", + "to design a menu" + ], + "vocabulary": ["mango", "papaya", "pineapple", "watermelon"] + } + }, + { + "chapter_id": "uuid_chapter_5_grade1", + "lesson_number": 8, + "lesson_title": "Consolidation", + "lesson_type": "json_content", + "lesson_description": "Review and test Unit 5", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to review and consolidate language introduced in the unit", + "to do Unit Test" + ] + } + } +] \ No newline at end of file diff --git a/data/family/g1/unit6.json b/data/family/g1/unit6.json new file mode 100644 index 0000000..9b5e0a4 --- /dev/null +++ b/data/family/g1/unit6.json @@ -0,0 +1,140 @@ +[ + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 1, + "lesson_title": "Lesson 1: My Lunchbox", + "lesson_type": "json_content", + "lesson_description": "Identify different foods in a lunchbox", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to identify different foods in a lunchbox", + "to use the words in the context of a chant" + ], + "vocabulary": ["lunchbox", "sandwich", "drink", "banana", "cookie", "pear"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 2, + "lesson_title": "Lesson 2: I have...", + "lesson_type": "json_content", + "lesson_description": "Make sentences with have", + "lesson_content_type": "grammar", + "content_json": { + "type": "grammar", + "learning_objectives": [ + "to make sentences with 'have'", + "to recognize difference between 'a' and 'an'" + ], + "grammar": "I have a pear. I have my lunchbox." + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 3, + "lesson_title": "Lesson 3: Letters I, J, K, L, M", + "lesson_type": "json_content", + "lesson_description": "Learn letters I, J, K, L, M and their sounds", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to learn the names of the letters i, j, k, l, m", + "to review the upper- and lowercase forms of the letters", + "to associate them with their corresponding sounds", + "to pronounce the sounds /ɪ/, /ʤ/, /k/, /l/, /m/ at the beginning of words", + "to pronounce the sounds /k/, /l/, /m/ at the end of words" + ], + "letters": ["I", "J", "K", "L", "M"], + "sounds": ["/ɪ/", "/ʤ/", "/k/", "/l/", "/m/"], + "vocabulary": ["b", "d", "f", "g", "I", "j", "k", "l", "m"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 4, + "lesson_title": "Lesson 4: Numbers 6-10", + "lesson_type": "json_content", + "lesson_description": "Review numbers 6-10", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to review the numbers 6-10", + "to associate the numbers 6-10 with the words six to ten" + ], + "vocabulary": ["six", "seven", "eight", "nine", "ten"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 5, + "lesson_title": "Lesson 5: Review Letters A-M", + "lesson_type": "json_content", + "lesson_description": "Review letters A to M", + "lesson_content_type": "phonics", + "content_json": { + "type": "phonics", + "learning_objectives": [ + "to review the names of the letters learned so far (a to m)", + "to review the upper- and lowercase forms of the letters (a to m)", + "to associate them with their corresponding sounds", + "to recognize letters at the ends/beginnings of words", + "to pronounce their sounds appropriately (/b/, /d/, /f/, /g/, /k/, /l/, /m/)" + ], + "letters": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"], + "sounds": ["/b/", "/d/", "/f/", "/g/", "/k/", "/l/", "/m/"], + "vocabulary": ["b", "d", "f", "g", "k", "l", "m"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 6, + "lesson_title": "Lesson 6: Story and Values", + "lesson_type": "json_content", + "lesson_description": "Understand a short story and learn values", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to recognize and identify words", + "to understand a short story", + "to review and consolidate language introduced in the unit" + ], + "values": ["Share with others"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 7, + "lesson_title": "Culture: Bondi Beach in Australia", + "lesson_type": "json_content", + "lesson_description": "Learn about Bondi Beach in Australia", + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + "learning_objectives": [ + "to learn some words about a beach in Australia", + "to read a postcard" + ], + "vocabulary": ["sand", "sea", "shell", "sun"] + } + }, + { + "chapter_id": "uuid_chapter_6_grade1", + "lesson_number": 8, + "lesson_title": "Consolidation", + "lesson_type": "json_content", + "lesson_description": "Review and test Unit 6", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "learning_objectives": [ + "to review and consolidate language introduced in the unit", + "to do Unit Test" + ] + } + } +] \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..0e5bb31 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,102 @@ +/** + * PM2 Ecosystem Configuration + * Quản lý các process của Sena School Management System + */ + +module.exports = { + apps: [ + // Main API Server + { + name: 'sena-api', + script: './server.js', + instances: 2, // Chạy 2 instances để tận dụng CPU + exec_mode: 'cluster', + env: { + NODE_ENV: 'production', + PORT: 3000, + }, + env_development: { + NODE_ENV: 'development', + PORT: 3000, + }, + error_file: './logs/api-error.log', + out_file: './logs/api-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + merge_logs: true, + max_memory_restart: '1G', + autorestart: true, + watch: false, + max_restarts: 10, + min_uptime: '10s', + }, + + // Database Write Worker + { + name: 'worker-db-write', + script: './workers/databaseWriteWorker.js', + instances: 1, + exec_mode: 'fork', + env: { + NODE_ENV: 'production', + }, + env_development: { + NODE_ENV: 'development', + }, + error_file: './logs/worker-db-write-error.log', + out_file: './logs/worker-db-write-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + merge_logs: true, + max_memory_restart: '512M', + autorestart: true, + watch: false, + max_restarts: 10, + min_uptime: '10s', + }, + + // Lesson Data Fill Worker + { + name: 'worker-lesson-fill', + script: './workers/lessonDataFillWorker.js', + instances: 1, + exec_mode: 'fork', + env: { + NODE_ENV: 'production', + }, + env_development: { + NODE_ENV: 'development', + }, + error_file: './logs/worker-lesson-fill-error.log', + out_file: './logs/worker-lesson-fill-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + merge_logs: true, + max_memory_restart: '512M', + autorestart: true, + watch: false, + max_restarts: 10, + min_uptime: '10s', + }, + + // Process Data Worker + { + name: 'worker-process-data', + script: './workers/processDataWorker.js', + instances: 2, // Chạy 2 instances để xử lý song song + exec_mode: 'fork', + env: { + NODE_ENV: 'production', + }, + env_development: { + NODE_ENV: 'development', + }, + error_file: './logs/worker-process-data-error.log', + out_file: './logs/worker-process-data-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + merge_logs: true, + max_memory_restart: '512M', + autorestart: true, + watch: false, + max_restarts: 10, + min_uptime: '10s', + }, + ], +}; diff --git a/package.json b/package.json index ad9f63e..1ae738e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,20 @@ "start": "node server.js", "dev": "nodemon server.js", "test": "jest --coverage", - "lint": "eslint ." + "lint": "eslint .", + "pm2:start": "node scripts/pm2.js start", + "pm2:stop": "node scripts/pm2.js stop", + "pm2:restart": "node scripts/pm2.js restart", + "pm2:delete": "node scripts/pm2.js delete", + "pm2:status": "node scripts/pm2.js status", + "pm2:logs": "node scripts/pm2.js logs", + "pm2:monit": "node scripts/pm2.js monit", + "pm2:flush": "node scripts/pm2.js flush", + "pm2:save": "node scripts/pm2.js save", + "pm2:startup": "node scripts/pm2.js startup", + "pm2:start:api": "node scripts/pm2.js start:api", + "pm2:start:workers": "node scripts/pm2.js start:workers", + "pm2:help": "node scripts/pm2.js help" }, "keywords": [ "expressjs", diff --git a/routes/vocabRoutes.js b/routes/vocabRoutes.js index 9a2811f..446406c 100644 --- a/routes/vocabRoutes.js +++ b/routes/vocabRoutes.js @@ -158,7 +158,7 @@ router.post('/', authenticateToken, vocabController.createVocab); * 500: * description: Server error */ -router.get('/', authenticateToken, vocabController.getAllVocabs); +router.get('/', vocabController.getAllVocabs); /** * @swagger diff --git a/scripts/pm2.js b/scripts/pm2.js new file mode 100644 index 0000000..6340457 --- /dev/null +++ b/scripts/pm2.js @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +/** + * PM2 Management Script + * Helper script để quản lý các PM2 processes + */ + +const { execSync } = require('child_process'); +const path = require('path'); + +const ECOSYSTEM_FILE = path.join(__dirname, '..', 'ecosystem.config.js'); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + red: '\x1b[31m', +}; + +const log = (message, color = 'reset') => { + console.log(`${colors[color]}${message}${colors.reset}`); +}; + +const exec = (command, options = {}) => { + try { + const result = execSync(command, { + stdio: 'inherit', + ...options, + }); + return result; + } catch (error) { + log(`Error executing command: ${command}`, 'red'); + process.exit(1); + } +}; + +// Commands +const commands = { + start: () => { + log('\n🚀 Starting all processes...', 'blue'); + exec(`pm2 start ${ECOSYSTEM_FILE}`); + log('✅ All processes started!', 'green'); + }, + + stop: () => { + log('\n⏸️ Stopping all processes...', 'yellow'); + exec('pm2 stop ecosystem.config.js'); + log('✅ All processes stopped!', 'green'); + }, + + restart: () => { + log('\n🔄 Restarting all processes...', 'blue'); + exec(`pm2 restart ${ECOSYSTEM_FILE}`); + log('✅ All processes restarted!', 'green'); + }, + + delete: () => { + log('\n🗑️ Deleting all processes...', 'yellow'); + exec('pm2 delete ecosystem.config.js'); + log('✅ All processes deleted!', 'green'); + }, + + status: () => { + log('\n📊 Process status:', 'blue'); + exec('pm2 list'); + }, + + logs: (processName) => { + if (processName) { + log(`\n📋 Showing logs for ${processName}...`, 'blue'); + exec(`pm2 logs ${processName}`); + } else { + log('\n📋 Showing all logs...', 'blue'); + exec('pm2 logs'); + } + }, + + monit: () => { + log('\n📈 Opening process monitor...', 'blue'); + exec('pm2 monit'); + }, + + flush: () => { + log('\n🧹 Flushing all logs...', 'yellow'); + exec('pm2 flush'); + log('✅ Logs flushed!', 'green'); + }, + + save: () => { + log('\n💾 Saving PM2 process list...', 'blue'); + exec('pm2 save'); + log('✅ Process list saved!', 'green'); + }, + + startup: () => { + log('\n⚙️ Configuring PM2 to start on system boot...', 'blue'); + exec('pm2 startup'); + log('✅ Run the command above, then run: npm run pm2:save', 'yellow'); + }, + + 'start:api': () => { + log('\n🚀 Starting API server only...', 'blue'); + exec(`pm2 start ${ECOSYSTEM_FILE} --only sena-api`); + log('✅ API server started!', 'green'); + }, + + 'start:workers': () => { + log('\n🚀 Starting all workers...', 'blue'); + exec(`pm2 start ${ECOSYSTEM_FILE} --only worker-db-write,worker-lesson-fill,worker-process-data`); + log('✅ All workers started!', 'green'); + }, + + help: () => { + console.log(` +╔════════════════════════════════════════════════════════════════╗ +║ Sena PM2 Management Commands ║ +╠════════════════════════════════════════════════════════════════╣ +║ ║ +║ npm run pm2:start - Start all processes ║ +║ npm run pm2:stop - Stop all processes ║ +║ npm run pm2:restart - Restart all processes ║ +║ npm run pm2:delete - Delete all processes ║ +║ npm run pm2:status - Show process status ║ +║ npm run pm2:logs [name] - Show logs (optional: process) ║ +║ npm run pm2:monit - Open process monitor ║ +║ npm run pm2:flush - Flush all logs ║ +║ npm run pm2:save - Save process list ║ +║ npm run pm2:startup - Configure startup script ║ +║ ║ +║ npm run pm2:start:api - Start API server only ║ +║ npm run pm2:start:workers - Start all workers only ║ +║ ║ +╠════════════════════════════════════════════════════════════════╣ +║ Process Names: ║ +║ - sena-api (Main API Server) ║ +║ - worker-db-write (Database Write Worker) ║ +║ - worker-lesson-fill (Lesson Data Fill Worker) ║ +║ - worker-process-data (Process Data Worker) ║ +╚════════════════════════════════════════════════════════════════╝ + `); + }, +}; + +// Parse command +const command = process.argv[2]; +const arg = process.argv[3]; + +if (!command || command === 'help' || command === '--help' || command === '-h') { + commands.help(); + process.exit(0); +} + +if (commands[command]) { + commands[command](arg); +} else { + log(`Unknown command: ${command}`, 'red'); + commands.help(); + process.exit(1); +} diff --git a/scripts/triggerLessonDataFill.js b/scripts/triggerLessonDataFill.js new file mode 100644 index 0000000..44aee52 --- /dev/null +++ b/scripts/triggerLessonDataFill.js @@ -0,0 +1,117 @@ +/** + * Script để trigger Lesson Data Fill Worker + * Chạy script này để bắt đầu xử lý tất cả lessons và tạo process_data tasks + */ + +const { Queue } = require('bullmq'); +const { connectionOptions } = require('../config/bullmq'); + +/** + * Trigger worker để xử lý lessons + */ +async function triggerLessonDataFill(options = {}) { + const { + batchSize = 50, + totalLessons = null, // null = process all + filters = {}, // { lesson_content_type: 'vocabulary', chapter_id: 'xxx' } + } = options; + + const queue = new Queue('lesson-data-fill', { + connection: connectionOptions, + prefix: process.env.BULLMQ_PREFIX || 'vcb', + }); + + try { + let offset = 0; + let jobsCreated = 0; + let hasMore = true; + + console.log('🚀 Starting lesson data fill process...'); + console.log('📋 Config:', { batchSize, totalLessons, filters }); + + while (hasMore) { + // Kiểm tra nếu đã đạt giới hạn + if (totalLessons && offset >= totalLessons) { + console.log(`✅ Reached limit of ${totalLessons} lessons`); + break; + } + + // Tạo job cho batch này + const job = await queue.add( + `process-lessons-batch-${offset}`, + { + batchSize, + offset, + filters, + }, + { + priority: 5, + jobId: `lesson-fill-${offset}-${Date.now()}`, + } + ); + + console.log(`✅ Created job ${job.id} for offset ${offset}`); + jobsCreated++; + + // Chờ một chút để tránh quá tải + await new Promise(resolve => setTimeout(resolve, 100)); + + offset += batchSize; + + // Nếu không có totalLessons, sẽ dừng sau 1 job để kiểm tra hasMore + if (!totalLessons) { + // Worker sẽ báo hasMore trong result + hasMore = false; // Tạm dừng, có thể chạy lại script để tiếp tục + } + } + + console.log(`\n✅ Process completed!`); + console.log(`📊 Total jobs created: ${jobsCreated}`); + console.log(`📊 Lessons range: 0 to ${offset}`); + + await queue.close(); + process.exit(0); + + } catch (error) { + console.error('❌ Error:', error.message); + await queue.close(); + process.exit(1); + } +} + +// Parse command line arguments +const args = process.argv.slice(2); +const options = {}; + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--batch-size' && args[i + 1]) { + options.batchSize = parseInt(args[i + 1]); + i++; + } + + if (arg === '--total' && args[i + 1]) { + options.totalLessons = parseInt(args[i + 1]); + i++; + } + + if (arg === '--type' && args[i + 1]) { + options.filters = options.filters || {}; + options.filters.lesson_content_type = args[i + 1]; + i++; + } + + if (arg === '--chapter' && args[i + 1]) { + options.filters = options.filters || {}; + options.filters.chapter_id = args[i + 1]; + i++; + } +} + +// Run +console.log('═══════════════════════════════════════════════════════'); +console.log(' Lesson Data Fill Trigger Script'); +console.log('═══════════════════════════════════════════════════════\n'); + +triggerLessonDataFill(options); diff --git a/workers/README_LESSON_DATA_FILL.md b/workers/README_LESSON_DATA_FILL.md new file mode 100644 index 0000000..531b67d --- /dev/null +++ b/workers/README_LESSON_DATA_FILL.md @@ -0,0 +1,188 @@ +# Lesson Data Fill Worker + +Worker này tự động xử lý các lesson trong database và tạo task lên BullMQ để xử lý dữ liệu. + +## Chức năng + +Worker sẽ: +1. Lấy các lesson từ database (chỉ lesson đã publish và có content_json) +2. Phân tích content_json để trích xuất: + - Danh sách vocabulary IDs + - Danh sách grammar IDs +3. Tạo task lên BullMQ queue `process-data` với label `process_data` + +## Cài đặt + +Worker sử dụng BullMQ và Redis đã được cấu hình sẵn trong project. + +## Chạy Worker + +### 1. Chạy worker trực tiếp: + +```bash +node workers/lessonDataFillWorker.js +``` + +Worker sẽ chạy và lắng nghe jobs từ queue `lesson-data-fill`. + +### 2. Trigger worker xử lý tất cả lessons: + +```bash +# Xử lý tất cả lessons (mặc định batch size = 50) +node scripts/triggerLessonDataFill.js + +# Xử lý với batch size tùy chỉnh +node scripts/triggerLessonDataFill.js --batch-size 100 + +# Xử lý giới hạn số lượng lessons +node scripts/triggerLessonDataFill.js --total 200 + +# Chỉ xử lý lessons có content_type cụ thể +node scripts/triggerLessonDataFill.js --type vocabulary + +# Chỉ xử lý lessons trong một chapter +node scripts/triggerLessonDataFill.js --chapter + +# Kết hợp các options +node scripts/triggerLessonDataFill.js --batch-size 100 --total 500 --type grammar +``` + +## Cấu trúc dữ liệu + +### Input (Lesson content_json) + +```json +{ + "type": "vocabulary", + "vocabulary_ids": ["uuid1", "uuid2", "uuid3"], + "exercises": [...] +} +``` + +hoặc + +```json +{ + "type": "grammar", + "grammar_ids": ["uuid1", "uuid2"], + "examples": [...], + "exercises": [...] +} +``` + +hoặc review lesson: + +```json +{ + "type": "review", + "sections": [ + { + "type": "vocabulary", + "vocabulary_ids": ["uuid1", "uuid2"] + }, + { + "type": "grammar", + "grammar_ids": ["uuid3"] + } + ] +} +``` + +### Output (Task data gửi lên BullMQ) + +```json +{ + "type": "lesson_data", + "lesson": { + "lesson_id": "uuid", + "lesson_title": "Lesson Title", + "lesson_number": 1, + "lesson_content_type": "vocabulary", + "chapter_id": "uuid", + "chapter_title": "Chapter Title", + "vocabularies": ["vocab-uuid-1", "vocab-uuid-2"], + "grammars": ["grammar-uuid-1"] + }, + "timestamp": "2026-01-27T10:30:00.000Z" +} +``` + +## Queue và Worker Configuration + +- **Input Queue**: `lesson-data-fill` +- **Output Queue**: `process-data` +- **Job Label**: `process_data` +- **Concurrency**: 3 jobs đồng thời +- **Rate Limit**: 10 jobs/second +- **Retry**: 3 lần với exponential backoff + +## Monitoring + +Worker sẽ log các thông tin sau: + +``` +[LessonDataFillWorker] Starting job +[LessonDataFillWorker] Found 50 lessons +[LessonDataFillWorker] Lesson : 10 vocabularies +[LessonDataFillWorker] Lesson : 5 grammars +[LessonDataFillWorker] ✅ Created task: for lesson +[LessonDataFillWorker] ✅ Created 45 process_data tasks +[LessonDataFillWorker] ✅ Job completed +``` + +## Filters + +Worker hỗ trợ các filter sau: + +- `lesson_content_type`: Lọc theo loại nội dung (`vocabulary`, `grammar`, `phonics`, `review`, `mixed`) +- `chapter_id`: Lọc theo chapter cụ thể +- Luôn chỉ lấy lesson đã publish (`is_published = true`) +- Luôn chỉ lấy lesson có content_json (`lesson_type = 'json_content'`) + +## Error Handling + +- Worker sẽ tự động retry 3 lần khi gặp lỗi +- Failed jobs sẽ được giữ lại 7 ngày để review +- Completed jobs sẽ được giữ lại 24 giờ + +## Integration + +Để xử lý các task `process_data` được tạo ra, bạn cần tạo một worker khác để consume queue `process-data`: + +```javascript +const { Worker } = require('bullmq'); +const { connectionOptions } = require('../config/bullmq'); + +const processDataWorker = new Worker( + 'process-data', + async (job) => { + const { lesson } = job.data; + + // Xử lý dữ liệu lesson ở đây + console.log('Processing lesson:', lesson.lesson_id); + console.log('Vocabularies:', lesson.vocabularies); + console.log('Grammars:', lesson.grammars); + + // TODO: Implement your processing logic + }, + { + connection: connectionOptions, + prefix: process.env.BULLMQ_PREFIX || 'vcb', + } +); +``` + +## Graceful Shutdown + +Worker hỗ trợ graceful shutdown khi nhận signal SIGTERM hoặc SIGINT: + +```bash +# Dừng worker an toàn +Ctrl + C +``` + +Worker sẽ: +1. Dừng nhận job mới +2. Hoàn thành các job đang xử lý +3. Đóng kết nối Redis +4. Thoát process diff --git a/workers/lessonDataFillWorker.js b/workers/lessonDataFillWorker.js new file mode 100644 index 0000000..bfcf24c --- /dev/null +++ b/workers/lessonDataFillWorker.js @@ -0,0 +1,282 @@ +const { Worker, Queue } = require('bullmq'); +const { connectionOptions } = require('../config/bullmq'); +const models = require('../models'); +const { Op } = require('sequelize'); + +/** + * Lesson Data Fill Worker + * Worker này lấy các lesson từ database và tạo task xử lý dữ liệu lên BullMQ + * - Liệt kê các từ vựng hoặc ngữ pháp của lesson + * - Ghi thành task lên BullMQ với label 'process_data' + */ +class LessonDataFillWorker { + constructor() { + // Queue để ghi task process_data + this.processDataQueue = new Queue('process-data', { + connection: connectionOptions, + prefix: process.env.BULLMQ_PREFIX || 'vcb', + defaultJobOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 2000, + }, + removeOnComplete: { + age: 24 * 3600, + count: 1000, + }, + removeOnFail: { + age: 7 * 24 * 3600, + }, + }, + }); + + // Worker để xử lý các lesson + this.worker = new Worker( + 'lesson-data-fill', + async (job) => await this.processJob(job), + { + connection: connectionOptions, + prefix: process.env.BULLMQ_PREFIX || 'vcb', + concurrency: 3, + limiter: { + max: 10, + duration: 1000, + }, + } + ); + + this.setupEventHandlers(); + } + + /** + * Process job - Lấy lessons và tạo task xử lý + */ + async processJob(job) { + console.log(`[LessonDataFillWorker] Starting job ${job.id}`); + + try { + const { batchSize = 50, offset = 0, filters = {} } = job.data; + + // Lấy lessons từ database + const lessons = await this.getLessons(batchSize, offset, filters); + + console.log(`[LessonDataFillWorker] Found ${lessons.length} lessons`); + + // Xử lý từng lesson + const tasks = []; + for (const lesson of lessons) { + const taskData = await this.extractLessonData(lesson); + + if (taskData) { + // Tạo task lên BullMQ + const task = await this.createProcessDataTask(taskData); + tasks.push(task); + } + } + + console.log(`[LessonDataFillWorker] ✅ Created ${tasks.length} process_data tasks`); + + return { + success: true, + lessonsProcessed: lessons.length, + tasksCreated: tasks.length, + offset, + hasMore: lessons.length === batchSize, + }; + + } catch (error) { + console.error(`[LessonDataFillWorker] ❌ Error:`, error.message); + throw error; + } + } + + /** + * Lấy lessons từ database + */ + async getLessons(limit, offset, filters) { + const whereClause = { + is_published: true, + lesson_type: 'json_content', + content_json: { + [Op.ne]: null, + }, + }; + + // Thêm filters nếu có + if (filters.lesson_content_type) { + whereClause.lesson_content_type = filters.lesson_content_type; + } + if (filters.chapter_id) { + whereClause.chapter_id = filters.chapter_id; + } + + const lessons = await models.Lesson.findAll({ + where: whereClause, + limit, + offset, + order: [['created_at', 'DESC']], + include: [ + { + model: models.Chapter, + as: 'chapter', + attributes: ['id', 'chapter_number', 'chapter_title', 'subject_id'], + }, + ], + }); + + return lessons; + } + + /** + * Trích xuất dữ liệu từ lesson + */ + async extractLessonData(lesson) { + try { + const contentJson = lesson.content_json; + + if (!contentJson || typeof contentJson !== 'object') { + return null; + } + + const lessonData = { + lesson_id: lesson.id, + lesson_title: lesson.lesson_title, + lesson_number: lesson.lesson_number, + lesson_content_type: lesson.lesson_content_type, + chapter_id: lesson.chapter_id, + chapter_title: lesson.chapter?.chapter_title, + vocabularies: [], + grammars: [], + }; + + // Trích xuất vocabulary IDs + if (contentJson.vocabulary_ids && Array.isArray(contentJson.vocabulary_ids)) { + lessonData.vocabularies = contentJson.vocabulary_ids; + console.log(`[LessonDataFillWorker] Lesson ${lesson.id}: ${lessonData.vocabularies.length} vocabularies`); + } + + // Trích xuất grammar IDs + if (contentJson.grammar_ids && Array.isArray(contentJson.grammar_ids)) { + lessonData.grammars = contentJson.grammar_ids; + console.log(`[LessonDataFillWorker] Lesson ${lesson.id}: ${lessonData.grammars.length} grammars`); + } + + // Xử lý review lessons (có thể chứa cả vocab và grammar) + if (contentJson.sections && Array.isArray(contentJson.sections)) { + for (const section of contentJson.sections) { + if (section.vocabulary_ids && Array.isArray(section.vocabulary_ids)) { + lessonData.vocabularies.push(...section.vocabulary_ids); + } + if (section.grammar_ids && Array.isArray(section.grammar_ids)) { + lessonData.grammars.push(...section.grammar_ids); + } + } + + // Loại bỏ duplicate IDs + lessonData.vocabularies = [...new Set(lessonData.vocabularies)]; + lessonData.grammars = [...new Set(lessonData.grammars)]; + } + + // Chỉ return nếu có vocabulary hoặc grammar + if (lessonData.vocabularies.length > 0 || lessonData.grammars.length > 0) { + return lessonData; + } + + return null; + + } catch (error) { + console.error(`[LessonDataFillWorker] Error extracting lesson data:`, error.message); + return null; + } + } + + /** + * Tạo task process_data lên BullMQ + */ + async createProcessDataTask(lessonData) { + try { + const job = await this.processDataQueue.add( + 'process_data', + { + type: 'lesson_data', + lesson: lessonData, + timestamp: new Date().toISOString(), + }, + { + priority: 5, + jobId: `lesson-${lessonData.lesson_id}-${Date.now()}`, + } + ); + + console.log(`[LessonDataFillWorker] ✅ Created task: ${job.id} for lesson ${lessonData.lesson_id}`); + + return { + jobId: job.id, + lessonId: lessonData.lesson_id, + }; + + } catch (error) { + console.error(`[LessonDataFillWorker] ❌ Error creating task:`, error.message); + throw error; + } + } + + /** + * Setup event handlers + */ + setupEventHandlers() { + this.worker.on('completed', (job) => { + console.log(`[LessonDataFillWorker] ✅ Job ${job.id} completed`); + }); + + this.worker.on('failed', (job, err) => { + console.error(`[LessonDataFillWorker] ❌ Job ${job?.id} failed:`, err.message); + }); + + this.worker.on('error', (err) => { + console.error(`[LessonDataFillWorker] ❌ Worker error:`, err.message); + }); + + this.worker.on('ready', () => { + console.log('[LessonDataFillWorker] 🚀 Worker ready'); + }); + + this.processDataQueue.on('error', (err) => { + console.error(`[ProcessDataQueue] ❌ Queue error:`, err.message); + }); + } + + /** + * Graceful shutdown + */ + async close() { + console.log('[LessonDataFillWorker] Closing worker...'); + await this.worker.close(); + await this.processDataQueue.close(); + console.log('[LessonDataFillWorker] Worker closed'); + } +} + +// Export class và instance để sử dụng +module.exports = LessonDataFillWorker; + +// Nếu chạy trực tiếp file này +if (require.main === module) { + const worker = new LessonDataFillWorker(); + + // Graceful shutdown + process.on('SIGTERM', async () => { + console.log('SIGTERM received, closing worker...'); + await worker.close(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + console.log('SIGINT received, closing worker...'); + await worker.close(); + process.exit(0); + }); + + console.log('🚀 Lesson Data Fill Worker started'); +} diff --git a/workers/processDataWorker.js b/workers/processDataWorker.js new file mode 100644 index 0000000..1457a62 --- /dev/null +++ b/workers/processDataWorker.js @@ -0,0 +1,258 @@ +const { Worker } = require('bullmq'); +const { connectionOptions } = require('../config/bullmq'); +const models = require('../models'); + +/** + * Process Data Worker + * Worker này xử lý các task từ queue 'process-data' + * - Nhận lesson data với danh sách vocabulary và grammar IDs + * - Thực hiện các xử lý nghiệp vụ với dữ liệu + */ +class ProcessDataWorker { + constructor() { + this.worker = new Worker( + 'process-data', + async (job) => await this.processJob(job), + { + connection: connectionOptions, + prefix: process.env.BULLMQ_PREFIX || 'vcb', + concurrency: 5, // Process 5 jobs đồng thời + limiter: { + max: 20, + duration: 1000, // 20 jobs/second + }, + } + ); + + this.setupEventHandlers(); + } + + /** + * Process job - Xử lý task process_data + */ + async processJob(job) { + const { type, lesson, timestamp } = job.data; + + console.log(`[ProcessDataWorker] Processing job ${job.id} - Type: ${type}`); + console.log(`[ProcessDataWorker] Lesson: ${lesson.lesson_title} (${lesson.lesson_id})`); + + try { + if (type === 'lesson_data') { + const result = await this.processLessonData(lesson); + + console.log(`[ProcessDataWorker] ✅ Completed job ${job.id}`); + return result; + } else { + console.log(`[ProcessDataWorker] ⚠️ Unknown type: ${type}`); + return { success: false, error: 'Unknown type' }; + } + + } catch (error) { + console.error(`[ProcessDataWorker] ❌ Error processing job ${job.id}:`, error.message); + throw error; + } + } + + /** + * Xử lý lesson data + */ + async processLessonData(lesson) { + const { + lesson_id, + lesson_title, + vocabularies, + grammars, + chapter_id, + } = lesson; + + console.log(`[ProcessDataWorker] Processing lesson: ${lesson_title}`); + console.log(`[ProcessDataWorker] - Vocabularies: ${vocabularies.length}`); + console.log(`[ProcessDataWorker] - Grammars: ${grammars.length}`); + + const result = { + lesson_id, + processed: { + vocabularies: 0, + grammars: 0, + }, + details: { + vocabularies: [], + grammars: [], + }, + }; + + // Xử lý vocabularies + if (vocabularies.length > 0) { + const vocabResult = await this.processVocabularies(vocabularies, lesson_id); + result.processed.vocabularies = vocabResult.count; + result.details.vocabularies = vocabResult.items; + } + + // Xử lý grammars + if (grammars.length > 0) { + const grammarResult = await this.processGrammars(grammars, lesson_id); + result.processed.grammars = grammarResult.count; + result.details.grammars = grammarResult.items; + } + + console.log(`[ProcessDataWorker] ✅ Processed: ${result.processed.vocabularies} vocabs, ${result.processed.grammars} grammars`); + + return result; + } + + /** + * Xử lý vocabularies + * TODO: Implement logic xử lý vocabularies theo yêu cầu nghiệp vụ + */ + async processVocabularies(vocabularyIds, lessonId) { + const items = []; + + // Lấy thông tin vocabularies từ database + const vocabs = await models.Vocab.findAll({ + where: { + vocab_id: vocabularyIds, + }, + include: [ + { + model: models.VocabMapping, + as: 'mappings', + }, + { + model: models.VocabForm, + as: 'forms', + }, + ], + }); + + for (const vocab of vocabs) { + console.log(`[ProcessDataWorker] - Vocab: ${vocab.word} (${vocab.vocab_id})`); + + // TODO: Thực hiện xử lý nghiệp vụ ở đây + // Ví dụ: + // - Cập nhật thống kê + // - Tạo cache + // - Sync với hệ thống khác + // - Tạo notification + // - Log audit + + items.push({ + vocab_id: vocab.vocab_id, + word: vocab.word, + word_type: vocab.word_type, + mappings_count: vocab.mappings?.length || 0, + forms_count: vocab.forms?.length || 0, + }); + } + + return { + count: vocabs.length, + items, + }; + } + + /** + * Xử lý grammars + * TODO: Implement logic xử lý grammars theo yêu cầu nghiệp vụ + */ + async processGrammars(grammarIds, lessonId) { + const items = []; + + // Lấy thông tin grammars từ database + const grammars = await models.Grammar.findAll({ + where: { + id: grammarIds, + }, + include: [ + { + model: models.GrammarMapping, + as: 'mappings', + }, + ], + }); + + for (const grammar of grammars) { + console.log(`[ProcessDataWorker] - Grammar: ${grammar.title} (${grammar.id})`); + + // TODO: Thực hiện xử lý nghiệp vụ ở đây + // Ví dụ: + // - Cập nhật thống kê + // - Tạo cache + // - Sync với hệ thống khác + // - Tạo notification + // - Log audit + + items.push({ + grammar_id: grammar.id, + title: grammar.title, + grammar_type: grammar.grammar_type, + mappings_count: grammar.mappings?.length || 0, + }); + } + + return { + count: grammars.length, + items, + }; + } + + /** + * Setup event handlers + */ + setupEventHandlers() { + this.worker.on('completed', (job, returnvalue) => { + console.log(`[ProcessDataWorker] ✅ Job ${job.id} completed`); + }); + + this.worker.on('failed', (job, err) => { + console.error(`[ProcessDataWorker] ❌ Job ${job?.id} failed:`, err.message); + }); + + this.worker.on('error', (err) => { + console.error(`[ProcessDataWorker] ❌ Worker error:`, err.message); + }); + + this.worker.on('ready', () => { + console.log('[ProcessDataWorker] 🚀 Worker ready and listening for jobs...'); + }); + + this.worker.on('active', (job) => { + console.log(`[ProcessDataWorker] 🔄 Processing job ${job.id}...`); + }); + } + + /** + * Graceful shutdown + */ + async close() { + console.log('[ProcessDataWorker] Closing worker...'); + await this.worker.close(); + console.log('[ProcessDataWorker] Worker closed'); + } +} + +// Export class +module.exports = ProcessDataWorker; + +// Nếu chạy trực tiếp file này +if (require.main === module) { + const worker = new ProcessDataWorker(); + + // Graceful shutdown + process.on('SIGTERM', async () => { + console.log('SIGTERM received, closing worker...'); + await worker.close(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + console.log('SIGINT received, closing worker...'); + await worker.close(); + process.exit(0); + }); + + console.log('═══════════════════════════════════════════════════════'); + console.log(' Process Data Worker'); + console.log(' Queue: process-data'); + console.log(' Concurrency: 5'); + console.log('═══════════════════════════════════════════════════════\n'); +}