Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
278 lines
11 KiB
TypeScript
278 lines
11 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 } 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 { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.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'], // 允许的请求头
|
||
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: 10 * 1024 * 1024, // 10MB
|
||
},
|
||
});
|
||
console.log('✅ 文件上传插件已配置: 最大文件大小 10MB');
|
||
|
||
|
||
// ============================================
|
||
// 【平台基础设施】健康检查路由
|
||
// ============================================
|
||
// 注册健康检查路由(/health, /health/liveness, /health/readiness)
|
||
await registerHealthRoutes(fastify);
|
||
logger.info('✅ 健康检查路由已注册');
|
||
|
||
// ============================================
|
||
// 【平台基础设施】认证模块
|
||
// ============================================
|
||
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';
|
||
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
|
||
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
|
||
await fastify.register(userRoutes, { prefix: '/api/admin/users' });
|
||
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users');
|
||
|
||
// ============================================
|
||
// 【临时】平台基础设施测试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');
|
||
|
||
// ============================================
|
||
// 【业务模块】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');
|
||
|
||
// 启动服务器
|
||
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');
|
||
|
||
// 注册DC提取Workers
|
||
registerExtractionWorkers();
|
||
logger.info('✅ DC extraction workers registered');
|
||
|
||
// 注册DC Tool C Excel解析Worker
|
||
registerParseExcelWorker();
|
||
logger.info('✅ DC Tool C parse excel worker registered');
|
||
|
||
// 注册RVW审稿Worker
|
||
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();
|