316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
const { sequelize } = require('../config/database');
|
||
const UsersAuth = require('../models/UsersAuth');
|
||
const UserProfile = require('../models/UserProfile');
|
||
const StudentDetail = require('../models/StudentDetail');
|
||
const School = require('../models/School');
|
||
const Class = require('../models/Class');
|
||
const bcrypt = require('bcrypt');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
/**
|
||
* Script import học sinh trường Tiểu học Ấp Đình từ apdinh.tsv
|
||
* Chạy: node src/scripts/import-apdinh-students.js
|
||
*/
|
||
|
||
// Hàm hash password
|
||
async function hashPassword(password) {
|
||
const salt = await bcrypt.genSalt(10);
|
||
const hash = await bcrypt.hash(password, salt);
|
||
return { hash, salt };
|
||
}
|
||
|
||
// Hàm chuyển tiếng Việt có dấu thành không dấu
|
||
function removeVietnameseTones(str) {
|
||
str = str.toLowerCase();
|
||
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, 'a');
|
||
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, 'e');
|
||
str = str.replace(/ì|í|ị|ỉ|ĩ/g, 'i');
|
||
str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, 'o');
|
||
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, 'u');
|
||
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, 'y');
|
||
str = str.replace(/đ/g, 'd');
|
||
return str;
|
||
}
|
||
|
||
// Hàm tạo username từ họ tên và ngày sinh
|
||
function generateUsername(fullName, dateOfBirth) {
|
||
// Chuyển tên thành không dấu và viết liền
|
||
const nameParts = fullName.trim().split(/\s+/);
|
||
const nameSlug = nameParts.map(part => removeVietnameseTones(part)).join('');
|
||
|
||
// Parse ngày sinh: 23/06/2019 -> 2306 (chỉ lấy ngày tháng)
|
||
const dateParts = dateOfBirth.split('/');
|
||
const dateSlug = dateParts[0] + dateParts[1]; // Chỉ lấy ngày và tháng
|
||
|
||
// Format: pri_apdinh_[tênkhôngdấu]_[ngàytháng]
|
||
return `pri_apdinh_${nameSlug}_${dateSlug}`.toLowerCase();
|
||
}
|
||
|
||
// Hàm parse ngày sinh từ format dd/mm/yyyy
|
||
function parseDate(dateStr) {
|
||
const [day, month, year] = dateStr.split('/');
|
||
return new Date(year, month - 1, day);
|
||
}
|
||
|
||
// Hàm tạo mật khẩu mặc định từ ngày sinh
|
||
function generateDefaultPassword(dateOfBirth) {
|
||
// Mật khẩu: ddmmyyyy
|
||
return dateOfBirth.replace(/\//g, '');
|
||
}
|
||
|
||
async function importApdinhStudents() {
|
||
try {
|
||
console.log('🚀 Bắt đầu import học sinh Trường Tiểu học Ấp Đình...\n');
|
||
|
||
// 1. Đọc file apdinh.tsv
|
||
const filePath = path.join(__dirname, '../../data/apdinh.tsv');
|
||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||
const lines = fileContent.split('\n').filter(line => line.trim());
|
||
|
||
// Bỏ header
|
||
const dataLines = lines.slice(1);
|
||
console.log(`📚 Tìm thấy ${dataLines.length} học sinh trong file\n`);
|
||
|
||
// Kết nối database
|
||
await sequelize.authenticate();
|
||
console.log('✅ Kết nối database thành công\n');
|
||
|
||
// 2. Tìm trường Tiểu học Ấp Đình
|
||
console.log('🔍 Đang tìm trường Tiểu học Ấp Đình...');
|
||
const school = await School.findOne({
|
||
where: sequelize.where(
|
||
sequelize.fn('LOWER', sequelize.col('school_name')),
|
||
'LIKE',
|
||
'%ấp đình%'
|
||
)
|
||
});
|
||
|
||
if (!school) {
|
||
throw new Error('❌ Không tìm thấy trường Tiểu học Ấp Đình trong database!');
|
||
}
|
||
|
||
console.log(`✅ Tìm thấy trường: ${school.school_name} (${school.school_code})\n`);
|
||
|
||
// 3. Lấy tất cả các lớp của trường Ấp Đình
|
||
console.log('🔍 Đang tải danh sách lớp...');
|
||
const classes = await Class.findAll({
|
||
where: { school_id: school.id }
|
||
});
|
||
|
||
if (classes.length === 0) {
|
||
throw new Error('❌ Không tìm thấy lớp nào của trường Ấp Đình! Vui lòng import classes trước.');
|
||
}
|
||
|
||
console.log(`✅ Tìm thấy ${classes.length} lớp\n`);
|
||
|
||
// Tạo map class_name -> class_id để tra cứu nhanh
|
||
const classMap = {};
|
||
classes.forEach(cls => {
|
||
// Lưu cả class_code và class_name
|
||
classMap[cls.class_code] = cls;
|
||
classMap[cls.class_name] = cls;
|
||
});
|
||
|
||
let importedCount = 0;
|
||
let skippedCount = 0;
|
||
const errors = [];
|
||
const classStats = {};
|
||
|
||
// 4. Parse và import từng học sinh
|
||
for (const line of dataLines) {
|
||
try {
|
||
// Parse TSV: STT\tLớp\tHọ Tên học sinh\tNgày sinh
|
||
const parts = line.split('\t');
|
||
if (parts.length < 4) {
|
||
console.log(`⚠️ Bỏ qua dòng không hợp lệ: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const [stt, className, fullName, dateOfBirth] = parts.map(p => p.trim());
|
||
|
||
// Bỏ qua nếu thiếu thông tin
|
||
if (!fullName || !dateOfBirth || !className) {
|
||
console.log(`⚠️ Bỏ qua dòng thiếu thông tin: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
// Tìm class_id từ className (format: 1_1, 1_2, etc.)
|
||
const classKey = className.replace('_', ''); // 1_1 -> 11
|
||
let classObj = null;
|
||
|
||
// Thử tìm class theo nhiều cách
|
||
for (const cls of classes) {
|
||
if (cls.class_code.includes(className) ||
|
||
cls.class_name.includes(className) ||
|
||
cls.class_code.includes(classKey)) {
|
||
classObj = cls;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!classObj) {
|
||
console.log(`⚠️ Không tìm thấy lớp ${className}, bỏ qua: ${fullName}`);
|
||
errors.push({
|
||
line,
|
||
error: `Không tìm thấy lớp ${className}`
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// Tạo username và password
|
||
const username = generateUsername(fullName, dateOfBirth);
|
||
const password = generateDefaultPassword(dateOfBirth);
|
||
const email = `${username}@apdinh.edu.vn`;
|
||
const studentCode = username.toUpperCase();
|
||
|
||
console.log(`📖 [${className}] Đang xử lý: ${fullName} (${username})`);
|
||
|
||
// Kiểm tra user đã tồn tại
|
||
const existingAuth = await UsersAuth.findOne({
|
||
where: { username }
|
||
});
|
||
|
||
if (existingAuth) {
|
||
console.log(` ⚠️ User đã tồn tại, bỏ qua\n`);
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
// Hash password
|
||
const { hash, salt } = await hashPassword(password);
|
||
|
||
// Parse date of birth
|
||
const dob = parseDate(dateOfBirth);
|
||
|
||
// Transaction để đảm bảo tính toàn vẹn dữ liệu
|
||
await sequelize.transaction(async (t) => {
|
||
// 5a. Tạo UsersAuth
|
||
const userAuth = await UsersAuth.create({
|
||
username,
|
||
email,
|
||
password_hash: hash,
|
||
salt,
|
||
role: 'student',
|
||
is_active: true,
|
||
is_verified: true,
|
||
}, { transaction: t });
|
||
|
||
// 5b. Tạo UserProfile
|
||
const nameParts = fullName.trim().split(/\s+/);
|
||
const lastName = nameParts[0];
|
||
const firstName = nameParts.slice(1).join(' ');
|
||
|
||
const userProfile = await UserProfile.create({
|
||
user_id: userAuth.id,
|
||
full_name: fullName,
|
||
first_name: firstName || fullName,
|
||
last_name: lastName,
|
||
date_of_birth: dob,
|
||
phone: '',
|
||
school_id: school.id,
|
||
address: 'Huyện Hóc Môn',
|
||
city: 'Thành phố Hồ Chí Minh',
|
||
district: 'Huyện Hóc Môn',
|
||
}, { transaction: t });
|
||
|
||
// 5c. Tạo StudentDetail với class_id
|
||
await StudentDetail.create({
|
||
user_id: userAuth.id,
|
||
student_code: studentCode,
|
||
enrollment_date: new Date(),
|
||
current_class_id: classObj.id,
|
||
status: 'active',
|
||
}, { transaction: t });
|
||
|
||
// Cập nhật current_students trong class
|
||
await classObj.increment('current_students', { transaction: t });
|
||
});
|
||
|
||
console.log(` ✅ Tạo thành công (Lớp: ${classObj.class_name}, User: ${username}, Pass: ${password})\n`);
|
||
importedCount++;
|
||
|
||
// Thống kê theo lớp
|
||
if (!classStats[classObj.class_name]) {
|
||
classStats[classObj.class_name] = 0;
|
||
}
|
||
classStats[classObj.class_name]++;
|
||
|
||
} catch (error) {
|
||
console.error(` ❌ Lỗi: ${error.message}\n`);
|
||
errors.push({
|
||
line,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
// Tổng kết
|
||
console.log('\n' + '='.repeat(70));
|
||
console.log('📊 KẾT QUẢ IMPORT TRƯỜNG TIỂU HỌC ẤP ĐÌNH');
|
||
console.log('='.repeat(70));
|
||
console.log(`✅ Học sinh mới: ${importedCount}`);
|
||
console.log(`⚠️ Đã tồn tại: ${skippedCount}`);
|
||
console.log(`❌ Lỗi: ${errors.length}`);
|
||
console.log('='.repeat(70));
|
||
|
||
// Thống kê theo lớp
|
||
console.log('\n📚 THỐNG KÊ THEO LỚP:');
|
||
const sortedClasses = Object.keys(classStats).sort();
|
||
for (const className of sortedClasses) {
|
||
console.log(` ${className}: ${classStats[className]} học sinh`);
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
console.log('\n⚠️ CHI TIẾT LỖI:');
|
||
errors.slice(0, 10).forEach((err, index) => {
|
||
console.log(`\n${index + 1}. ${err.line}`);
|
||
console.log(` Lỗi: ${err.error}`);
|
||
});
|
||
if (errors.length > 10) {
|
||
console.log(`\n ... và ${errors.length - 10} lỗi khác`);
|
||
}
|
||
}
|
||
|
||
// Thống kê tổng hợp - đếm qua UserProfile
|
||
const totalStudents = await UserProfile.count({
|
||
where: { school_id: school.id }
|
||
});
|
||
|
||
console.log('\n📊 THỐNG KÊ TỔNG HỢP:');
|
||
console.log(` Tổng số học sinh trường Ấp Đình: ${totalStudents}`);
|
||
console.log(` Tổng số lớp: ${classes.length}`);
|
||
|
||
console.log('\n✅ Hoàn thành import dữ liệu!\n');
|
||
|
||
// Xuất file credentials để giáo viên có thể tra cứu
|
||
const credentialsPath = path.join(__dirname, '../../data/apdinh-credentials.txt');
|
||
let credentialsContent = '# Thông tin đăng nhập học sinh Trường Tiểu học Ấp Đình\n';
|
||
credentialsContent += '# Format: Lớp | Họ tên | Username | Password\n\n';
|
||
|
||
console.log(`📄 Đang tạo file thông tin đăng nhập: apdinh-credentials.txt\n`);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Lỗi nghiêm trọng:', error);
|
||
throw error;
|
||
} finally {
|
||
await sequelize.close();
|
||
console.log('🔌 Đã đóng kết nối database');
|
||
}
|
||
}
|
||
|
||
// Chạy script
|
||
if (require.main === module) {
|
||
importApdinhStudents()
|
||
.then(() => {
|
||
console.log('\n🎉 Script hoàn thành thành công!');
|
||
process.exit(0);
|
||
})
|
||
.catch((error) => {
|
||
console.error('\n💥 Script thất bại:', error);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = { importApdinhStudents };
|