Deliver the RVW V4.0 journal configuration center across backend, frontend, migration, and docs with zh/en editorial baseline support and tenant-level prompt/template overrides. Unify tenant login to /:tenantCode/login and auto-enable RVW module when tenant type is JOURNAL to prevent post-login access gaps. Made-with: Cursor
390 lines
16 KiB
TypeScript
390 lines
16 KiB
TypeScript
import Fastify from 'fastify';
|
||
import cors from '@fastify/cors';
|
||
import multipart from '@fastify/multipart';
|
||
import { config, validateEnv } from './config/env.js';
|
||
import { testDatabaseConnection, prisma } from './config/database.js';
|
||
import { projectRoutes } from './legacy/routes/projects.js';
|
||
import { agentRoutes } from './legacy/routes/agents.js';
|
||
import { conversationRoutes } from './legacy/routes/conversations.js';
|
||
import knowledgeBaseRoutes from './legacy/routes/knowledgeBases.js';
|
||
import { chatRoutes } from './legacy/routes/chatRoutes.js';
|
||
import { batchRoutes } from './legacy/routes/batchRoutes.js';
|
||
import reviewRoutes from './legacy/routes/reviewRoutes.js';
|
||
import { rvwRoutes } from './modules/rvw/index.js';
|
||
import { aslRoutes } from './modules/asl/routes/index.js';
|
||
import { registerDCRoutes, initDCModule } from './modules/dc/index.js';
|
||
import pkbRoutes from './modules/pkb/routes/index.js';
|
||
import { aiaRoutes } from './modules/aia/index.js';
|
||
import { registerHealthRoutes, markShuttingDown } from './common/health/index.js';
|
||
import { logger } from './common/logging/index.js';
|
||
import { authRoutes, registerAuthPlugin } from './common/auth/index.js';
|
||
import { promptRoutes } from './common/prompt/index.js';
|
||
import { registerTestRoutes } from './test-platform-api.js';
|
||
import { registerScreeningWorkers } from './modules/asl/services/screeningWorker.js';
|
||
import { registerResearchWorker } from './modules/asl/workers/researchWorker.js';
|
||
import { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.js';
|
||
import { registerExtractionWorkers as registerAslExtractionWorkers } from './modules/asl/extraction/workers/index.js';
|
||
import { registerParseExcelWorker } from './modules/dc/tool-c/workers/parseExcelWorker.js';
|
||
import { registerReviewWorker } from './modules/rvw/workers/reviewWorker.js';
|
||
import { jobQueue } from './common/jobs/index.js';
|
||
|
||
|
||
// 全局处理BigInt序列化
|
||
(BigInt.prototype as any).toJSON = function() {
|
||
return Number(this);
|
||
};
|
||
|
||
// 生产环境使用JSON格式日志(性能更好),开发环境使用pino-pretty(易读)
|
||
const fastify = Fastify({
|
||
logger: config.nodeEnv === 'production'
|
||
? {
|
||
level: config.logLevel,
|
||
// 生产环境:简单的JSON日志,适合日志收集系统
|
||
}
|
||
: {
|
||
level: config.logLevel,
|
||
// 开发环境:使用pino-pretty美化输出
|
||
transport: {
|
||
target: 'pino-pretty',
|
||
options: {
|
||
colorize: true,
|
||
translateTime: 'HH:MM:ss Z',
|
||
ignore: 'pid,hostname',
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// 注册CORS插件 - 完整配置
|
||
await fastify.register(cors, {
|
||
origin: true, // 开发环境允许所有来源
|
||
credentials: true,
|
||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'], // 明确允许的HTTP方法
|
||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin', 'x-tenant-id'], // 允许的请求头(含 RVW V4.0 多租户 Header)
|
||
exposedHeaders: ['Content-Range', 'X-Content-Range'], // 暴露的响应头
|
||
maxAge: 600, // preflight请求缓存时间(秒)
|
||
preflightContinue: false, // Fastify处理preflight请求
|
||
});
|
||
console.log('✅ CORS已配置: 允许所有HTTP方法 (GET, POST, PUT, DELETE, PATCH, OPTIONS)');
|
||
|
||
// 注册文件上传插件
|
||
await fastify.register(multipart, {
|
||
limits: {
|
||
fileSize: 50 * 1024 * 1024, // 50MB(与前端提示一致)
|
||
},
|
||
});
|
||
console.log('✅ 文件上传插件已配置: 最大文件大小 10MB');
|
||
|
||
|
||
// ============================================
|
||
// 【平台基础设施】健康检查路由
|
||
// ============================================
|
||
// 注册健康检查路由(/health, /health/liveness, /health/readiness)
|
||
await registerHealthRoutes(fastify);
|
||
logger.info('✅ 健康检查路由已注册');
|
||
|
||
// ============================================
|
||
// 【公开API】无需认证的公共接口
|
||
// ============================================
|
||
import { getTenantLoginConfig } from './modules/admin/controllers/tenantController.js';
|
||
fastify.get('/api/v1/public/tenant-config/:tenantCode', getTenantLoginConfig);
|
||
logger.info('✅ 公开API已注册: /api/v1/public/tenant-config/:tenantCode');
|
||
|
||
// ============================================
|
||
// 【平台基础设施】认证模块
|
||
// ============================================
|
||
await registerAuthPlugin(fastify);
|
||
await fastify.register(authRoutes, { prefix: '/api/v1/auth' });
|
||
logger.info('✅ 认证路由已注册: /api/v1/auth');
|
||
|
||
// ============================================
|
||
// 【运营管理】Prompt管理模块
|
||
// ============================================
|
||
await fastify.register(promptRoutes, { prefix: '/api/admin/prompts' });
|
||
logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
|
||
|
||
// ============================================
|
||
// 【运营管理】租户管理模块
|
||
// ============================================
|
||
import { tenantRoutes, moduleRoutes } from './modules/admin/routes/tenantRoutes.js';
|
||
import { userRoutes } from './modules/admin/routes/userRoutes.js';
|
||
import { statsRoutes, userOverviewRoute } from './modules/admin/routes/statsRoutes.js';
|
||
import { systemKbRoutes } from './modules/admin/system-kb/index.js';
|
||
import { iitProjectRoutes, iitQcRuleRoutes, iitUserMappingRoutes, iitBatchRoutes, iitQcCockpitRoutes, iitEqueryRoutes } from './modules/admin/iit-projects/index.js';
|
||
import { rvwConfigRoutes } from './modules/admin/rvw-config/rvwConfigRoutes.js';
|
||
import { journalConfigRoutes } from './modules/admin/journal-config/journalConfigRoutes.js';
|
||
import { authenticate, requireRoles } from './common/auth/auth.middleware.js';
|
||
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
|
||
await fastify.register(rvwConfigRoutes, { prefix: '/api/admin/tenants' });
|
||
await fastify.register(journalConfigRoutes, { prefix: '/api/admin/journal-configs' });
|
||
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
|
||
await fastify.register(userRoutes, { prefix: '/api/admin/users' });
|
||
await fastify.register(statsRoutes, { prefix: '/api/admin/stats' });
|
||
await fastify.register(userOverviewRoute, { prefix: '/api/admin/users' });
|
||
await fastify.register(systemKbRoutes, { prefix: '/api/v1/admin/system-kb' });
|
||
|
||
// IIT 项目管理路由 — 认证 + 角色守卫(SUPER_ADMIN, PROMPT_ENGINEER, IIT_OPERATOR, PHARMA_ADMIN, HOSPITAL_ADMIN 可访问)
|
||
await fastify.register(async (scope) => {
|
||
scope.addHook('preHandler', authenticate);
|
||
scope.addHook('preHandler', requireRoles('SUPER_ADMIN', 'PROMPT_ENGINEER', 'IIT_OPERATOR', 'PHARMA_ADMIN', 'HOSPITAL_ADMIN'));
|
||
await scope.register(iitProjectRoutes);
|
||
await scope.register(iitQcRuleRoutes);
|
||
await scope.register(iitUserMappingRoutes);
|
||
await scope.register(iitBatchRoutes);
|
||
await scope.register(iitQcCockpitRoutes);
|
||
await scope.register(iitEqueryRoutes);
|
||
}, { prefix: '/api/v1/admin/iit-projects' });
|
||
|
||
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/journal-configs, /api/admin/modules, /api/admin/users, /api/admin/stats, /api/v1/admin/system-kb, /api/v1/admin/iit-projects (authenticated)');
|
||
|
||
// ============================================
|
||
// 【临时】平台基础设施测试API
|
||
// ============================================
|
||
await registerTestRoutes(fastify);
|
||
logger.info('✅ 测试API已注册: /test/platform');
|
||
|
||
// API路由前缀
|
||
fastify.get('/api/v1', async () => {
|
||
return {
|
||
message: 'AI Clinical Research Platform API',
|
||
version: '1.0.0',
|
||
environment: config.nodeEnv,
|
||
};
|
||
});
|
||
|
||
// 注册项目管理路由
|
||
await fastify.register(projectRoutes, { prefix: '/api/v1' });
|
||
|
||
// 注册智能体管理路由
|
||
await fastify.register(agentRoutes, { prefix: '/api/v1' });
|
||
|
||
// 注册对话管理路由
|
||
await fastify.register(conversationRoutes, { prefix: '/api/v1' });
|
||
|
||
// 注册知识库管理路由
|
||
await fastify.register(knowledgeBaseRoutes, { prefix: '/api/v1' });
|
||
|
||
// 注册通用对话路由
|
||
await fastify.register(chatRoutes, { prefix: '/api/v1' });
|
||
|
||
// Phase 3: 注册批处理路由
|
||
await fastify.register(batchRoutes, { prefix: '/api/v1' });
|
||
|
||
// 注册稿件审查路由(旧版,保留兼容)
|
||
await fastify.register(reviewRoutes, { prefix: '/api/v1' });
|
||
|
||
// ============================================
|
||
// 【业务模块】RVW - 稿件审查系统
|
||
// ============================================
|
||
await fastify.register(rvwRoutes, { prefix: '/api/v1/rvw' });
|
||
logger.info('✅ RVW稿件审查路由已注册: /api/v1/rvw');
|
||
|
||
// ============================================
|
||
// 【业务模块】PKB - 个人知识库
|
||
// ============================================
|
||
await fastify.register(pkbRoutes, { prefix: '/api/v1/pkb' });
|
||
logger.info('✅ PKB个人知识库路由已注册: /api/v1/pkb');
|
||
|
||
// ============================================
|
||
// 【业务模块】AIA - AI智能问答
|
||
// ============================================
|
||
await fastify.register(aiaRoutes, { prefix: '/api/v1/aia' });
|
||
logger.info('✅ AIA智能问答路由已注册: /api/v1/aia');
|
||
|
||
// ============================================
|
||
// 【业务模块】Protocol Agent - 研究方案制定Agent
|
||
// ============================================
|
||
import { protocolAgentRoutes } from './modules/agent/protocol/index.js';
|
||
await fastify.register((instance, opts, done) => {
|
||
protocolAgentRoutes(instance, { prisma, ...opts }).then(() => done()).catch(done);
|
||
}, { prefix: '/api/v1/aia/protocol-agent' });
|
||
logger.info('✅ Protocol Agent路由已注册: /api/v1/aia/protocol-agent');
|
||
|
||
// ============================================
|
||
// 【业务模块】ASL - AI智能文献筛选
|
||
// ============================================
|
||
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });
|
||
logger.info('✅ ASL智能文献筛选路由已注册: /api/v1/asl');
|
||
|
||
// ============================================
|
||
// 【业务模块】DC - 数据清洗整理
|
||
// ============================================
|
||
await registerDCRoutes(fastify);
|
||
logger.info('✅ DC数据清洗模块路由已注册: /api/v1/dc/tool-b');
|
||
|
||
// ============================================
|
||
// 【业务模块】IIT Manager Agent - IIT研究智能助手
|
||
// ============================================
|
||
import { registerIitRoutes, initIitManager } from './modules/iit-manager/index.js';
|
||
await registerIitRoutes(fastify);
|
||
logger.info('✅ IIT Manager Agent路由已注册: /api/v1/iit');
|
||
|
||
// ============================================
|
||
// 【业务模块】SSA - 智能统计分析
|
||
// ============================================
|
||
import { ssaRoutes } from './modules/ssa/index.js';
|
||
await fastify.register(ssaRoutes, { prefix: '/api/v1/ssa' });
|
||
logger.info('✅ SSA智能统计分析路由已注册: /api/v1/ssa');
|
||
|
||
// ============================================
|
||
// 【集成模块】Legacy Bridge - 旧系统集成桥接
|
||
// ============================================
|
||
import { legacyBridgeRoutes } from './modules/legacy-bridge/routes.js';
|
||
await fastify.register(legacyBridgeRoutes, { prefix: '/api/v1/legacy' });
|
||
logger.info('✅ Legacy Bridge路由已注册: /api/v1/legacy');
|
||
|
||
// 启动服务器
|
||
const start = async () => {
|
||
try {
|
||
// 验证环境变量
|
||
validateEnv();
|
||
|
||
// 测试数据库连接
|
||
console.log('🔍 正在测试数据库连接...');
|
||
const dbConnected = await testDatabaseConnection();
|
||
|
||
if (!dbConnected) {
|
||
console.error('❌ 数据库连接失败,无法启动服务器');
|
||
process.exit(1);
|
||
}
|
||
|
||
// ============================================
|
||
// 【Postgres-Only】启动队列和注册Workers
|
||
// ============================================
|
||
try {
|
||
logger.info('🚀 Starting Postgres-Only queue and workers...');
|
||
|
||
// 启动队列(pg-boss)
|
||
await jobQueue.start();
|
||
logger.info('✅ Queue started (pg-boss)');
|
||
|
||
// 注册ASL筛选Workers
|
||
registerScreeningWorkers();
|
||
logger.info('✅ ASL screening workers registered');
|
||
|
||
// 注册ASL智能文献检索Worker
|
||
registerResearchWorker();
|
||
logger.info('✅ ASL research worker registered');
|
||
|
||
// 注册DC提取Workers
|
||
registerExtractionWorkers();
|
||
logger.info('✅ DC extraction workers registered');
|
||
|
||
// 注册DC Tool C Excel解析Worker
|
||
registerParseExcelWorker();
|
||
logger.info('✅ DC Tool C parse excel worker registered');
|
||
|
||
// 注册ASL工具3全文提取Workers(散装派发 + Aggregator)
|
||
await registerAslExtractionWorkers();
|
||
logger.info('✅ ASL extraction workers registered (Tool 3)');
|
||
|
||
// 注册RVW审稿Worker(包含启动时清理卡住任务)
|
||
await registerReviewWorker();
|
||
logger.info('✅ RVW review worker registered');
|
||
|
||
// 注册IIT Manager Workers
|
||
await initIitManager();
|
||
logger.info('✅ IIT Manager workers registered');
|
||
|
||
// ⚠️ 等待3秒,确保所有 Worker 异步注册到 pg-boss 完成
|
||
console.log('\n⏳ 等待 Workers 异步注册完成...');
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
logger.info('✅ All workers registration completed (waited 3s)');
|
||
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('✅ Postgres-Only 架构已启动');
|
||
console.log('='.repeat(60));
|
||
console.log('📦 队列类型: pg-boss');
|
||
console.log('📦 缓存类型: PostgreSQL');
|
||
console.log('📦 注册的Workers:');
|
||
console.log(' - asl_screening_batch (文献筛选批次处理)');
|
||
console.log(' - dc_extraction_batch (数据提取批次处理)');
|
||
console.log(' - dc_toolc_parse_excel (Tool C Excel解析)');
|
||
console.log(' - rvw_review_task (稿件审查任务)');
|
||
console.log(' - iit_quality_check (IIT质控+企微推送)');
|
||
console.log(' - iit_redcap_poll (IIT REDCap轮询)');
|
||
console.log('='.repeat(60) + '\n');
|
||
} catch (error) {
|
||
logger.error('❌ Failed to start Postgres-Only architecture', { error });
|
||
console.error('❌ Postgres-Only 架构启动失败:', error);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 初始化DC模块(Seed预设模板)
|
||
try {
|
||
await initDCModule();
|
||
logger.info('✅ DC模块初始化成功');
|
||
} catch (error) {
|
||
logger.warn('⚠️ DC模块初始化失败,但不影响启动', { error });
|
||
}
|
||
|
||
// 启动Fastify服务器
|
||
await fastify.listen({
|
||
port: config.port,
|
||
host: config.host,
|
||
});
|
||
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('🚀 AI临床研究平台 - 后端服务器启动成功!');
|
||
console.log('='.repeat(60));
|
||
console.log(`📍 服务地址: http://${config.host === '0.0.0.0' ? 'localhost' : config.host}:${config.port}`);
|
||
console.log(`🔍 健康检查: http://localhost:${config.port}/health`);
|
||
console.log(`📡 API入口: http://localhost:${config.port}/api/v1`);
|
||
console.log(`🌍 运行环境: ${config.nodeEnv}`);
|
||
console.log('='.repeat(60) + '\n');
|
||
} catch (err) {
|
||
fastify.log.error(err);
|
||
process.exit(1);
|
||
}
|
||
};
|
||
|
||
start();
|
||
|
||
// ============================================
|
||
// 🛡️ 优雅关闭处理(Graceful Shutdown)
|
||
// ============================================
|
||
const SHUTDOWN_TIMEOUT_MS = 30_000;
|
||
|
||
const gracefulShutdown = async (signal: string) => {
|
||
console.log(`\n⚠️ 收到 ${signal} 信号,开始优雅关闭...`);
|
||
|
||
// 立即标记停机,健康检查返回 503,CLB 不再派发新请求
|
||
markShuttingDown();
|
||
console.log('🚫 健康检查已切换为 503,CLB 将停止路由新流量');
|
||
|
||
// 强制超时兜底:防止 SSE 长连接或死循环任务阻塞退出
|
||
const forceTimer = setTimeout(() => {
|
||
console.error(`❌ 优雅关闭超时 (${SHUTDOWN_TIMEOUT_MS / 1000}s),强制退出`);
|
||
process.exit(1);
|
||
}, SHUTDOWN_TIMEOUT_MS);
|
||
forceTimer.unref();
|
||
|
||
try {
|
||
// 1. 停止接收新请求(已有 SSE 连接继续跑完)
|
||
await fastify.close();
|
||
console.log('✅ HTTP 服务已停止');
|
||
|
||
// 2. 停止队列(等待当前任务完成)
|
||
await jobQueue.stop();
|
||
console.log('✅ 任务队列已停止');
|
||
|
||
// 3. 关闭数据库连接
|
||
await prisma.$disconnect();
|
||
console.log('✅ 数据库连接已关闭');
|
||
|
||
// 4. 关闭 Legacy MySQL 连接池
|
||
const { closeLegacyMysqlPool } = await import('./modules/legacy-bridge/mysql-pool.js');
|
||
await closeLegacyMysqlPool();
|
||
console.log('✅ Legacy MySQL连接池已关闭');
|
||
|
||
console.log('👋 优雅关闭完成,再见!');
|
||
process.exit(0);
|
||
} catch (error) {
|
||
console.error('❌ 优雅关闭失败:', error);
|
||
process.exit(1);
|
||
}
|
||
};
|
||
|
||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|