Files
ClassFeedback/scripts/check-api-vs-local.js
chengzi 3b75170862 feat: 添加第12课课评、班级总结及学生画像更新
- K4周日1900班第12课《花朵随心画》课评(梁境城、钟嘉逸、王睿意补课)
- AICODE03/CSP03各班级第12课课评及班级总结
- 更新多班级学生画像
- 课评生成技能优化
2026-05-24 20:48:34 +08:00

487 lines
17 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.
/**
* 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);