/** * 测试脚本:验证 REDCap 计算字段导出和全量质控 * * 测试内容: * 1. 验证 exportCalculatedFields 参数是否生效 * 2. 检查年龄等计算字段是否正确获取 * 3. 执行全量质控并查看结果 */ import { PrismaClient } from '@prisma/client'; import { RedcapAdapter } from './src/modules/iit-manager/adapters/RedcapAdapter.js'; import { createSkillRunner } from './src/modules/iit-manager/engines/SkillRunner.js'; const prisma = new PrismaClient(); async function main() { console.log('='.repeat(60)); console.log('📋 REDCap 计算字段导出测试'); console.log('='.repeat(60)); // 1. 获取项目配置 const project = await prisma.iitProject.findFirst({ where: { name: { contains: 'test0207' } }, select: { id: true, name: true, redcapUrl: true, redcapApiToken: true, fieldMappings: true } }); if (!project) { console.log('❌ 未找到 test0207 项目'); await prisma.$disconnect(); return; } console.log(`\n✅ 找到项目: ${project.name} (ID: ${project.id})`); // 2. 创建 REDCap 适配器(使用默认的 exportCalculatedFields=true) const adapter = new RedcapAdapter(project.redcapUrl, project.redcapApiToken); // =============================== // 测试 1:导出单条记录 // =============================== console.log('\n' + '='.repeat(60)); console.log('📊 测试 1:导出 Record ID=1 的数据'); console.log('='.repeat(60)); try { const record = await adapter.getRecordById('1'); if (!record) { console.log('❌ 未找到 Record ID=1'); } else { // 显示所有字段 console.log('\n📋 所有字段及其值:'); const sortedKeys = Object.keys(record).sort(); for (const key of sortedKeys) { const value = record[key]; const displayValue = value === '' ? '(空)' : value; console.log(` ${key}: ${displayValue}`); } // 特别检查关键字段 console.log('\n🔍 关键字段检查:'); const keyFields = ['record_id', 'age', '年龄', 'date_of_birth', '出生日期', 'gender', '性别']; for (const field of keyFields) { if (record[field] !== undefined) { console.log(` ✅ ${field} = ${record[field] || '(空)'}`); } } // 检查是否有年龄相关字段 console.log('\n🔍 年龄相关字段(模糊匹配):'); const ageRelated = Object.entries(record).filter(([key]) => key.toLowerCase().includes('age') || key.toLowerCase().includes('年龄') || key.toLowerCase().includes('birth') || key.toLowerCase().includes('出生') ); if (ageRelated.length > 0) { for (const [key, value] of ageRelated) { console.log(` ${key} = ${value || '(空)'}`); } } else { console.log(' ⚠️ 未找到年龄相关字段'); } } } catch (error: any) { console.error('❌ REDCap 查询失败:', error.message); } // =============================== // 测试 2:导出所有记录并统计 // =============================== console.log('\n' + '='.repeat(60)); console.log('📊 测试 2:导出所有记录统计'); console.log('='.repeat(60)); try { const allRecords = await adapter.getAllRecordsMerged(); console.log(`\n📋 共获取 ${allRecords.length} 条记录`); // 检查每条记录的年龄字段 const ageField = 'age'; // 假设字段名为 age let hasAge = 0; let noAge = 0; for (const record of allRecords) { // 查找任何包含 age 的字段 const ageValue = record['age'] ?? record['Age'] ?? record['年龄']; if (ageValue !== undefined && ageValue !== '') { hasAge++; } else { noAge++; } } console.log(` 有年龄值的记录: ${hasAge}`); console.log(` 无年龄值的记录: ${noAge}`); // 显示前 3 条记录的摘要 console.log('\n📋 前 3 条记录摘要:'); for (let i = 0; i < Math.min(3, allRecords.length); i++) { const r = allRecords[i]; console.log(` [Record ${r.record_id}] age=${r.age ?? '(无)'}, date_of_birth=${r.date_of_birth ?? '(无)'}`); } } catch (error: any) { console.error('❌ 导出所有记录失败:', error.message); } // =============================== // 测试 3:检查质控规则 // =============================== console.log('\n' + '='.repeat(60)); console.log('📊 测试 3:检查项目质控规则'); console.log('='.repeat(60)); const skills = await prisma.iitSkill.findMany({ where: { projectId: project.id }, select: { id: true, name: true, config: true, isActive: true } }); console.log(`\n📋 项目共有 ${skills.length} 个 Skill:`); for (const skill of skills) { console.log(` ${skill.isActive ? '✅' : '❌'} ${skill.name} (${skill.id})`); // 显示规则摘要 const config = skill.config as any; if (config?.rules && Array.isArray(config.rules)) { console.log(` 规则数: ${config.rules.length}`); for (const rule of config.rules.slice(0, 3)) { console.log(` - ${rule.name}: field=${rule.field}`); } if (config.rules.length > 3) { console.log(` ... 还有 ${config.rules.length - 3} 条规则`); } } } // =============================== // 测试 4:执行全量质控 // =============================== console.log('\n' + '='.repeat(60)); console.log('📊 测试 4:执行全量质控'); console.log('='.repeat(60)); try { const activeSkills = skills.filter(s => s.isActive); if (activeSkills.length === 0) { console.log('⚠️ 没有激活的 Skill,跳过质控测试'); } else { console.log(`\n🚀 开始执行全量质控 (项目 ${project.id})...`); // 使用 createSkillRunner 创建运行器 const runner = createSkillRunner(project.id); // 执行 manual 触发类型的质控(全量) const results = await runner.runByTrigger('manual'); console.log(`\n✅ 质控完成! 处理了 ${results.length} 条记录`); // 统计所有问题 let totalIssues = 0; const allIssuesList: any[] = []; const ruleStats: Record = {}; for (const result of results) { for (const skillResult of result.skillResults) { for (const issue of skillResult.issues) { totalIssues++; allIssuesList.push({ recordId: result.recordId, ...issue }); const ruleName = issue.ruleName || 'unknown'; ruleStats[ruleName] = (ruleStats[ruleName] || 0) + 1; } } } console.log(` 发现问题总数: ${totalIssues}`); if (totalIssues > 0) { console.log(`\n 问题分布:`); for (const [ruleName, count] of Object.entries(ruleStats)) { console.log(` ${ruleName}: ${count} 次`); } // 显示前 5 个问题详情 console.log(`\n 前 5 个问题详情:`); for (const issue of allIssuesList.slice(0, 5)) { console.log(` [Record ${issue.recordId}] ${issue.ruleName}`); console.log(` 实际值: ${issue.actualValue ?? '(空)'}`); console.log(` 期望: ${issue.expectedValue ?? '(未知)'}`); console.log(` 消息: ${issue.llmMessage ?? issue.message}`); } } } } catch (error: any) { console.error('❌ 质控执行失败:', error.message); console.error(error.stack); } // =============================== // 测试 5:查看最新质控日志 // =============================== console.log('\n' + '='.repeat(60)); console.log('📊 测试 5:最新质控日志'); console.log('='.repeat(60)); const recentLogs = await prisma.iitQcLog.findMany({ where: { projectId: project.id }, orderBy: { createdAt: 'desc' }, take: 5, select: { id: true, recordId: true, status: true, issues: true, createdAt: true, } }); console.log(`\n📋 最近 ${recentLogs.length} 条质控日志:`); for (const log of recentLogs) { const issues = log.issues as any; const issueCount = Array.isArray(issues) ? issues.length : (issues?.items?.length ?? 0); console.log(` [${log.recordId}] ${log.status} - ${issueCount} 个问题 (${log.createdAt.toISOString()})`); // 显示问题详情 const issueList = Array.isArray(issues) ? issues : (issues?.items ?? []); for (const issue of issueList.slice(0, 2)) { console.log(` ${issue.ruleName}: actualValue=${issue.actualValue ?? '(空)'}, expectedValue=${issue.expectedValue ?? '(未知)'}`); } } 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); });