feat: 添加第12课课评、班级总结及学生画像更新

- K4周日1900班第12课《花朵随心画》课评(梁境城、钟嘉逸、王睿意补课)
- AICODE03/CSP03各班级第12课课评及班级总结
- 更新多班级学生画像
- 课评生成技能优化
This commit is contained in:
chengzi
2026-05-24 20:48:34 +08:00
parent 682bc4e93a
commit 3b75170862
515 changed files with 66389 additions and 1424 deletions

View File

@@ -0,0 +1,486 @@
/**
* API出勤记录与本地课评交叉核对脚本
* 严格遵循《课评写入和查找规则V1.0》的两级查找逻辑
*
* 用法: node check-api-vs-local.js <开始日期> <结束日期>
* 示例: node check-api-vs-local.js 2026-05-15 2026-05-17
*/
const axios = require('axios');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
const API_BASE_URL = process.env.API_BASE_URL;
const AUTHORIZATION = process.env.AUTHORIZATION;
const MEMORY_DIR = path.join(__dirname, '..', '.claude', 'memory', 'class');
// ============ 课程编号别名映射 ============
// API课程编号 → 本地目录名可能的大小写变体
const DIR_CODE_ALIASES = {
'KITTEN04': ['Kitten04', 'KITTEN04', 'kitten04'],
'KITTEN': ['Kitten', 'KITTEN', 'kitten'],
'AICODE03': ['AICODE03'],
'AICODE01': ['AICODE01'],
'CSP03': ['CSP03'],
'CSP05': ['CSP05'],
};
// API课程编号 → 文件名中可能的别名
const FILE_CODE_ALIASES = {
'KITTEN04': ['KITTEN04', 'Kitten04', 'kitten04', 'K4', 'k4'],
'KITTEN': ['KITTEN', 'Kitten', 'kitten', 'K4', 'k4'],
'AICODE03': ['AICODE03'],
'AICODE01': ['AICODE01'],
'CSP03': ['CSP03'],
'CSP05': ['CSP05'],
};
// 非常规班目录关键词
const IRREGULAR_KEYWORDS = ['集训', '体验', '临时', '测试'];
// ============ 辅助函数 ============
function extractCourseCode(className) {
const match = className.match(/^([A-Z]+\d+)/);
return match ? match[1] : className;
}
function getWeekday(dateStr) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const d = new Date(dateStr + 'T00:00:00');
return weekdays[d.getDay()];
}
function extractTimeCode(timePeriod) {
if (!timePeriod) return '';
const match = timePeriod.match(/(\d{2}):(\d{2})/);
return match ? match[1] + match[2] : '';
}
function getFileAliases(courseCode) {
return FILE_CODE_ALIASES[courseCode] || [courseCode];
}
function getDirAliases(courseCode) {
return DIR_CODE_ALIASES[courseCode] || [courseCode];
}
// 列出所有本地班级目录
function listAllClassDirs() {
if (!fs.existsSync(MEMORY_DIR)) return [];
return fs.readdirSync(MEMORY_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
}
// 判断是否非常规班目录
function isIrregularDir(dirName) {
return IRREGULAR_KEYWORDS.some(k => dirName.includes(k));
}
// ============ 两级查找逻辑 ============
/**
* 第1级精确匹配课程编号 + 老师 + 星期 + 时间)
*/
function findLevel1Dir(courseCode, weekday, timeCode) {
const dirs = listAllClassDirs();
const aliases = getDirAliases(courseCode);
for (const code of aliases) {
const pattern = new RegExp(`${code}.*橙子.*${weekday}.*${timeCode}`, 'i');
const match = dirs.find(d => pattern.test(d));
if (match) return match;
}
// 模糊回退
for (const code of aliases) {
const match = dirs.find(d =>
d.toLowerCase().includes(code.toLowerCase()) &&
d.includes(weekday) &&
(timeCode === '' || d.includes(timeCode))
);
if (match) return match;
}
return null;
}
/**
* 第2级放宽时间课程编号 + 老师排除非常规班和第1级已匹配的目录
*/
function findLevel2Dirs(courseCode, excludeDir) {
const dirs = listAllClassDirs();
const aliases = getDirAliases(courseCode);
return dirs.filter(dir => {
if (dir === excludeDir) return false;
if (isIrregularDir(dir)) return false;
if (!dir.includes('橙子')) return false;
return aliases.some(code => dir.toLowerCase().includes(code.toLowerCase()));
});
}
/**
* 在指定目录下检查学生课评文件
* 返回: { hasFeedback, files, fileTypes, note }
*/
function checkStudentFeedback(baseDir, studentName, dateNum, aliases) {
const feedbackDir = path.join(baseDir, studentName, 'feedback');
if (!fs.existsSync(feedbackDir)) {
return { hasFeedback: false, files: [], fileTypes: [], note: '无feedback目录' };
}
const files = fs.readdirSync(feedbackDir)
.filter(f => f.endsWith('.md'))
.filter(f => {
const dateMatch = f.startsWith(dateNum);
const codeMatch = aliases.some(a => f.includes(a));
return dateMatch && codeMatch;
});
const fileTypes = files.map(f => {
if (f.includes('(请假)')) return '请假';
if (f.includes('(补课')) return '补课';
if (f.includes('(未到')) return '未到';
return '正常课评';
});
return {
hasFeedback: files.length > 0,
files,
fileTypes,
note: files.length > 0 ? fileTypes.join(', ') : '无匹配文件'
};
}
/**
* 检查学生目录是否存在(用于判断是"新增"还是"有目录但无文件"
*/
function hasStudentDir(baseDir, studentName) {
return fs.existsSync(path.join(baseDir, studentName));
}
// ============ API查询 ============
async function queryDailySchedule(date) {
try {
const response = await axios.get(`${API_BASE_URL}/reports/teaching-schedule`, {
headers: { 'Authorization': AUTHORIZATION },
params: {
teacher_name: '橙子(程城)',
teaching_date: date
},
timeout: 15000
});
if (response.data.code === 0 && response.data.data.items.length > 0) {
return response.data.data.items;
}
return [];
} catch (error) {
console.error(`查询 ${date} 失败:`, error.response?.data?.message || error.message);
return [];
}
}
// ============ 主函数 ============
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('用法: node check-api-vs-local.js <开始日期> <结束日期>');
console.log('示例: node check-api-vs-local.js 2026-05-15 2026-05-17');
process.exit(1);
}
const beginDate = args[0];
const endDate = args[1];
console.log('========================================');
console.log(' API出勤记录 vs 本地课评 交叉核对');
console.log(` 日期范围: ${beginDate}${endDate}`);
console.log(' 查找规则: V1.0 两级查找(精确 → 放宽时间)');
console.log('========================================\n');
// 生成日期列表(本地时区)
const dates = [];
let curr = new Date(beginDate + 'T00:00:00');
const end = new Date(endDate + 'T00:00:00');
while (curr <= end) {
const y = curr.getFullYear();
const m = String(curr.getMonth() + 1).padStart(2, '0');
const d = String(curr.getDate()).padStart(2, '0');
dates.push(`${y}-${m}-${d}`);
curr.setDate(curr.getDate() + 1);
}
const allResults = [];
for (const date of dates) {
console.log(`\n[${date} ${getWeekday(date)}]`);
const courses = await queryDailySchedule(date);
if (courses.length === 0) {
console.log(' 无课程安排');
continue;
}
for (const course of courses) {
const courseCode = extractCourseCode(course.class_name);
const weekday = getWeekday(date);
const timeCode = extractTimeCode(course.teaching_time_period);
const dateNum = date.replace(/-/g, '');
const fileAliases = getFileAliases(courseCode);
console.log(`\n 课程: ${course.class_name}`);
console.log(` 时间: ${course.teaching_time_period}`);
console.log(` API应到: ${course.student_count}人 (${course.student_names?.join(', ') || '无名单'})`);
// ====== 两级查找:确定搜索目录范围 ======
const level1Dir = findLevel1Dir(courseCode, weekday, timeCode);
const level2Dirs = findLevel2Dirs(courseCode, level1Dir);
const searchDirs = [];
if (level1Dir) searchDirs.push({ dir: level1Dir, level: 1 });
for (const d of level2Dirs) searchDirs.push({ dir: d, level: 2 });
if (searchDirs.length === 0) {
console.log(` 本地目录: ❌ 未找到任何匹配目录 (第1/2级均失败)`);
allResults.push({
date, courseCode, time: course.teaching_time_period,
className: course.class_name, level1Dir: null, level2Dirs: [], searchDirs: [],
apiStudents: course.student_names || [], apiCount: course.student_count,
studentResults: [], feedbackCount: 0, issues: ['本地无匹配班级目录']
});
continue;
}
console.log(` 第1级目录: ${level1Dir || '无'}`);
if (level2Dirs.length > 0) console.log(` 第2级目录: ${level2Dirs.join(', ')}`);
// ====== 以学生为中心查找课评 ======
const apiNames = (course.student_names || []).map(n => n.replace(/\(.*?\)/g, '').trim());
const studentResults = [];
for (const apiName of apiNames) {
let found = false;
let foundAt = null;
for (const { dir, level } of searchDirs) {
// 1a/2a: 主目录 S/feedback/
const mainResult = checkStudentFeedback(
path.join(MEMORY_DIR, dir), apiName, dateNum, fileAliases
);
if (mainResult.hasFeedback) {
found = true;
foundAt = { dir, level, type: '主目录', ...mainResult };
break;
}
// 1b/2b: 补课目录 补课/S/feedback/
const makeupResult = checkStudentFeedback(
path.join(MEMORY_DIR, dir, '补课'), apiName, dateNum, fileAliases
);
if (makeupResult.hasFeedback) {
found = true;
foundAt = { dir, level, type: '补课目录', ...makeupResult };
break;
}
}
studentResults.push({ name: apiName, found, foundAt });
}
// ====== 比对:收集问题 ======
const issues = [];
const foundStudents = studentResults.filter(s => s.found);
const missingStudents = studentResults.filter(s => !s.found);
const foundInLevel2 = foundStudents.filter(s => s.foundAt.level === 2);
// 1. 未找到课评的学生
for (const s of missingStudents) {
// 检查在哪些目录中有学生目录(但无课评文件)
const dirsWithStudent = searchDirs.filter(({ dir }) =>
hasStudentDir(path.join(MEMORY_DIR, dir), s.name)
);
if (dirsWithStudent.length > 0) {
const dirNames = dirsWithStudent.map(d => d.dir).join(', ');
issues.push(`${s.name}: 有学生目录但无课评文件 (在: ${dirNames})`);
} else {
issues.push(`${s.name}: 本地无学生目录(可能新增/转班)`);
}
}
// 2. 在第2级目录中找到的学生提示非错误
for (const s of foundInLevel2) {
issues.push(`${s.name}: 课评在「${s.foundAt.dir}」找到(${s.foundAt.type}),建议确认是否为补课/调时间`);
}
// 3. 检查课评文件日期(对已找到的学生)
for (const s of foundStudents) {
for (const f of s.foundAt.files) {
const fileDate = f.substring(0, 8);
if (fileDate !== dateNum) {
issues.push(`${s.name}: 文件日期不匹配 ${f} (文件${fileDate} vs API${dateNum})`);
}
}
}
// 4. 检查课程编号(对已找到的学生)
for (const s of foundStudents) {
for (const f of s.foundAt.files) {
const codeMatch = fileAliases.some(a => f.includes(a));
if (!codeMatch) {
issues.push(`${s.name}: 课程编号不匹配 ${f} (期望含${courseCode}或其别名)`);
}
}
}
// ====== 输出结果 ======
const feedbackCount = foundStudents.length;
if (issues.length === 0) {
console.log(` ✅ 核对通过 (${feedbackCount}/${apiNames.length}人有课评)`);
} else {
const realIssues = issues.filter(i => !i.includes('建议确认')); // 第2级找到是提示不是错误
const hints = issues.filter(i => i.includes('建议确认'));
if (realIssues.length > 0) {
console.log(` ⚠️ 发现 ${realIssues.length} 个问题:`);
realIssues.forEach(issue => console.log(` - ${issue}`));
}
if (hints.length > 0) {
console.log(` 💡 ${hints.length} 个提示:`);
hints.forEach(h => console.log(` - ${h}`));
}
}
allResults.push({
date, courseCode, time: course.teaching_time_period,
className: course.class_name,
level1Dir, level2Dirs, searchDirs,
apiStudents: course.student_names || [], apiCount: course.student_count,
studentResults, feedbackCount,
issues
});
}
}
// ============ 汇总报告 ============
console.log('\n\n========================================');
console.log(' 核对汇总报告');
console.log('========================================');
const totalCourses = allResults.length;
const okCourses = allResults.filter(r =>
r.issues.filter(i => !i.includes('建议确认')).length === 0
).length;
const issueCourses = allResults.filter(r =>
r.issues.filter(i => !i.includes('建议确认')).length > 0
);
const missingDirCourses = allResults.filter(r => r.searchDirs && r.searchDirs.length === 0);
const level2Hints = allResults.filter(r =>
r.issues.some(i => i.includes('建议确认'))
).length;
console.log(`\n总课程数: ${totalCourses}`);
console.log(`核对通过: ${okCourses}`);
console.log(`存在问题: ${issueCourses.length}`);
console.log(`无本地目录: ${missingDirCourses.length}`);
console.log(`有第2级提示: ${level2Hints}`);
if (issueCourses.length > 0) {
console.log('\n--- 问题详情 ---');
issueCourses.forEach(r => {
const realIssues = r.issues.filter(i => !i.includes('建议确认'));
console.log(`\n[${r.date} ${r.time}] ${r.className}`);
console.log(` 第1级: ${r.level1Dir || '无'}, 第2级: ${r.level2Dirs.join(', ') || '无'}`);
realIssues.forEach(issue => console.log(` ⚠️ ${issue}`));
});
}
// ============ 保存报告 ============
const reportPath = path.join(__dirname, '..', 'output', `api核对报告_${beginDate}_${endDate}.md`);
const reportDir = path.dirname(reportPath);
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
let md = `# API出勤记录 vs 本地课评核对报告\n\n`;
md += `- **日期范围**: ${beginDate}${endDate}\n`;
md += `- **核对时间**: ${new Date().toLocaleString()}\n`;
md += `- **老师**: 橙子(程城)\n`;
md += `- **查找规则**: V1.0 两级查找(精确→放宽时间)\n\n`;
md += `## 汇总\n\n`;
md += `- 总课程数: ${totalCourses}\n`;
md += `- 核对通过: ${okCourses}\n`;
md += `- 存在问题: ${issueCourses.length}\n`;
md += `- 无本地目录: ${missingDirCourses.length}\n`;
md += `- 有第2级查找提示: ${level2Hints}\n\n`;
md += `## 详细核对结果\n\n`;
md += `| 日期 | 时间 | 课程 | 第1级目录 | 第2级目录 | API人数 | 找到课评 | 状态 |\n`;
md += `|------|------|------|-----------|-----------|---------|----------|------|\n`;
for (const r of allResults) {
const hasRealIssues = r.issues.filter(i => !i.includes('建议确认')).length > 0;
const hasHints = r.issues.some(i => i.includes('建议确认'));
let status = '✅';
if (r.searchDirs.length === 0) status = '❌';
else if (hasRealIssues) status = '⚠️';
else if (hasHints) status = '💡';
md += `| ${r.date} | ${r.time} | ${r.className} | ${r.level1Dir || '-'} | ${r.level2Dirs.join(', ') || '-'} | ${r.apiCount} | ${r.feedbackCount || 0} | ${status} |\n`;
}
if (issueCourses.length > 0) {
md += `\n## 问题详情\n\n`;
for (const r of issueCourses) {
const realIssues = r.issues.filter(i => !i.includes('建议确认'));
if (realIssues.length === 0) continue;
md += `### ${r.date} ${r.time} - ${r.className}\n\n`;
md += `- 第1级目录: ${r.level1Dir || '无'}\n`;
md += `- 第2级目录: ${r.level2Dirs.join(', ') || '无'}\n`;
md += `- API学生: ${r.apiStudents.join(', ')}\n\n`;
const foundList = r.studentResults.filter(s => s.found).map(s => {
const loc = s.foundAt.level === 1 ? '第1级' : '第2级';
return `${s.name}(${loc},${s.foundAt.type})`;
}).join(', ');
const missingList = r.studentResults.filter(s => !s.found).map(s => s.name).join(', ');
if (foundList) md += `- 已找到: ${foundList}\n`;
if (missingList) md += `- 未找到: ${missingList}\n`;
md += `\n`;
for (const issue of realIssues) {
md += `- ⚠️ ${issue}\n`;
}
md += `\n`;
}
}
// 第2级查找提示详情
const hintCourses = allResults.filter(r => r.issues.some(i => i.includes('建议确认')));
if (hintCourses.length > 0) {
md += `\n## 第2级查找提示课评在非精确匹配目录中找到\n\n`;
for (const r of hintCourses) {
const hints = r.issues.filter(i => i.includes('建议确认'));
md += `### ${r.date} ${r.time} - ${r.className}\n\n`;
md += `- 精确目录: ${r.level1Dir || '无'}\n`;
md += `- 实际找到目录: ${r.level2Dirs.join(', ')}\n\n`;
for (const h of hints) {
md += `- 💡 ${h}\n`;
}
md += `\n`;
}
}
fs.writeFileSync(reportPath, md);
console.log(`\n\n详细报告已保存: ${reportPath}`);
}
main().catch(console.error);