Update README and project cleanup

This commit is contained in:
inkling
2026-04-08 14:52:09 +08:00
commit fafd267288
71 changed files with 14865 additions and 0 deletions

414
scripts/import-excel.js Normal file
View File

@@ -0,0 +1,414 @@
/**
* 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();