/** * 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>(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 || {}) }; 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();