Files
onion-dmp/scripts/import-excel.js
2026-04-08 14:52:09 +08:00

415 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Excel 数据导入脚本 v2
* 将"家庭教育档案-天数.xlsx"中的完整数据导入到数据库
* 支持多维度标签分类
*
* 用法: node scripts/import-excel.js [path/to/file.xlsx]
*/
const ExcelJS = require('exceljs');
const path = require('path');
const { getDb, initializeDatabase } = require('../db/init');
const EXCEL_FILE = process.argv[2] || path.join(__dirname, '../家庭教育档案-天数.xlsx');
// ────────────────────────────────────
// 标签分类定义
// ────────────────────────────────────
const TAG_CATEGORIES = [
// 1. 监护人信息
{
key: 'guardian_role',
name: '监护人身份',
color: '#3b82f6',
column: 3 // C: 家庭角色
},
{
key: 'guardian_education',
name: '监护人文化程度',
color: '#8b5cf6',
column: 4 // D: 文化程度
},
{
key: 'guardian1_personality',
name: '监护人1性格特征',
color: '#a78bfa',
column: 7 // G: 性格特征
},
{
key: 'guardian2_personality',
name: '监护人2性格特征',
color: '#c084fc',
column: 14 // N: 性格特征_2
},
// 2. 孩子信息
{
key: 'child_gender',
name: '孩子性别',
color: '#ec4899',
column: 17 // Q: 性别
},
{
key: 'child_personality',
name: '孩子性格特征',
color: '#f472b6',
column: 20 // T: 孩子性格特征
},
{
key: 'child_score',
name: '孩子学习成绩',
color: '#f59e0b',
column: 21 // U: 学习成绩
},
// 3. 家庭情况
{
key: 'family_structure',
name: '家庭基本情况',
color: '#06b6d4',
column: 23 // W: 家庭基本情况(含"三代同堂"等)
},
{
key: 'family_atmosphere',
name: '家庭氛围',
color: '#10b981',
column: 24 // X: 家庭氛围
},
{
key: 'parent_child_relation',
name: '亲子关系',
color: '#6366f1',
column: 25 // Y: 亲子关系
},
// 4. 教育行为
{
key: 'education_conflict',
name: '教育理念一致性',
column: 26 // Z: 家长有无教育分歧
},
{
key: 'child_negation',
name: '否定现象',
column: 27 // AA: 是否经常否定孩子
},
{
key: 'physical_punishment',
name: '纪律方式',
column: 28 // AB: 有无打骂教育
},
{
key: 'child_with_parents',
name: '亲子陪伴',
column: 29 // AC: 孩子是否在父母身边长大
},
// 5. 指导周期
{
key: 'duration',
name: '指导周期',
color: '#ef4444',
column: 38 // AL: 天数
}
];
// 标签值映射将Excel值转化为标签
const TAG_VALUE_MAP = {
'guardian_role': {
'母亲': '母亲',
'妈妈': '母亲',
'母': '母亲',
'父亲': '父亲',
'爸爸': '父亲',
'奶奶': '奶奶',
'爷爷': '爷爷',
'外婆': '外婆',
'外公': '外公',
'姥姥': '外婆',
'姥爷': '外公',
'祖母': '奶奶',
'大姐': '成年子女',
'舅舅': '其他亲属',
'妻子': '配偶'
},
'guardian_education': {
'初中': '初中',
'初小': '小学',
'小学': '小学',
'中师': '中专',
'中专': '中专',
'高中': '高中',
'大专': '大专',
'大学': '本科',
'本科': '本科',
'大学本科': '本科',
'硕士': '硕士',
'研究生': '硕士',
'在职研究生': '硕士'
},
'child_gender': {
'女': '女孩',
'男': '男孩',
'女、男': '双胞胎'
},
'child_score': {
'优秀': '优秀',
'良好': '良好',
'一般': '一般',
'差': '较差',
'较差': '较差',
'A': '优秀',
'B': '良好',
'C': '一般',
'D': '较差'
},
'duration': {
'60天': '60天课程',
'180天': '180天课程',
'90天': '90天课程',
'365天': '365天课程'
}
};
// 需要进行关键词提取的字段
const KEYWORD_EXTRACTION_FIELDS = {
'family_structure': {
column: 22,
keywords: ['三代同堂', '四口之家', '三口之家', '单亲', '离异', '隔代抚养', '二代', '三代']
}
};
async function importExcelData() {
try {
console.log(`\n📂 读取 Excel 文件: ${EXCEL_FILE}`);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(EXCEL_FILE);
const worksheet = workbook.getWorksheet(1);
if (!worksheet) {
throw new Error('找不到工作表');
}
console.log(`📊 总行数: ${worksheet.rowCount}`);
const db = getDb('onion');
// 初始化数据库
initializeDatabase('onion');
// 创建所有标签分类
console.log('🏗️ 建立分类体系...');
const categoryMap = {};
for (const cat of TAG_CATEGORIES) {
const result = db.prepare(`
INSERT OR IGNORE INTO tag_categories (key, name, sort_order, color)
VALUES (?, ?, ?, ?)
`).run(cat.key, cat.name, 0, cat.color || '#6366f1');
const catRecord = db.prepare(`
SELECT id FROM tag_categories WHERE key = ?
`).get(cat.key);
categoryMap[cat.key] = catRecord.id;
}
console.log(`✅ 创建了 ${Object.keys(categoryMap).length} 个分类`);
// 处理数据行
let insertedCount = 0;
const insertUserStmt = db.prepare(`
INSERT OR IGNORE INTO users (uid, name, extra_json)
VALUES (?, ?, ?)
`);
const insertUserTagStmt = db.prepare(`
INSERT OR IGNORE INTO user_tags (user_id, tag_id)
VALUES (?, ?)
`);
// 获取事先创建的标签ID映射
const tagCache = {};
function getOrCreateTag(catKey, tagName) {
if (!tagName || !catKey) return null;
const cacheKey = `${catKey}:${tagName}`;
if (tagCache[cacheKey]) return tagCache[cacheKey];
// 生成唯一的key - 对于长文本(性格特征)使用简化版本
let tagKey;
const isPersonality = catKey.includes('personality');
if (isPersonality && tagName.length > 30) {
// 对于长的性格特征,使用简化的标识符
// 使用前20个字符 + 长度id
const simplified = tagName.substring(0, 20).toLowerCase().replace(/\s+/g, '_').replace(/[^\w]/g, '');
const hash = require('crypto').createHash('md5').update(tagName).digest('hex').substring(0, 8);
tagKey = `${catKey}_${simplified}_${hash}`;
} else {
// 对于其他标签,使用原有方法
tagKey = `${catKey}_${tagName.toLowerCase().replace(/\s+/g, '_').replace(/[^\w]/g, '')}`;
}
const stmt = db.prepare(`
SELECT id FROM tags WHERE key = ?
`);
let tag = stmt.get(tagKey);
if (!tag) {
// 创建新标签
db.prepare(`
INSERT INTO tags (key, name, category_id, sort_order)
VALUES (?, ?, ?, ?)
`).run(tagKey, tagName, categoryMap[catKey], 0);
tag = stmt.get(tagKey);
}
tagCache[cacheKey] = tag?.id;
return tag?.id;
}
// 遍历Excel数据行
let rowCount = 0;
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return; // 跳过表头
rowCount++;
const values = row.values || [];
// 提取基本信息
const fileName = values[1]; // 文件名称
const childName = values[16]; // 孩子姓名
if (!fileName) {
console.warn(`⚠️ 行 ${rowNumber} 缺少文件名,跳过`);
return;
}
// 构建用户额外数据
const extraData = {
fileName: fileName,
childName: childName || '',
guardian1Name: values[2],
childAge: values[17],
grade: values[19],
learningScore: values[21],
familyAddress: values[23],
questionnaireSummary: values[37],
};
// 插入用户
const result = insertUserStmt.run(fileName, childName || fileName, JSON.stringify(extraData));
if (result.changes > 0) {
insertedCount++;
const userId = result.lastInsertRowid;
// 为用户添加标签
addUserTags(userId, values, rowNumber, getOrCreateTag, insertUserTagStmt);
if (rowCount % 30 === 0) {
console.log(` 📝 已处理 ${rowCount} 行...`);
}
}
});
console.log(`\n✅ 用户导入完成:${insertedCount}`);
// 更新所有标签的覆盖统计
console.log('🔄 更新标签统计...');
updateTagStats(db);
console.log('\n📊 数据统计:');
const stats = db.prepare(`
SELECT
(SELECT COUNT(*) FROM users) as total_users,
(SELECT COUNT(*) FROM tags) as total_tags,
(SELECT COUNT(*) FROM tag_categories) as total_categories
`).get();
console.log(` • 总用户: ${stats.total_users}`);
console.log(` • 总标签: ${stats.total_tags}`);
console.log(` • 分类数: ${stats.total_categories}`);
db.close();
console.log('\n🎉 导入流程完成!\n');
} catch (error) {
console.error('❌ 导入失败:', error.message);
console.error(error.stack);
process.exit(1);
}
}
function addUserTags(userId, values, rowNumber, getOrCreateTag, insertUserTagStmt) {
for (const cat of TAG_CATEGORIES) {
const colIdx = cat.column;
if (colIdx >= values.length) continue;
let value = values[colIdx];
if (!value) continue;
value = String(value).trim();
// 特殊处理学习成绩的混合值(分解"优秀、良好"为两个标签)
if (cat.key === 'child_score' && value.includes('、')) {
const scores = value.split('、').map(s => s.trim());
for (const score of scores) {
const mapped = TAG_VALUE_MAP[cat.key]?.[score] || score;
const tagId = getOrCreateTag(cat.key, mapped);
if (tagId) {
insertUserTagStmt.run(userId, tagId);
}
}
continue;
}
// 处理值映射
if (TAG_VALUE_MAP[cat.key] && TAG_VALUE_MAP[cat.key][value]) {
value = TAG_VALUE_MAP[cat.key][value];
}
// 获取或创建标签
const tagId = getOrCreateTag(cat.key, value);
if (tagId) {
insertUserTagStmt.run(userId, tagId);
}
// 处理关键词提取
if (KEYWORD_EXTRACTION_FIELDS[cat.key]) {
const keywords = KEYWORD_EXTRACTION_FIELDS[cat.key].keywords;
for (const keyword of keywords) {
if (value.includes(keyword)) {
const kwTagId = getOrCreateTag(cat.key, keyword);
if (kwTagId) {
insertUserTagStmt.run(userId, kwTagId);
}
}
}
}
}
}
function updateTagStats(db) {
const tags = db.prepare(`SELECT id FROM tags`).all();
const totalUsers = db.prepare(`SELECT COUNT(*) as n FROM users`).get().n;
for (const tag of tags) {
const result = db.prepare(`
SELECT COUNT(*) as n FROM user_tags WHERE tag_id = ?
`).get(tag.id);
const coverage = result.n || 0;
const coverageRate = totalUsers > 0 ? (coverage / totalUsers * 100).toFixed(2) : 0;
db.prepare(`
UPDATE tags SET coverage = ?, coverage_rate = ? WHERE id = ?
`).run(coverage, coverageRate, tag.id);
}
}
importExcelData();