This commit is contained in:
Ken
2026-01-19 09:33:35 +07:00
parent 374dc12b2d
commit 70838a4bc1
103 changed files with 16929 additions and 2 deletions

View File

@@ -0,0 +1,315 @@
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 };