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

164
backend/test-form-status.ts Normal file
View File

@@ -0,0 +1,164 @@
/**
* 测试脚本:验证表单状态和计算字段获取
*/
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));
// 1. 获取项目配置
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('📊 测试 1计算字段列表');
console.log('='.repeat(60));
const calcFields = await adapter.getCalculatedFields();
console.log(`\n共有 ${calcFields.length} 个计算字段:`);
for (const field of calcFields) {
console.log(` 📐 ${field.fieldName} (${field.fieldLabel})`);
console.log(` 表单: ${field.formName}`);
console.log(` 公式: ${field.calculation.substring(0, 80)}${field.calculation.length > 80 ? '...' : ''}`);
}
// ===============================
// 测试 2获取表单完成状态
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 测试 2表单完成状态 (前 5 条记录)');
console.log('='.repeat(60));
const formStatus = await adapter.getFormCompletionStatus();
// 只显示前 5 条
for (const record of formStatus.slice(0, 5)) {
console.log(`\n [Record ${record.recordId}] ${record.allComplete ? '✅ 全部完成' : '⚠️ 部分未完成'}`);
// 统计各状态数量
const statusCounts = { Complete: 0, Unverified: 0, Incomplete: 0 };
for (const [formName, status] of Object.entries(record.forms)) {
statusCounts[status.statusLabel]++;
}
console.log(` Complete: ${statusCounts.Complete}, Unverified: ${statusCounts.Unverified}, Incomplete: ${statusCounts.Incomplete}`);
// 显示非 Complete 的表单
const nonComplete = Object.entries(record.forms)
.filter(([_, s]) => s.statusLabel !== 'Complete')
.map(([name, s]) => `${name}(${s.statusLabel})`);
if (nonComplete.length > 0 && nonComplete.length <= 5) {
console.log(` 未完成: ${nonComplete.join(', ')}`);
} else if (nonComplete.length > 5) {
console.log(` 未完成: ${nonComplete.slice(0, 5).join(', ')}... (共${nonComplete.length}个)`);
}
}
// ===============================
// 测试 3单个表单状态检查
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 测试 3检查 Record 3 的人口学表单状态');
console.log('='.repeat(60));
const demoFormStatus = await adapter.isFormComplete('3', 'basic_demography_form');
console.log(`\n Record 3 - basic_demography_form:`);
console.log(` 状态: ${demoFormStatus.statusLabel} (${demoFormStatus.status})`);
console.log(` 完成: ${demoFormStatus.isComplete ? '是' : '否'}`);
// ===============================
// 测试 4关联分析 - 计算字段 vs 表单状态
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 测试 4计算字段与表单状态关联分析');
console.log('='.repeat(60));
// 获取所有记录数据
const allRecords = await adapter.getAllRecordsMerged();
console.log('\n 分析计算字段 "age" 与表单状态的关系:');
console.log(' ' + '-'.repeat(50));
for (const record of allRecords.slice(0, 5)) {
const recId = record.record_id;
const age = record.age;
const dob = record.date_of_birth;
// 获取该记录的表单状态
const statusInfo = formStatus.find(s => s.recordId === recId);
const demoStatus = statusInfo?.forms['basic_demography_form']?.statusLabel || 'Unknown';
const ageDisplay = age !== undefined && age !== '' ? age : '(空)';
console.log(` Record ${recId}: age=${ageDisplay}, dob=${dob || '(空)'}, 表单状态=${demoStatus}`);
}
// ===============================
// 测试 5按事件维度的表单完成状态正确方式
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 测试 5按事件维度的表单完成状态 (Record 1)');
console.log('='.repeat(60));
const eventStatus = await adapter.getFormCompletionStatusByEvent('1');
console.log(`\nRecord 1 共有 ${eventStatus.length} 个事件:`);
for (const event of eventStatus) {
const statusIcon = event.eventComplete ? '✅' : '⚠️';
console.log(`\n ${statusIcon} 事件: ${event.eventLabel} (${event.eventName})`);
for (const [formName, status] of Object.entries(event.forms)) {
const icon = status.status === 2 ? '✅' : (status.status === 1 ? '🟡' : '🔴');
console.log(` ${icon} ${formName}: ${status.statusLabel}`);
}
}
// ===============================
// 测试 6获取表单-事件映射
// ===============================
console.log('\n' + '='.repeat(60));
console.log('📊 测试 6表单-事件映射统计');
console.log('='.repeat(60));
const formEventMapping = await adapter.getFormEventMapping();
console.log(`\n表单-事件映射总数: ${formEventMapping.length}`);
// 按事件分组统计
const eventFormCount = new Map<string, number>();
for (const m of formEventMapping) {
eventFormCount.set(m.eventLabel, (eventFormCount.get(m.eventLabel) || 0) + 1);
}
console.log('\n各事件的表单数:');
for (const [event, count] of eventFormCount) {
console.log(` ${event}: ${count} 个表单`);
}
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);
});