272 lines
8.9 KiB
TypeScript
272 lines
8.9 KiB
TypeScript
/**
|
||
* 测试脚本:验证 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);
|
||
});
|