Summary: - Implement intelligent multi-metric grouping detection algorithm - Add direction 1: timepoint-as-row, metric-as-column (analysis format) - Add direction 2: timepoint-as-column, metric-as-row (display format) - Fix column name pattern detection (FMA___ issue) - Maintain original Record ID order in output - Add full-select/clear buttons in UI - Integrate into TransformDialog with Radio selection - Update 3 documentation files Technical Details: - Python: detect_metric_groups(), apply_multi_metric_to_long(), apply_multi_metric_to_matrix() - Backend: 3 new methods in QuickActionService - Frontend: MultiMetricPanel.tsx (531 lines) - Total: ~1460 lines of new code Status: Fully tested and verified, ready for production
326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
/**
|
||
* 全文复筛API集成测试
|
||
*
|
||
* 运行方式:
|
||
* npx tsx src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client';
|
||
|
||
const prisma = new PrismaClient();
|
||
const BASE_URL = 'http://localhost:3001';
|
||
const API_PREFIX = '/api/v1/asl/fulltext-screening';
|
||
|
||
// 测试辅助函数
|
||
async function fetchJSON(url: string, options: RequestInit = {}) {
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options.headers,
|
||
},
|
||
});
|
||
|
||
const data = await response.json();
|
||
return { response, data };
|
||
}
|
||
|
||
// 等待函数
|
||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
async function runTests() {
|
||
console.log('🧪 开始全文复筛API集成测试\n');
|
||
|
||
try {
|
||
// ==================== 准备测试数据 ====================
|
||
console.log('📋 步骤1: 准备测试数据...');
|
||
|
||
// 获取第一个项目
|
||
const project = await prisma.aslScreeningProject.findFirst({
|
||
include: {
|
||
literatures: {
|
||
take: 3,
|
||
select: {
|
||
id: true,
|
||
title: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!project) {
|
||
throw new Error('未找到测试项目,请先创建项目和文献');
|
||
}
|
||
|
||
if (project.literatures.length === 0) {
|
||
throw new Error('项目中没有文献,请先导入文献');
|
||
}
|
||
|
||
const projectId = project.id;
|
||
const literatureIds = project.literatures.map((lit) => lit.id).slice(0, 2);
|
||
|
||
console.log(` ✅ 项目ID: ${projectId}`);
|
||
console.log(` ✅ 文献数量: ${literatureIds.length}`);
|
||
console.log(` ✅ 文献列表:`, literatureIds);
|
||
console.log('');
|
||
|
||
// ==================== 测试API 1: 创建任务 ====================
|
||
console.log('📋 步骤2: 测试创建全文复筛任务...');
|
||
|
||
const { response: createResponse, data: createData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks`,
|
||
{
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
projectId,
|
||
literatureIds,
|
||
modelA: 'deepseek-v3',
|
||
modelB: 'qwen-max',
|
||
promptVersion: 'v1.0.0',
|
||
}),
|
||
}
|
||
);
|
||
|
||
if (createResponse.status !== 201 || !createData.success) {
|
||
throw new Error(`创建任务失败: ${JSON.stringify(createData)}`);
|
||
}
|
||
|
||
const taskId = createData.data.taskId;
|
||
console.log(` ✅ 任务创建成功`);
|
||
console.log(` ✅ 任务ID: ${taskId}`);
|
||
console.log(` ✅ 状态: ${createData.data.status}`);
|
||
console.log(` ✅ 文献总数: ${createData.data.totalCount}`);
|
||
console.log('');
|
||
|
||
// ==================== 测试API 2: 获取任务进度 ====================
|
||
console.log('📋 步骤3: 测试获取任务进度...');
|
||
|
||
// 等待一段时间让任务开始处理
|
||
console.log(' ⏳ 等待3秒让任务开始处理...');
|
||
await sleep(3000);
|
||
|
||
const { response: progressResponse, data: progressData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}`
|
||
);
|
||
|
||
if (progressResponse.status !== 200 || !progressData.success) {
|
||
throw new Error(`获取进度失败: ${JSON.stringify(progressData)}`);
|
||
}
|
||
|
||
console.log(` ✅ 任务状态: ${progressData.data.status}`);
|
||
console.log(` ✅ 进度: ${progressData.data.progress.processedCount}/${progressData.data.progress.totalCount} (${progressData.data.progress.progressPercent}%)`);
|
||
console.log(` ✅ 成功: ${progressData.data.progress.successCount}`);
|
||
console.log(` ✅ 失败: ${progressData.data.progress.failedCount}`);
|
||
console.log(` ✅ 降级: ${progressData.data.progress.degradedCount}`);
|
||
console.log(` ✅ Token: ${progressData.data.statistics.totalTokens}`);
|
||
console.log(` ✅ 成本: ¥${progressData.data.statistics.totalCost.toFixed(4)}`);
|
||
console.log('');
|
||
|
||
// 如果任务还在处理,等待完成
|
||
if (progressData.data.status === 'processing' || progressData.data.status === 'pending') {
|
||
console.log(' ⏳ 任务仍在处理中,等待完成...');
|
||
|
||
let attempts = 0;
|
||
const maxAttempts = 20; // 最多等待20次(约100秒)
|
||
|
||
while (attempts < maxAttempts) {
|
||
await sleep(5000); // 每5秒查询一次
|
||
|
||
const { data: checkData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}`
|
||
);
|
||
|
||
console.log(` 📊 [${attempts + 1}/${maxAttempts}] 进度: ${checkData.data.progress.progressPercent}%, 状态: ${checkData.data.status}`);
|
||
|
||
if (checkData.data.status === 'completed' || checkData.data.status === 'failed') {
|
||
console.log(` ✅ 任务已完成,状态: ${checkData.data.status}`);
|
||
break;
|
||
}
|
||
|
||
attempts++;
|
||
}
|
||
|
||
if (attempts >= maxAttempts) {
|
||
console.log(' ⚠️ 任务处理超时,但继续测试后续API');
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
// ==================== 测试API 3: 获取任务结果 ====================
|
||
console.log('📋 步骤4: 测试获取任务结果...');
|
||
|
||
// 4.1 获取所有结果
|
||
const { response: resultsResponse, data: resultsData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results`
|
||
);
|
||
|
||
if (resultsResponse.status !== 200 || !resultsData.success) {
|
||
throw new Error(`获取结果失败: ${JSON.stringify(resultsData)}`);
|
||
}
|
||
|
||
console.log(` ✅ 获取所有结果成功`);
|
||
console.log(` ✅ 总结果数: ${resultsData.data.total}`);
|
||
console.log(` ✅ 当前页结果数: ${resultsData.data.results.length}`);
|
||
console.log(` ✅ 冲突数: ${resultsData.data.summary.conflictCount}`);
|
||
console.log(` ✅ 待审核: ${resultsData.data.summary.pendingReview}`);
|
||
console.log(` ✅ 已审核: ${resultsData.data.summary.reviewed}`);
|
||
|
||
if (resultsData.data.results.length > 0) {
|
||
const firstResult = resultsData.data.results[0];
|
||
console.log(`\n 📄 第一个结果详情:`);
|
||
console.log(` - 文献ID: ${firstResult.literatureId}`);
|
||
console.log(` - 标题: ${firstResult.literature.title.slice(0, 60)}...`);
|
||
console.log(` - 模型A状态: ${firstResult.modelAResult.status}`);
|
||
console.log(` - 模型B状态: ${firstResult.modelBResult.status}`);
|
||
console.log(` - 是否冲突: ${firstResult.conflict.isConflict ? '是' : '否'}`);
|
||
console.log(` - 最终决策: ${firstResult.review.finalDecision || '待审核'}`);
|
||
}
|
||
console.log('');
|
||
|
||
// 4.2 测试筛选功能
|
||
console.log(' 🔍 测试结果筛选功能...');
|
||
|
||
const { data: conflictData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results?filter=conflict`
|
||
);
|
||
console.log(` ✅ 冲突项筛选: ${conflictData.data.filtered}条`);
|
||
|
||
const { data: pendingData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results?filter=pending`
|
||
);
|
||
console.log(` ✅ 待审核筛选: ${pendingData.data.filtered}条`);
|
||
|
||
console.log('');
|
||
|
||
// ==================== 测试API 4: 人工审核决策 ====================
|
||
if (resultsData.data.results.length > 0) {
|
||
console.log('📋 步骤5: 测试人工审核决策...');
|
||
|
||
const resultId = resultsData.data.results[0].resultId;
|
||
|
||
// 5.1 测试纳入决策
|
||
const { response: decisionResponse, data: decisionData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/results/${resultId}/decision`,
|
||
{
|
||
method: 'PUT',
|
||
body: JSON.stringify({
|
||
finalDecision: 'include',
|
||
reviewNotes: '集成测试 - 自动审核纳入',
|
||
}),
|
||
}
|
||
);
|
||
|
||
if (decisionResponse.status !== 200 || !decisionData.success) {
|
||
throw new Error(`更新决策失败: ${JSON.stringify(decisionData)}`);
|
||
}
|
||
|
||
console.log(` ✅ 更新决策成功`);
|
||
console.log(` ✅ 结果ID: ${decisionData.data.resultId}`);
|
||
console.log(` ✅ 最终决策: ${decisionData.data.finalDecision}`);
|
||
console.log(` ✅ 审核人: ${decisionData.data.reviewedBy}`);
|
||
console.log(` ✅ 审核时间: ${new Date(decisionData.data.reviewedAt).toLocaleString('zh-CN')}`);
|
||
console.log('');
|
||
|
||
// 5.2 测试排除决策(如果有第二个结果)
|
||
if (resultsData.data.results.length > 1) {
|
||
const secondResultId = resultsData.data.results[1].resultId;
|
||
|
||
const { data: excludeData } = await fetchJSON(
|
||
`${BASE_URL}${API_PREFIX}/results/${secondResultId}/decision`,
|
||
{
|
||
method: 'PUT',
|
||
body: JSON.stringify({
|
||
finalDecision: 'exclude',
|
||
exclusionReason: '测试排除原因 - 数据不完整',
|
||
reviewNotes: '集成测试 - 自动审核排除',
|
||
}),
|
||
}
|
||
);
|
||
|
||
console.log(` ✅ 排除决策测试成功`);
|
||
console.log(` ✅ 排除原因: ${excludeData.data.exclusionReason}`);
|
||
console.log('');
|
||
}
|
||
}
|
||
|
||
// ==================== 测试API 5: 导出Excel ====================
|
||
console.log('📋 步骤6: 测试导出Excel...');
|
||
|
||
const exportResponse = await fetch(
|
||
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/export`,
|
||
{
|
||
headers: {
|
||
Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
},
|
||
}
|
||
);
|
||
|
||
if (exportResponse.status !== 200) {
|
||
throw new Error(`导出Excel失败: ${exportResponse.statusText}`);
|
||
}
|
||
|
||
const buffer = await exportResponse.arrayBuffer();
|
||
console.log(` ✅ Excel导出成功`);
|
||
console.log(` ✅ 文件大小: ${(buffer.byteLength / 1024).toFixed(2)} KB`);
|
||
console.log(` ✅ Content-Type: ${exportResponse.headers.get('Content-Type')}`);
|
||
console.log('');
|
||
|
||
// ==================== 测试完成 ====================
|
||
console.log('✅ 所有测试通过!\n');
|
||
|
||
console.log('📊 测试总结:');
|
||
console.log(' ✅ API 1: 创建任务 - 通过');
|
||
console.log(' ✅ API 2: 获取进度 - 通过');
|
||
console.log(' ✅ API 3: 获取结果 - 通过');
|
||
console.log(' ✅ API 4: 人工审核 - 通过');
|
||
console.log(' ✅ API 5: 导出Excel - 通过');
|
||
console.log('');
|
||
|
||
} catch (error: any) {
|
||
console.error('\n❌ 测试失败:', error.message);
|
||
console.error('\n详细错误:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|
||
}
|
||
|
||
// 运行测试
|
||
runTests().catch((error) => {
|
||
console.error('测试运行失败:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|