Files
sena_db_api_layer/scripts/import-apdinh-students.js
2026-01-19 09:33:35 +07:00

316 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };