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:
2025-11-23 10:52:07 +08:00
parent 08aa3f6c28
commit 88cc049fb3
232 changed files with 7780 additions and 441 deletions

View File

@@ -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);
});