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