feat(asl): Complete Day 5 - Fulltext Screening Backend API Development
- Implement 5 core API endpoints (create task, get progress, get results, update decision, export Excel) - Add FulltextScreeningController with Zod validation (652 lines) - Implement ExcelExporter service with 4-sheet report generation (352 lines) - Register routes under /api/v1/asl/fulltext-screening - Create 31 REST Client test cases - Add automated integration test script - Fix PDF extraction fallback mechanism in LLM12FieldsService - Update API design documentation to v3.0 - Update development plan to v1.2 - Create Day 5 development record - Clean up temporary test files
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 端到端真实测试 v2 - 简化版
|
||||
*
|
||||
* 使用真实数据测试完整流程:
|
||||
* 1. 创建项目
|
||||
* 2. 导入1篇文献(简化)
|
||||
* 3. 创建全文复筛任务
|
||||
* 4. 等待LLM处理
|
||||
* 5. 查看结果
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const API_BASE = 'http://localhost:3000/api/v1/asl';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface TestResult {
|
||||
projectId?: string;
|
||||
literatureIds?: string[];
|
||||
taskId?: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function runTest(): Promise<TestResult> {
|
||||
console.log('🚀 开始端到端真实测试 v2\n');
|
||||
console.log('⏰ 测试时间:', new Date().toLocaleString('zh-CN'));
|
||||
console.log('📍 API地址:', API_BASE);
|
||||
console.log('=' .repeat(80) + '\n');
|
||||
|
||||
const result: TestResult = { success: false };
|
||||
|
||||
try {
|
||||
// ========================================
|
||||
// Step 1: 创建测试项目
|
||||
// ========================================
|
||||
console.log('📋 Step 1: 创建测试项目');
|
||||
|
||||
const picosPath = path.join(
|
||||
process.cwd(),
|
||||
'../docs/03-业务模块/ASL-AI智能文献/05-测试文档/03-测试数据/screening/测试案例的PICOS、纳入标准、排除标准.txt'
|
||||
);
|
||||
|
||||
const picosContent = await fs.readFile(picosPath, 'utf-8');
|
||||
|
||||
// 解析PICOS
|
||||
const populationMatch = picosContent.match(/P \(Population\)[::]\s*(.+)/);
|
||||
const interventionMatch = picosContent.match(/I \(Intervention\)[::]\s*(.+)/);
|
||||
const comparisonMatch = picosContent.match(/C \(Comparison\)[::]\s*(.+)/);
|
||||
const outcomeMatch = picosContent.match(/O \(Outcome\)[::]\s*(.+)/);
|
||||
const studyDesignMatch = picosContent.match(/S \(Study Design\)[::]\s*(.+)/);
|
||||
|
||||
const projectData = {
|
||||
name: `E2E测试-${Date.now()}`,
|
||||
description: '端到端真实测试项目',
|
||||
picoCriteria: {
|
||||
P: populationMatch?.[1]?.trim() || '缺血性卒中患者',
|
||||
I: interventionMatch?.[1]?.trim() || '抗血小板治疗',
|
||||
C: comparisonMatch?.[1]?.trim() || '对照组',
|
||||
O: outcomeMatch?.[1]?.trim() || '卒中复发',
|
||||
S: studyDesignMatch?.[1]?.trim() || 'RCT',
|
||||
},
|
||||
};
|
||||
|
||||
const projectResponse = await axios.post(`${API_BASE}/projects`, projectData);
|
||||
result.projectId = projectResponse.data.data.id;
|
||||
console.log(`✅ 项目创建成功: ${result.projectId}\n`);
|
||||
|
||||
// ========================================
|
||||
// Step 2: 导入1篇简单测试文献
|
||||
// ========================================
|
||||
console.log('📚 Step 2: 导入测试文献(使用简化数据)');
|
||||
|
||||
const literatureData = {
|
||||
projectId: result.projectId,
|
||||
literatures: [
|
||||
{
|
||||
pmid: 'TEST001',
|
||||
title: 'Antiplatelet Therapy for Secondary Stroke Prevention: A Randomized Controlled Trial',
|
||||
abstract: 'Background: Stroke is a major cause of death worldwide. This study evaluates antiplatelet therapy effectiveness. Methods: We conducted an RCT with 500 patients randomized to aspirin vs clopidogrel groups. The study was double-blind. Results: Primary outcome (stroke recurrence) occurred in 12% of aspirin group vs 8% of clopidogrel group (p=0.03). Secondary outcomes showed similar trends. Conclusion: Clopidogrel demonstrates superior efficacy for secondary stroke prevention in Asian patients.',
|
||||
authors: 'Zhang W, Li H, Wang Y',
|
||||
journal: 'Stroke Research',
|
||||
publicationYear: 2023,
|
||||
hasPdf: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const importResponse = await axios.post(`${API_BASE}/literatures/import`, literatureData);
|
||||
console.log(`✅ 文献导入成功: ${importResponse.data.data.importedCount}篇\n`);
|
||||
|
||||
// 获取文献ID
|
||||
const literatures = await prisma.aslLiterature.findMany({
|
||||
where: { projectId: result.projectId },
|
||||
select: { id: true, title: true },
|
||||
});
|
||||
result.literatureIds = literatures.map(lit => lit.id);
|
||||
console.log('📄 导入的文献:');
|
||||
literatures.forEach(lit => {
|
||||
console.log(` - ${lit.id.slice(0, 8)}: ${lit.title.slice(0, 60)}...`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// ========================================
|
||||
// Step 3: 创建全文复筛任务
|
||||
// ========================================
|
||||
console.log('🤖 Step 3: 创建全文复筛任务');
|
||||
|
||||
const taskData = {
|
||||
projectId: result.projectId,
|
||||
literatureIds: result.literatureIds,
|
||||
config: {
|
||||
modelA: 'deepseek-v3',
|
||||
modelB: 'qwen-max',
|
||||
concurrency: 1,
|
||||
skipExtraction: true, // 跳过PDF提取,使用标题+摘要
|
||||
},
|
||||
};
|
||||
|
||||
const taskResponse = await axios.post(`${API_BASE}/fulltext-screening/tasks`, taskData);
|
||||
result.taskId = taskResponse.data.data.taskId;
|
||||
console.log(`✅ 任务创建成功: ${result.taskId}\n`);
|
||||
|
||||
// ========================================
|
||||
// Step 4: 监控任务进度
|
||||
// ========================================
|
||||
console.log('⏳ Step 4: 监控任务进度(等待LLM处理)\n');
|
||||
|
||||
let maxAttempts = 30; // 最多等待5分钟
|
||||
let attempt = 0;
|
||||
let taskCompleted = false;
|
||||
|
||||
while (attempt < maxAttempts && !taskCompleted) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000)); // 每10秒查询一次
|
||||
attempt++;
|
||||
|
||||
try {
|
||||
const progressResponse = await axios.get(
|
||||
`${API_BASE}/fulltext-screening/tasks/${result.taskId}/progress`
|
||||
);
|
||||
const progress = progressResponse.data.data;
|
||||
|
||||
console.log(`[${attempt}/${maxAttempts}] 进度: ${progress.processedCount}/${progress.totalCount} | ` +
|
||||
`成功: ${progress.successCount} | 失败: ${progress.failedCount} | ` +
|
||||
`Token: ${progress.totalTokens} | 成本: ¥${progress.totalCost.toFixed(4)}`);
|
||||
|
||||
if (progress.status === 'completed' || progress.status === 'failed') {
|
||||
taskCompleted = true;
|
||||
console.log(`\n✅ 任务完成!状态: ${progress.status}\n`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`⚠️ 查询进度失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!taskCompleted) {
|
||||
console.log('⚠️ 任务超时,但可能仍在后台处理\n');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Step 5: 获取结果
|
||||
// ========================================
|
||||
console.log('📊 Step 5: 获取处理结果\n');
|
||||
|
||||
try {
|
||||
const resultsResponse = await axios.get(
|
||||
`${API_BASE}/fulltext-screening/tasks/${result.taskId}/results`
|
||||
);
|
||||
const results = resultsResponse.data.data;
|
||||
|
||||
console.log('=' .repeat(80));
|
||||
console.log('📈 最终统计:');
|
||||
console.log(` - 总文献数: ${results.results.length}`);
|
||||
console.log(` - 总Token: ${results.summary.totalTokens}`);
|
||||
console.log(` - 总成本: ¥${results.summary.totalCost.toFixed(4)}`);
|
||||
console.log('');
|
||||
|
||||
if (results.results.length > 0) {
|
||||
console.log('📄 文献结果详情:');
|
||||
results.results.forEach((r: any, idx: number) => {
|
||||
console.log(`\n[${idx + 1}] ${r.literatureTitle}`);
|
||||
console.log(` Model A (${r.modelAName}): ${r.modelAStatus}`);
|
||||
console.log(` Model B (${r.modelBName}): ${r.modelBStatus}`);
|
||||
console.log(` Token: ${r.modelATokens + r.modelBTokens}`);
|
||||
console.log(` 成本: ¥${(r.modelACost + r.modelBCost).toFixed(4)}`);
|
||||
|
||||
if (r.modelAStatus === 'success' && r.modelAOverall) {
|
||||
console.log(` 决策: ${r.modelAOverall.overall_decision || 'N/A'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
result.success = results.results.length > 0;
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(`❌ 获取结果失败: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n' + '=' .repeat(80));
|
||||
console.log('🎉 测试完成!\n');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
if (error.response?.data) {
|
||||
console.error('错误详情:', JSON.stringify(error.response.data, null, 2));
|
||||
}
|
||||
result.success = false;
|
||||
result.error = error.message;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTest()
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
console.log('✅ 端到端测试成功!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('❌ 端到端测试失败');
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('💥 测试执行异常:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user