Files
AIclinicalresearch/backend/src/modules/dc/tool-b/controllers/ExtractionController.ts
HaHafeng d4d33528c7 feat(dc): Complete Phase 1 - Portal workbench page development
Summary:
- Implement DC module Portal page with 3 tool cards
- Create ToolCard component with decorative background and hover animations
- Implement TaskList component with table layout and progress bars
- Implement AssetLibrary component with tab switching and file cards
- Complete database verification (4 tables confirmed)
- Complete backend API verification (6 endpoints ready)
- Optimize UI to match prototype design (V2.html)

Frontend Components (~715 lines):
- components/ToolCard.tsx - Tool cards with animations
- components/TaskList.tsx - Recent tasks table view
- components/AssetLibrary.tsx - Data asset library with tabs
- hooks/useRecentTasks.ts - Task state management
- hooks/useAssets.ts - Asset state management
- pages/Portal.tsx - Main portal page
- types/portal.ts - TypeScript type definitions

Backend Verification:
- Backend API: 1495 lines code verified
- Database: dc_schema with 4 tables verified
- API endpoints: 6 endpoints tested (templates API works)

Documentation:
- Database verification report
- Backend API test report
- Phase 1 completion summary
- UI optimization report
- Development task checklist
- Development plan for Tool B

Status: Phase 1 completed (100%), ready for browser testing
Next: Phase 2 - Tool B Step 1 and 2 development
2025-12-02 21:53:24 +08:00

392 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DC模块 - 提取控制器
*
* API端点
* - POST /api/v1/dc/tool-b/health-check - 健康检查
* - GET /api/v1/dc/tool-b/templates - 获取模板列表
* - POST /api/v1/dc/tool-b/tasks - 创建提取任务
* - GET /api/v1/dc/tool-b/tasks/:taskId/progress - 查询任务进度
* - GET /api/v1/dc/tool-b/tasks/:taskId/items - 获取验证网格数据
* - POST /api/v1/dc/tool-b/items/:itemId/resolve - 裁决冲突
*
* 平台能力复用:
* - ✅ logger: 日志记录
* - ✅ prisma: 数据库操作
* - ✅ storage: 文件操作
* - ✅ jobQueue: 异步任务
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { healthCheckService } from '../services/HealthCheckService.js';
import { templateService } from '../services/TemplateService.js';
import { dualModelExtractionService } from '../services/DualModelExtractionService.js';
import { conflictDetectionService } from '../services/ConflictDetectionService.js';
import { storage } from '../../../../common/storage/index.js';
import { logger } from '../../../../common/logging/index.js';
import { prisma } from '../../../../config/database.js';
import * as xlsx from 'xlsx';
export class ExtractionController {
/**
* 健康检查
* POST /health-check
*/
async healthCheck(request: FastifyRequest<{
Body: {
fileKey: string;
columnName: string;
}
}>, reply: FastifyReply) {
try {
const { fileKey, columnName } = request.body;
const userId = (request as any).userId || 'default-user'; // TODO: 从auth middleware获取
logger.info('[API] Health check request', { fileKey, columnName, userId });
const result = await healthCheckService.check(fileKey, columnName, userId);
return reply.code(200).send({
success: true,
data: result
});
} catch (error) {
logger.error('[API] Health check failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
/**
* 获取模板列表
* GET /templates
*/
async getTemplates(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('[API] Get templates request');
const templates = await templateService.getAllTemplates();
return reply.code(200).send({
success: true,
data: { templates }
});
} catch (error) {
logger.error('[API] Get templates failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
/**
* 创建提取任务
* POST /tasks
*/
async createTask(request: FastifyRequest<{
Body: {
projectName: string;
sourceFileKey: string;
textColumn: string;
diseaseType: string;
reportType: string;
modelA?: string;
modelB?: string;
}
}>, reply: FastifyReply) {
try {
const {
projectName,
sourceFileKey,
textColumn,
diseaseType,
reportType,
modelA = 'deepseek-v3',
modelB = 'qwen-max'
} = request.body;
const userId = (request as any).userId || 'default-user';
logger.info('[API] Create task request', {
userId,
projectName,
diseaseType,
reportType
});
// 1. 获取模板
const template = await templateService.getTemplate(diseaseType, reportType);
if (!template) {
return reply.code(404).send({
success: false,
error: `Template not found: ${diseaseType}/${reportType}`
});
}
// 2. 读取Excel文件创建items
const fileBuffer = await storage.download(sourceFileKey);
if (!fileBuffer) {
return reply.code(404).send({
success: false,
error: `File not found: ${sourceFileKey}`
});
}
const workbook = xlsx.read(fileBuffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const data = xlsx.utils.sheet_to_json<Record<string, any>>(worksheet);
if (!data[0].hasOwnProperty(textColumn)) {
return reply.code(400).send({
success: false,
error: `Column '${textColumn}' not found in Excel`
});
}
// 3. 创建任务
const task = await prisma.dCExtractionTask.create({
data: {
userId,
projectName,
sourceFileKey,
textColumn,
diseaseType,
reportType,
targetFields: template.fields,
modelA,
modelB,
totalCount: data.length,
status: 'pending'
}
});
// 4. 创建items
const itemsData = data.map((row, index) => ({
taskId: task.id,
rowIndex: index + 1,
originalText: String(row[textColumn] || '')
}));
await prisma.dCExtractionItem.createMany({
data: itemsData
});
// 5. 启动异步任务
// TODO: 使用jobQueue.add()
// 暂时直接调用
dualModelExtractionService.batchExtract(task.id).catch(err => {
logger.error('[API] Batch extraction failed', { error: err, taskId: task.id });
});
logger.info('[API] Task created', { taskId: task.id, itemCount: data.length });
return reply.code(201).send({
success: true,
data: {
taskId: task.id,
totalCount: data.length,
status: 'pending'
}
});
} catch (error) {
logger.error('[API] Create task failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
/**
* 查询任务进度
* GET /tasks/:taskId/progress
*/
async getTaskProgress(request: FastifyRequest<{
Params: { taskId: string }
}>, reply: FastifyReply) {
try {
const { taskId } = request.params;
logger.info('[API] Get task progress', { taskId });
const task = await prisma.dCExtractionTask.findUnique({
where: { id: taskId }
});
if (!task) {
return reply.code(404).send({
success: false,
error: 'Task not found'
});
}
return reply.code(200).send({
success: true,
data: {
taskId: task.id,
status: task.status,
totalCount: task.totalCount,
processedCount: task.processedCount,
cleanCount: task.cleanCount,
conflictCount: task.conflictCount,
failedCount: task.failedCount,
totalTokens: task.totalTokens,
totalCost: task.totalCost,
progress: task.totalCount > 0 ? Math.round((task.processedCount / task.totalCount) * 100) : 0
}
});
} catch (error) {
logger.error('[API] Get task progress failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
/**
* 获取验证网格数据
* GET /tasks/:taskId/items
*/
async getTaskItems(request: FastifyRequest<{
Params: { taskId: string };
Querystring: { page?: string; limit?: string; status?: string }
}>, reply: FastifyReply) {
try {
const { taskId } = request.params;
const page = parseInt(request.query.page || '1');
const limit = parseInt(request.query.limit || '50');
const statusFilter = request.query.status;
logger.info('[API] Get task items', { taskId, page, limit, statusFilter });
const where: any = { taskId };
if (statusFilter) {
where.status = statusFilter;
}
const [items, total] = await Promise.all([
prisma.dCExtractionItem.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { rowIndex: 'asc' }
}),
prisma.dCExtractionItem.count({ where })
]);
return reply.code(200).send({
success: true,
data: {
items: items.map(item => ({
id: item.id,
rowIndex: item.rowIndex,
originalText: item.originalText,
resultA: item.resultA,
resultB: item.resultB,
status: item.status,
conflictFields: item.conflictFields,
finalResult: item.finalResult
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
});
} catch (error) {
logger.error('[API] Get task items failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
/**
* 裁决冲突
* POST /items/:itemId/resolve
*/
async resolveConflict(request: FastifyRequest<{
Params: { itemId: string };
Body: {
field: string;
chosenValue: string;
}
}>, reply: FastifyReply) {
try {
const { itemId } = request.params;
const { field, chosenValue } = request.body;
logger.info('[API] Resolve conflict', { itemId, field });
// 获取当前记录
const item = await prisma.dCExtractionItem.findUnique({
where: { id: itemId }
});
if (!item) {
return reply.code(404).send({
success: false,
error: 'Item not found'
});
}
// 更新finalResult
const finalResult = { ...(item.finalResult as Record<string, string> || {}) };
finalResult[field] = chosenValue;
// 移除已解决的冲突字段
const conflictFields = item.conflictFields.filter(f => f !== field);
// 更新状态
const newStatus = conflictFields.length === 0 ? 'resolved' : 'conflict';
await prisma.dCExtractionItem.update({
where: { id: itemId },
data: {
finalResult,
conflictFields,
status: newStatus,
resolvedAt: conflictFields.length === 0 ? new Date() : null
}
});
logger.info('[API] Conflict resolved', { itemId, field, newStatus });
return reply.code(200).send({
success: true,
data: {
itemId,
status: newStatus,
remainingConflicts: conflictFields.length
}
});
} catch (error) {
logger.error('[API] Resolve conflict failed', { error });
return reply.code(500).send({
success: false,
error: String(error)
});
}
}
}
// 导出单例
export const extractionController = new ExtractionController();