Files
AIclinicalresearch/backend/test-redcap-calculated-fields.ts

272 lines
8.9 KiB
TypeScript
Raw 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.
/**
* 测试脚本:验证 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<string, number> = {};
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);
});