feat(iit): Implement event-level QC architecture V3.1 with dynamic rule filtering, report deduplication and AI intent enhancement

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 21:22:11 +08:00
parent 45c7b32dbb
commit 7a299e8562
51 changed files with 10638 additions and 184 deletions

View File

@@ -0,0 +1,175 @@
/**
* 调试脚本:诊断表单和事件问题
*
* 问题1表单数量 - 应该是19个含重复访视但只识别7个
* 问题2Age 为空 - 即使 Complete 状态,某些记录 age 仍为空
*/
import { PrismaClient } from '@prisma/client';
import { RedcapAdapter } from './src/modules/iit-manager/adapters/RedcapAdapter.js';
const prisma = new PrismaClient();
async function main() {
console.log('='.repeat(60));
console.log('🔍 表单和事件诊断');
console.log('='.repeat(60));
const project = await prisma.iitProject.findFirst({
where: { name: { contains: 'test0207' } },
});
if (!project) {
console.log('❌ 未找到项目');
await prisma.$disconnect();
return;
}
const adapter = new RedcapAdapter(project.redcapUrl, project.redcapApiToken);
// ===============================
// 诊断 1获取事件和表单映射
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 诊断 1REDCap 事件和表单结构');
console.log('='.repeat(60));
// 获取事件列表
try {
const formData = new (await import('form-data')).default();
formData.append('token', project.redcapApiToken);
formData.append('content', 'event');
formData.append('format', 'json');
const axios = (await import('axios')).default;
const eventsResponse = await axios.post(`${project.redcapUrl}/api/`, formData, {
headers: formData.getHeaders()
});
const events = eventsResponse.data;
console.log(`\n📋 事件列表 (共 ${events.length} 个):`);
for (const event of events) {
console.log(` ${event.unique_event_name}: ${event.event_name}`);
}
} catch (error: any) {
console.log(' 获取事件列表失败:', error.message);
}
// 获取表单-事件映射
try {
const formData = new (await import('form-data')).default();
formData.append('token', project.redcapApiToken);
formData.append('content', 'formEventMapping');
formData.append('format', 'json');
const axios = (await import('axios')).default;
const mappingResponse = await axios.post(`${project.redcapUrl}/api/`, formData, {
headers: formData.getHeaders()
});
const mapping = mappingResponse.data;
console.log(`\n📋 表单-事件映射 (共 ${mapping.length} 个):`);
// 按事件分组
const eventForms = new Map<string, string[]>();
for (const m of mapping) {
if (!eventForms.has(m.unique_event_name)) {
eventForms.set(m.unique_event_name, []);
}
eventForms.get(m.unique_event_name)!.push(m.form);
}
for (const [event, forms] of eventForms) {
console.log(`\n 📁 ${event}:`);
for (const form of forms) {
console.log(` - ${form}`);
}
}
// 统计唯一表单
const uniqueForms = [...new Set(mapping.map((m: any) => m.form))];
console.log(`\n📊 统计:`);
console.log(` 唯一表单数: ${uniqueForms.length}`);
console.log(` 表单-事件映射总数: ${mapping.length}`);
console.log(` 这就是为什么显示19个而不是7个!`);
} catch (error: any) {
console.log(' 获取表单-事件映射失败:', error.message);
}
// ===============================
// 诊断 2检查 Record 4, 5, 14 的 age 问题
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 诊断 2检查 age 为空的记录');
console.log('='.repeat(60));
const problemRecords = ['4', '5', '14'];
for (const recId of problemRecords) {
console.log(`\n === Record ${recId} ===`);
// 获取原始数据(所有事件)
const rawRecords = await adapter.exportRecords({ records: [recId] });
console.log(` 原始记录数: ${rawRecords.length} (多事件)`);
for (const r of rawRecords) {
const event = r.redcap_event_name || '(无事件)';
const age = r.age !== undefined && r.age !== '' ? r.age : '(空)';
const dob = r.date_of_birth || '(空)';
const formComplete = r.basic_demography_form_complete;
console.log(` 事件: ${event}`);
console.log(` age: ${age}, dob: ${dob}, form_complete: ${formComplete}`);
}
// 获取合并后数据
const merged = await adapter.getRecordById(recId);
console.log(` 合并后: age=${merged?.age || '(空)'}, dob=${merged?.date_of_birth || '(空)'}`);
}
// ===============================
// 诊断 3检查 age 字段的元数据
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 诊断 3age 字段的元数据');
console.log('='.repeat(60));
const calcFields = await adapter.getCalculatedFields();
const ageField = calcFields.find(f => f.fieldName === 'age');
if (ageField) {
console.log(`\n 字段名: ${ageField.fieldName}`);
console.log(` 标签: ${ageField.fieldLabel}`);
console.log(` 表单: ${ageField.formName}`);
console.log(` 计算公式: ${ageField.calculation}`);
} else {
console.log('\n ⚠️ 未找到 age 计算字段!');
// 检查是否有其他年龄相关字段
const metadata = await adapter.exportMetadata();
const ageRelated = metadata.filter((f: any) =>
f.field_name.toLowerCase().includes('age') ||
f.field_label?.toLowerCase().includes('年龄')
);
console.log(`\n 年龄相关字段:`);
for (const f of ageRelated) {
console.log(` ${f.field_name} (${f.field_type}): ${f.field_label}`);
if (f.field_type === 'calc') {
console.log(` 公式: ${f.select_choices_or_calculations}`);
}
}
}
console.log('\n' + '='.repeat(60));
console.log('✅ 诊断完成');
console.log('='.repeat(60));
await prisma.$disconnect();
}
main().catch(async (error) => {
console.error('❌ 脚本出错:', error);
await prisma.$disconnect();
process.exit(1);
});