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,293 @@
|
||||
/**
|
||||
* 全文复筛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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* 全文复筛端到端集成测试(真实LLM调用)
|
||||
*
|
||||
* 测试流程:
|
||||
* 1. 创建真实项目(使用测试案例的PICOS)
|
||||
* 2. 导入2篇真实文献
|
||||
* 3. 调用全文复筛API
|
||||
* 4. 使用真实LLM(DeepSeek-V3 + Qwen-Max)处理
|
||||
* 5. 验证12字段提取结果
|
||||
* 6. 检查冲突检测
|
||||
* 7. 导出Excel并保存
|
||||
* 8. 输出详细测试报告
|
||||
*
|
||||
* 运行方式:
|
||||
* npx tsx src/modules/asl/fulltext-screening/__tests__/e2e-real-test.ts
|
||||
*
|
||||
* 预计成本:¥0.1-0.2
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const BASE_URL = 'http://localhost:3001';
|
||||
const API_PREFIX = '/api/v1/asl';
|
||||
|
||||
// 测试用PICOS(来自真实案例)
|
||||
const TEST_PICOS = {
|
||||
population: '非心源性缺血性卒中(NCIS)、亚洲人群',
|
||||
intervention: '抗血小板治疗药物(阿司匹林、氯吡格雷、替格瑞洛等)或抗凝药物(华法林、低分子肝素等)',
|
||||
comparison: '对照组(安慰剂或标准治疗)',
|
||||
outcome: '疗效安全性:卒中进展、卒中复发、死亡、NIHSS评分变化、VTE、疗效、安全性',
|
||||
studyDesign: '系统评价(SR)、随机对照试验(RCT)、真实世界研究(RWE)、观察性研究(OBS)'
|
||||
};
|
||||
|
||||
const INCLUSION_CRITERIA = `
|
||||
1. 非心源性缺血性卒中、亚洲患者
|
||||
2. 接受二级预防治疗(抗血小板或抗凝治疗)
|
||||
3. 涉及相关药物:阿司匹林、氯吡格雷、替格瑞洛、华法林等
|
||||
4. 研究类型:SR、RCT、RWE、OBS
|
||||
5. 研究时间:2020年之后
|
||||
`;
|
||||
|
||||
const EXCLUSION_CRITERIA = `
|
||||
1. 心源性卒中患者、非亚洲人群
|
||||
2. 仅涉及急性期治疗(溶栓、取栓)而非二级预防
|
||||
3. 房颤患者
|
||||
4. 急性冠脉综合征(ACS)患者(无卒中史)
|
||||
5. 病例报告
|
||||
6. 非中英文文献
|
||||
`;
|
||||
|
||||
// 测试文献元数据
|
||||
const TEST_LITERATURES = [
|
||||
{
|
||||
pmid: '256859669',
|
||||
title: 'Effect of antiplatelet therapy on stroke recurrence in Asian patients with non-cardioembolic ischemic stroke',
|
||||
abstract: 'Background: Secondary prevention of stroke is crucial. This study investigates antiplatelet therapy effectiveness. Methods: RCT comparing aspirin vs clopidogrel. Results: Reduced recurrence observed.',
|
||||
authors: 'Zhang Y, Li X, Wang H',
|
||||
journal: 'Stroke Research',
|
||||
publicationYear: 2022,
|
||||
doi: '10.1234/stroke.2022.001',
|
||||
},
|
||||
{
|
||||
pmid: '256859738',
|
||||
title: 'Dual antiplatelet therapy for secondary stroke prevention in elderly Asian patients',
|
||||
abstract: 'Objective: Evaluate dual antiplatelet therapy (DAPT) in elderly. Design: Observational study. Findings: DAPT shows efficacy with acceptable safety profile.',
|
||||
authors: 'Chen W, Kim S, Liu J',
|
||||
journal: 'Journal of Neurology',
|
||||
publicationYear: 2023,
|
||||
doi: '10.1234/jneuro.2023.002',
|
||||
},
|
||||
];
|
||||
|
||||
// 辅助函数
|
||||
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 runE2ETest() {
|
||||
console.log('🧪 开始全文复筛端到端集成测试(真实LLM)\n');
|
||||
console.log('⚠️ 注意:此测试将调用真实LLM API,产生约¥0.1-0.2成本\n');
|
||||
|
||||
let projectId: string;
|
||||
let literatureIds: string[] = [];
|
||||
let taskId: string;
|
||||
|
||||
try {
|
||||
// ==================== 步骤1: 创建测试项目 ====================
|
||||
console.log('📋 步骤1: 创建测试项目...');
|
||||
|
||||
const { response: createProjectRes, data: projectData } = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/projects`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
projectName: `[E2E测试] 非心源性卒中二级预防 - ${new Date().toLocaleString('zh-CN')}`,
|
||||
picoCriteria: TEST_PICOS,
|
||||
inclusionCriteria: INCLUSION_CRITERIA,
|
||||
exclusionCriteria: EXCLUSION_CRITERIA,
|
||||
screeningConfig: {
|
||||
models: ['deepseek-v3', 'qwen-max'],
|
||||
temperature: 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (createProjectRes.status !== 201 || !projectData.success) {
|
||||
throw new Error(`创建项目失败: ${JSON.stringify(projectData)}`);
|
||||
}
|
||||
|
||||
projectId = projectData.data.id;
|
||||
console.log(` ✅ 项目创建成功: ${projectId}`);
|
||||
console.log(` ✅ 项目名称: ${projectData.data.projectName}`);
|
||||
console.log('');
|
||||
|
||||
// ==================== 步骤2: 导入文献 ====================
|
||||
console.log('📋 步骤2: 导入测试文献...');
|
||||
|
||||
const { response: importRes, data: importData } = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/literatures/import`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
literatures: TEST_LITERATURES,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (importRes.status !== 201 || !importData.success) {
|
||||
throw new Error(`导入文献失败: ${JSON.stringify(importData)}`);
|
||||
}
|
||||
|
||||
console.log(` ✅ 文献导入成功: ${importData.data.importedCount}篇`);
|
||||
|
||||
// 直接从数据库获取导入的文献
|
||||
const importedLiteratures = await prisma.aslLiterature.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 2,
|
||||
select: { id: true, title: true },
|
||||
});
|
||||
|
||||
if (importedLiteratures.length === 0) {
|
||||
throw new Error('未找到导入的文献');
|
||||
}
|
||||
|
||||
literatureIds = importedLiteratures.map((lit) => lit.id);
|
||||
console.log(` ✅ 获取文献ID: ${literatureIds.length}篇`);
|
||||
TEST_LITERATURES.forEach((lit, idx) => {
|
||||
console.log(` ${idx + 1}. ${lit.title.slice(0, 80)}...`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// ==================== 步骤3: 创建全文复筛任务 ====================
|
||||
console.log('📋 步骤3: 创建全文复筛任务...');
|
||||
console.log(' 🤖 模型配置: DeepSeek-V3 + Qwen-Max');
|
||||
console.log(' ⚠️ 开始调用真实LLM API...\n');
|
||||
|
||||
const { response: createTaskRes, data: taskData } = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/fulltext-screening/tasks`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
literatureIds,
|
||||
modelA: 'deepseek-v3',
|
||||
modelB: 'qwen-max',
|
||||
promptVersion: 'v1.0.0',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (createTaskRes.status !== 201 || !taskData.success) {
|
||||
throw new Error(`创建任务失败: ${JSON.stringify(taskData)}`);
|
||||
}
|
||||
|
||||
taskId = taskData.data.taskId;
|
||||
console.log(` ✅ 任务创建成功: ${taskId}`);
|
||||
console.log(` ✅ 状态: ${taskData.data.status}`);
|
||||
console.log(` ⏳ 预计处理时间: 2-5分钟\n`);
|
||||
|
||||
// ==================== 步骤4: 监控任务进度 ====================
|
||||
console.log('📋 步骤4: 监控任务处理进度...\n');
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 最多等待5分钟(每5秒查询一次)
|
||||
let completed = false;
|
||||
|
||||
while (attempts < maxAttempts && !completed) {
|
||||
await sleep(5000); // 每5秒查询一次
|
||||
|
||||
const { data: progressData } = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/fulltext-screening/tasks/${taskId}`
|
||||
);
|
||||
|
||||
const progress = progressData.data.progress;
|
||||
const stats = progressData.data.statistics;
|
||||
|
||||
console.log(` [${attempts + 1}/${maxAttempts}] 进度: ${progress.progressPercent}%`);
|
||||
console.log(` - 已处理: ${progress.processedCount}/${progress.totalCount}`);
|
||||
console.log(` - 成功: ${progress.successCount}, 失败: ${progress.failedCount}, 降级: ${progress.degradedCount}`);
|
||||
console.log(` - Token: ${stats.totalTokens.toLocaleString()}, 成本: ¥${stats.totalCost.toFixed(4)}`);
|
||||
|
||||
if (progressData.data.status === 'completed') {
|
||||
console.log('\n ✅ 任务处理完成!\n');
|
||||
completed = true;
|
||||
break;
|
||||
} else if (progressData.data.status === 'failed') {
|
||||
throw new Error(`任务失败: ${progressData.data.error?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
console.log('\n ⚠️ 任务处理超时,但继续获取当前结果...\n');
|
||||
}
|
||||
|
||||
// ==================== 步骤5: 获取并分析结果 ====================
|
||||
console.log('📋 步骤5: 获取全文复筛结果...\n');
|
||||
|
||||
const { data: resultsData } = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/fulltext-screening/tasks/${taskId}/results`
|
||||
);
|
||||
|
||||
if (!resultsData.success) {
|
||||
throw new Error(`获取结果失败: ${JSON.stringify(resultsData)}`);
|
||||
}
|
||||
|
||||
const results = resultsData.data.results;
|
||||
const summary = resultsData.data.summary;
|
||||
|
||||
console.log(' 📊 结果总览:');
|
||||
console.log(` - 总结果数: ${summary.totalResults}`);
|
||||
console.log(` - 冲突数: ${summary.conflictCount}`);
|
||||
console.log(` - 待审核: ${summary.pendingReview}`);
|
||||
console.log(` - 已审核: ${summary.reviewed}\n`);
|
||||
|
||||
// 分析每个结果
|
||||
if (results.length > 0) {
|
||||
console.log(' 📄 详细结果分析:\n');
|
||||
|
||||
results.forEach((result: any, idx: number) => {
|
||||
console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
console.log(` 文献 ${idx + 1}: ${result.literature.title.slice(0, 70)}...`);
|
||||
console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
||||
|
||||
// 模型A结果
|
||||
console.log(` 🤖 模型A (${result.modelAResult.modelName}):`);
|
||||
console.log(` 状态: ${result.modelAResult.status}`);
|
||||
console.log(` Token: ${result.modelAResult.tokens}, 成本: ¥${result.modelAResult.cost?.toFixed(4)}`);
|
||||
|
||||
if (result.modelAResult.status === 'success') {
|
||||
const overall = result.modelAResult.overall;
|
||||
console.log(` 决策: ${overall?.decision || 'N/A'}`);
|
||||
console.log(` 置信度: ${overall?.confidence || 'N/A'}`);
|
||||
console.log(` 数据质量: ${overall?.dataQuality || 'N/A'}`);
|
||||
console.log(` 理由: ${overall?.reason?.slice(0, 100) || 'N/A'}...`);
|
||||
|
||||
// 显示12字段概览
|
||||
const fields = result.modelAResult.fields;
|
||||
if (fields) {
|
||||
const fieldKeys = Object.keys(fields);
|
||||
console.log(` 字段提取: ${fieldKeys.length}/12个字段`);
|
||||
|
||||
// 显示几个关键字段
|
||||
['field5_population', 'field7_intervention', 'field9_outcomes'].forEach(key => {
|
||||
if (fields[key]) {
|
||||
console.log(` - ${key}: ${fields[key].assessment || 'N/A'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(` 错误: ${result.modelAResult.error || 'N/A'}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 模型B结果
|
||||
console.log(` 🤖 模型B (${result.modelBResult.modelName}):`);
|
||||
console.log(` 状态: ${result.modelBResult.status}`);
|
||||
console.log(` Token: ${result.modelBResult.tokens}, 成本: ¥${result.modelBResult.cost?.toFixed(4)}`);
|
||||
|
||||
if (result.modelBResult.status === 'success') {
|
||||
const overall = result.modelBResult.overall;
|
||||
console.log(` 决策: ${overall?.decision || 'N/A'}`);
|
||||
console.log(` 置信度: ${overall?.confidence || 'N/A'}`);
|
||||
} else {
|
||||
console.log(` 错误: ${result.modelBResult.error || 'N/A'}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 冲突检测
|
||||
if (result.conflict.isConflict) {
|
||||
console.log(` ⚠️ 冲突检测:`);
|
||||
console.log(` 严重程度: ${result.conflict.severity}`);
|
||||
console.log(` 冲突字段: ${result.conflict.conflictFields.join(', ')}`);
|
||||
console.log(` 审核优先级: ${result.review.priority}`);
|
||||
} else {
|
||||
console.log(` ✅ 无冲突 (双模型一致)`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
});
|
||||
} else {
|
||||
console.log(' ⚠️ 暂无结果(可能任务还在处理中)\n');
|
||||
}
|
||||
|
||||
// ==================== 步骤6: 导出Excel ====================
|
||||
console.log('📋 步骤6: 导出Excel报告...\n');
|
||||
|
||||
const exportResponse = await fetch(
|
||||
`${BASE_URL}${API_PREFIX}/fulltext-screening/tasks/${taskId}/export`
|
||||
);
|
||||
|
||||
if (exportResponse.status !== 200) {
|
||||
throw new Error(`导出Excel失败: ${exportResponse.statusText}`);
|
||||
}
|
||||
|
||||
const buffer = await exportResponse.arrayBuffer();
|
||||
const outputDir = path.join(process.cwd(), 'test-output');
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const filename = `fulltext_screening_e2e_test_${timestamp}.xlsx`;
|
||||
const filepath = path.join(outputDir, filename);
|
||||
|
||||
fs.writeFileSync(filepath, Buffer.from(buffer));
|
||||
|
||||
console.log(` ✅ Excel导出成功`);
|
||||
console.log(` ✅ 文件路径: ${filepath}`);
|
||||
console.log(` ✅ 文件大小: ${(buffer.byteLength / 1024).toFixed(2)} KB\n`);
|
||||
|
||||
// ==================== 测试总结 ====================
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('✅ 端到端集成测试完成!\n');
|
||||
|
||||
console.log('📊 测试总结:');
|
||||
console.log(` ✅ 项目创建: 成功`);
|
||||
console.log(` ✅ 文献导入: ${literatureIds.length}篇`);
|
||||
console.log(` ✅ 任务创建: 成功`);
|
||||
console.log(` ✅ LLM处理: ${summary?.totalResults || 0}篇完成`);
|
||||
console.log(` ✅ Excel导出: 成功`);
|
||||
console.log('');
|
||||
|
||||
console.log('💰 成本统计:');
|
||||
const finalProgress = await fetchJSON(
|
||||
`${BASE_URL}${API_PREFIX}/fulltext-screening/tasks/${taskId}`
|
||||
);
|
||||
const finalStats = finalProgress.data.data.statistics;
|
||||
console.log(` - 总Token: ${finalStats.totalTokens.toLocaleString()}`);
|
||||
console.log(` - 总成本: ¥${finalStats.totalCost.toFixed(4)}`);
|
||||
console.log(` - 平均成本/篇: ¥${(finalStats.totalCost / literatureIds.length).toFixed(4)}`);
|
||||
console.log('');
|
||||
|
||||
console.log('📁 输出文件:');
|
||||
console.log(` - Excel报告: ${filepath}`);
|
||||
console.log('');
|
||||
|
||||
console.log('🔗 相关链接:');
|
||||
console.log(` - 项目ID: ${projectId}`);
|
||||
console.log(` - 任务ID: ${taskId}`);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
console.error('\n详细错误:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('🧪 全文复筛端到端集成测试');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
runE2ETest().catch((error) => {
|
||||
console.error('测试运行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
###
|
||||
# 全文复筛API测试
|
||||
# 使用REST Client插件运行(VS Code)
|
||||
###
|
||||
|
||||
@baseUrl = http://localhost:3001
|
||||
@apiPrefix = /api/v1/asl/fulltext-screening
|
||||
|
||||
### ========================================
|
||||
### 准备工作:获取已有项目和文献
|
||||
### ========================================
|
||||
|
||||
### 1. 获取项目列表
|
||||
GET {{baseUrl}}/api/v1/asl/projects
|
||||
Content-Type: application/json
|
||||
|
||||
### 2. 获取项目文献列表(替换projectId)
|
||||
@projectId = 55941145-bba0-4b15-bda4-f0a398d78208
|
||||
GET {{baseUrl}}/api/v1/asl/projects/{{projectId}}/literatures?page=1&limit=10
|
||||
Content-Type: application/json
|
||||
|
||||
### ========================================
|
||||
### API 1: 创建全文复筛任务
|
||||
### ========================================
|
||||
|
||||
### 测试1.1: 创建任务(正常情况)
|
||||
# @name createTask
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "{{projectId}}",
|
||||
"literatureIds": [
|
||||
"e9c18ba3-9ad7-4cc9-ac78-b74a7ec91b12",
|
||||
"e44ea8d9-6ba8-4b88-8d24-4fb46b3584e0"
|
||||
],
|
||||
"modelA": "deepseek-v3",
|
||||
"modelB": "qwen-max",
|
||||
"promptVersion": "v1.0.0"
|
||||
}
|
||||
|
||||
### 保存taskId
|
||||
@taskId = {{createTask.response.body.data.taskId}}
|
||||
|
||||
### 测试1.2: 创建任务(缺少必填参数)
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "{{projectId}}"
|
||||
}
|
||||
|
||||
### 测试1.3: 创建任务(projectId不存在)
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "00000000-0000-0000-0000-000000000000",
|
||||
"literatureIds": ["lit-001"]
|
||||
}
|
||||
|
||||
### 测试1.4: 创建任务(literatureIds为空)
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "{{projectId}}",
|
||||
"literatureIds": []
|
||||
}
|
||||
|
||||
### ========================================
|
||||
### API 2: 获取任务进度
|
||||
### ========================================
|
||||
|
||||
### 测试2.1: 获取任务进度(正常情况)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试2.2: 获取任务进度(taskId不存在)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/00000000-0000-0000-0000-000000000000
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试2.3: 等待5秒后再次查询进度
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}
|
||||
Content-Type: application/json
|
||||
|
||||
### ========================================
|
||||
### API 3: 获取任务结果
|
||||
### ========================================
|
||||
|
||||
### 测试3.1: 获取所有结果(默认参数)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.2: 获取所有结果(第一页,每页10条)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?page=1&pageSize=10
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.3: 仅获取冲突项
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?filter=conflict
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.4: 仅获取待审核项
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?filter=pending
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.5: 仅获取已审核项
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?filter=reviewed
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.6: 按优先级降序排序
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?sortBy=priority&sortOrder=desc
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.7: 按创建时间升序排序
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?sortBy=createdAt&sortOrder=asc
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.8: 分页测试(第2页)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?page=2&pageSize=5
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.9: 无效的filter参数
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?filter=invalid
|
||||
Content-Type: application/json
|
||||
|
||||
### 测试3.10: 无效的pageSize(超过最大值)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?pageSize=999
|
||||
Content-Type: application/json
|
||||
|
||||
### ========================================
|
||||
### API 4: 人工审核决策
|
||||
### ========================================
|
||||
|
||||
### 先获取一个结果ID
|
||||
# @name getFirstResult
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/results?pageSize=1
|
||||
Content-Type: application/json
|
||||
|
||||
### 保存resultId
|
||||
@resultId = {{getFirstResult.response.body.data.results[0].resultId}}
|
||||
|
||||
### 测试4.1: 更新决策为纳入
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/{{resultId}}/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "include",
|
||||
"reviewNotes": "经人工审核,确认纳入"
|
||||
}
|
||||
|
||||
### 测试4.2: 更新决策为排除(带排除原因)
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/{{resultId}}/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "exclude",
|
||||
"exclusionReason": "关键字段field9(结局指标)数据不完整",
|
||||
"reviewNotes": "仅报告P值,缺少均值±SD"
|
||||
}
|
||||
|
||||
### 测试4.3: 排除但不提供排除原因(应该失败)
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/{{resultId}}/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "exclude"
|
||||
}
|
||||
|
||||
### 测试4.4: 无效的finalDecision值
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/{{resultId}}/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "maybe"
|
||||
}
|
||||
|
||||
### 测试4.5: resultId不存在
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/00000000-0000-0000-0000-000000000000/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "include"
|
||||
}
|
||||
|
||||
### ========================================
|
||||
### API 5: 导出Excel
|
||||
### ========================================
|
||||
|
||||
### 测试5.1: 导出Excel(正常情况)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{taskId}}/export
|
||||
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
### 测试5.2: 导出Excel(taskId不存在)
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/00000000-0000-0000-0000-000000000000/export
|
||||
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
### ========================================
|
||||
### 完整流程测试
|
||||
### ========================================
|
||||
|
||||
### 完整流程1: 创建任务
|
||||
# @name fullFlowTask
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "{{projectId}}",
|
||||
"literatureIds": [
|
||||
"e9c18ba3-9ad7-4cc9-ac78-b74a7ec91b12"
|
||||
],
|
||||
"modelA": "deepseek-v3",
|
||||
"modelB": "qwen-max"
|
||||
}
|
||||
|
||||
@fullFlowTaskId = {{fullFlowTask.response.body.data.taskId}}
|
||||
|
||||
### 完整流程2: 等待2秒后查询进度
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{fullFlowTaskId}}
|
||||
Content-Type: application/json
|
||||
|
||||
### 完整流程3: 获取结果
|
||||
# @name fullFlowResults
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{fullFlowTaskId}}/results
|
||||
Content-Type: application/json
|
||||
|
||||
@fullFlowResultId = {{fullFlowResults.response.body.data.results[0].resultId}}
|
||||
|
||||
### 完整流程4: 审核决策
|
||||
PUT {{baseUrl}}{{apiPrefix}}/results/{{fullFlowResultId}}/decision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"finalDecision": "include",
|
||||
"reviewNotes": "完整流程测试 - 确认纳入"
|
||||
}
|
||||
|
||||
### 完整流程5: 导出Excel
|
||||
GET {{baseUrl}}{{apiPrefix}}/tasks/{{fullFlowTaskId}}/export
|
||||
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
### ========================================
|
||||
### 压力测试(批量文献)
|
||||
### ========================================
|
||||
|
||||
### 批量测试: 创建包含多篇文献的任务
|
||||
POST {{baseUrl}}{{apiPrefix}}/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"projectId": "{{projectId}}",
|
||||
"literatureIds": [
|
||||
"e9c18ba3-9ad7-4cc9-ac78-b74a7ec91b12",
|
||||
"e44ea8d9-6ba8-4b88-8d24-4fb46b3584e0",
|
||||
"c8f9e2d1-3a4b-5c6d-7e8f-9a0b1c2d3e4f"
|
||||
],
|
||||
"modelA": "deepseek-v3",
|
||||
"modelB": "qwen-max"
|
||||
}
|
||||
|
||||
### ========================================
|
||||
### 清理测试数据(可选)
|
||||
### ========================================
|
||||
|
||||
### 注意:以下操作会删除测试数据,请谨慎使用
|
||||
|
||||
### 查询所有任务
|
||||
GET {{baseUrl}}/api/v1/asl/projects/{{projectId}}/literatures
|
||||
Content-Type: application/json
|
||||
|
||||
###
|
||||
|
||||
Reference in New Issue
Block a user