feat(admin): Complete tenant management and module access control system
Major Features: - Tenant management CRUD (list, create, edit, delete, module configuration) - Dynamic module management system (modules table with 8 modules) - Multi-tenant module permission merging (ModuleService) - Module access control middleware (requireModule) - User module permission API (GET /api/v1/auth/me/modules) - Frontend module permission filtering (HomePage + TopNavigation) Module Integration: - RVW module integrated with PromptService (editorial + methodology) - All modules (RVW/PKB/ASL/DC) added authenticate + requireModule middleware - Fixed ReviewTask foreign key constraint (cross-schema issue) - Removed all MOCK_USER_ID, unified to request.user?.userId Prompt Management Enhancements: - Module names displayed in Chinese (RVW -> 智能审稿) - Enhanced version history with view content and rollback features - List page shows both activeVersion and draftVersion columns Database Changes: - Added platform_schema.modules table - Modified tenant_modules table (added index and UUID) - Removed ReviewTask foreign key to public.users (cross-schema fix) - Seeded 8 modules: RVW, PKB, ASL, DC, IIT, AIA, SSA, ST Documentation Updates: - Updated ADMIN module development status - Updated TODO checklist (89% progress) - Updated Prompt management plan (Phase 3.5.5 completed) - Added module authentication specification Files Changed: 80+ Status: All features tested and verified locally Next: User management module development
This commit is contained in:
@@ -102,16 +102,20 @@ model Conversation {
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(uuid())
|
||||
conversationId String @map("conversation_id")
|
||||
role String
|
||||
content String
|
||||
model String?
|
||||
metadata Json?
|
||||
tokens Int?
|
||||
isPinned Boolean @default(false) @map("is_pinned")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
conversation Conversation @relation("ConversationMessages", fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(uuid())
|
||||
conversationId String @map("conversation_id")
|
||||
role String
|
||||
content String
|
||||
model String?
|
||||
metadata Json?
|
||||
tokens Int?
|
||||
isPinned Boolean @default(false) @map("is_pinned")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
// V2.1 新增字段
|
||||
thinkingContent String? @map("thinking_content") @db.Text // 深度思考内容 <think>...</think>
|
||||
attachments Json? @db.JsonB // 附件数组(上限5个,单个≤20MB,提取文本≤30K tokens)
|
||||
|
||||
conversation Conversation @relation("ConversationMessages", fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([conversationId], map: "idx_aia_messages_conversation_id")
|
||||
@@index([createdAt], map: "idx_aia_messages_created_at")
|
||||
@@ -258,37 +262,8 @@ model AdminLog {
|
||||
@@schema("public")
|
||||
}
|
||||
|
||||
model GeneralConversation {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
title String
|
||||
modelName String? @map("model_name")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@index([createdAt], map: "idx_aia_general_conversations_created_at")
|
||||
@@index([updatedAt], map: "idx_aia_general_conversations_updated_at")
|
||||
@@index([userId], map: "idx_aia_general_conversations_user_id")
|
||||
@@map("general_conversations")
|
||||
@@schema("aia_schema")
|
||||
}
|
||||
|
||||
model GeneralMessage {
|
||||
id String @id @default(uuid())
|
||||
conversationId String @map("conversation_id")
|
||||
role String
|
||||
content String
|
||||
model String?
|
||||
metadata Json?
|
||||
tokens Int?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([conversationId], map: "idx_aia_general_messages_conversation_id")
|
||||
@@index([createdAt], map: "idx_aia_general_messages_created_at")
|
||||
@@map("general_messages")
|
||||
@@schema("aia_schema")
|
||||
}
|
||||
// GeneralConversation 和 GeneralMessage 已删除(2026-01-11)
|
||||
// 原因:与 Conversation/Message 功能重叠,使用 conversations.project_id = NULL 表示无项目对话
|
||||
|
||||
model ReviewTask {
|
||||
id String @id @default(uuid())
|
||||
@@ -316,7 +291,8 @@ model ReviewTask {
|
||||
errorMessage String? @map("error_message")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
user users @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
// 注意:userId 暂不添加外键约束,因为用户来自不同 schema (platform_schema.users)
|
||||
// 跨 schema 外键在 PostgreSQL 中需要特殊处理
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@ -799,7 +775,7 @@ model users {
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime
|
||||
adminLogs AdminLog[]
|
||||
reviewTasks ReviewTask[]
|
||||
// reviewTasks 已移除,因为 ReviewTask.userId 现在不再引用此表
|
||||
|
||||
@@index([created_at])
|
||||
@@index([email])
|
||||
@@ -1034,8 +1010,25 @@ model tenant_members {
|
||||
@@schema("platform_schema")
|
||||
}
|
||||
|
||||
/// 系统模块配置表(动态管理可用模块)
|
||||
model modules {
|
||||
code String @id // 模块代码: RVW, PKB, ASL, DC, IIT, AIA
|
||||
name String // 显示名称
|
||||
description String? // 模块描述
|
||||
icon String? // 图标(可选)
|
||||
is_active Boolean @default(true) // 是否上线
|
||||
sort_order Int @default(0) // 排序
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@index([is_active])
|
||||
@@index([sort_order])
|
||||
@@schema("platform_schema")
|
||||
}
|
||||
|
||||
/// 租户模块订阅表
|
||||
model tenant_modules {
|
||||
id String @id
|
||||
id String @id @default(uuid())
|
||||
tenant_id String
|
||||
module_code String
|
||||
is_enabled Boolean @default(true)
|
||||
@@ -1044,6 +1037,8 @@ model tenant_modules {
|
||||
tenants tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([tenant_id, module_code])
|
||||
@@index([tenant_id])
|
||||
@@index([module_code])
|
||||
@@schema("platform_schema")
|
||||
}
|
||||
|
||||
|
||||
206
backend/scripts/query-users.js
Normal file
206
backend/scripts/query-users.js
Normal file
@@ -0,0 +1,206 @@
|
||||
// 查询数据库用户信息
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('\n========== 平台用户 (platform_schema.users) ==========\n');
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
role: true,
|
||||
status: true,
|
||||
tenant_id: true,
|
||||
},
|
||||
orderBy: { role: 'asc' }
|
||||
});
|
||||
|
||||
console.log('用户列表:');
|
||||
console.table(users.map(u => ({
|
||||
ID: u.id.substring(0, 8) + '...',
|
||||
手机号: u.phone,
|
||||
姓名: u.name,
|
||||
角色: u.role,
|
||||
状态: u.status,
|
||||
})));
|
||||
|
||||
console.log('\n默认密码: 123456');
|
||||
|
||||
console.log('\n========== 角色权限 (platform_schema.role_permissions) ==========\n');
|
||||
|
||||
const rolePerms = await prisma.role_permissions.findMany({
|
||||
include: {
|
||||
permissions: true
|
||||
},
|
||||
orderBy: { role: 'asc' }
|
||||
});
|
||||
|
||||
const permsByRole = {};
|
||||
rolePerms.forEach(rp => {
|
||||
if (!permsByRole[rp.role]) {
|
||||
permsByRole[rp.role] = [];
|
||||
}
|
||||
permsByRole[rp.role].push(rp.permissions.code);
|
||||
});
|
||||
|
||||
console.log('角色权限:');
|
||||
Object.entries(permsByRole).forEach(([role, perms]) => {
|
||||
console.log(`\n${role}:`);
|
||||
perms.forEach(p => console.log(` - ${p}`));
|
||||
});
|
||||
|
||||
console.log('\n========== 租户模块配置 (platform_schema.tenant_modules) ==========\n');
|
||||
|
||||
const tenantModules = await prisma.tenant_modules.findMany({
|
||||
orderBy: [{ tenant_id: 'asc' }, { module_code: 'asc' }]
|
||||
});
|
||||
|
||||
if (tenantModules.length === 0) {
|
||||
console.log('⚠️ 尚未配置任何租户模块(所有用户可能默认访问所有模块)');
|
||||
} else {
|
||||
const modulesByTenant = {};
|
||||
tenantModules.forEach(tm => {
|
||||
if (!modulesByTenant[tm.tenant_id]) {
|
||||
modulesByTenant[tm.tenant_id] = [];
|
||||
}
|
||||
modulesByTenant[tm.tenant_id].push({
|
||||
module: tm.module_code,
|
||||
enabled: tm.is_enabled,
|
||||
expires: tm.expires_at
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(modulesByTenant).forEach(([tenantId, modules]) => {
|
||||
console.log(`\n租户 ${tenantId.substring(0, 8)}...:`);
|
||||
modules.forEach(m => {
|
||||
const status = m.enabled ? '✅' : '❌';
|
||||
const expiry = m.expires ? ` (到期: ${m.expires})` : '';
|
||||
console.log(` ${status} ${m.module}${expiry}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n========== 租户列表 (platform_schema.tenants) ==========\n');
|
||||
|
||||
const tenants = await prisma.tenants.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
type: true,
|
||||
status: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.table(tenants.map(t => ({
|
||||
ID: t.id.substring(0, 8) + '...',
|
||||
名称: t.name,
|
||||
代码: t.code,
|
||||
类型: t.type,
|
||||
状态: t.status,
|
||||
})));
|
||||
|
||||
console.log('\n========== 用户-租户关系 ==========\n');
|
||||
|
||||
const usersWithTenant = await prisma.user.findMany({
|
||||
select: {
|
||||
phone: true,
|
||||
name: true,
|
||||
role: true,
|
||||
tenant_id: true,
|
||||
tenants: {
|
||||
select: {
|
||||
name: true,
|
||||
code: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { role: 'asc' }
|
||||
});
|
||||
|
||||
console.table(usersWithTenant.map(u => ({
|
||||
手机号: u.phone,
|
||||
姓名: u.name,
|
||||
角色: u.role,
|
||||
租户: u.tenants?.name || 'N/A',
|
||||
租户代码: u.tenants?.code || 'N/A',
|
||||
})));
|
||||
}
|
||||
|
||||
console.log('\n========== 示范医院详情 ==========\n');
|
||||
|
||||
const demoHospital = await prisma.tenants.findFirst({
|
||||
where: { code: 'demo-hospital' },
|
||||
include: {
|
||||
tenant_modules: true,
|
||||
users: {
|
||||
select: { id: true, phone: true, name: true, role: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (demoHospital) {
|
||||
console.log('租户ID:', demoHospital.id);
|
||||
console.log('名称:', demoHospital.name);
|
||||
console.log('联系人:', demoHospital.contact_name);
|
||||
console.log('联系电话:', demoHospital.contact_phone);
|
||||
console.log('\n已开通模块:');
|
||||
demoHospital.tenant_modules.forEach(m => {
|
||||
console.log(` ${m.is_enabled ? '✅' : '❌'} ${m.module_code}`);
|
||||
});
|
||||
console.log('\n租户下的用户:');
|
||||
demoHospital.users.forEach(u => {
|
||||
console.log(` - ${u.phone} ${u.name} (${u.role})`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n========== 张主任用户信息 ==========\n');
|
||||
|
||||
const zhangUser = await prisma.user.findFirst({
|
||||
where: { phone: '13800138001' },
|
||||
include: {
|
||||
tenants: {
|
||||
include: {
|
||||
tenant_modules: true
|
||||
}
|
||||
},
|
||||
tenant_members: {
|
||||
include: {
|
||||
tenants: {
|
||||
include: {
|
||||
tenant_modules: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (zhangUser) {
|
||||
console.log('用户ID:', zhangUser.id);
|
||||
console.log('手机号:', zhangUser.phone);
|
||||
console.log('姓名:', zhangUser.name);
|
||||
console.log('角色:', zhangUser.role);
|
||||
console.log('主租户ID:', zhangUser.tenant_id);
|
||||
console.log('主租户名称:', zhangUser.tenants?.name);
|
||||
console.log('\n主租户模块配置:');
|
||||
zhangUser.tenants?.tenant_modules?.forEach(m => {
|
||||
console.log(` ${m.is_enabled ? '✅' : '❌'} ${m.module_code}`);
|
||||
});
|
||||
console.log('\n额外加入的租户:');
|
||||
zhangUser.tenant_members?.forEach(tm => {
|
||||
console.log(` - ${tm.tenants.name}`);
|
||||
tm.tenants.tenant_modules?.forEach(m => {
|
||||
console.log(` ${m.is_enabled ? '✅' : '❌'} ${m.module_code}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
116
backend/scripts/seed-modules.js
Normal file
116
backend/scripts/seed-modules.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 初始化 modules 表数据
|
||||
*
|
||||
* 运行: node scripts/seed-modules.js
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const MODULES = [
|
||||
{
|
||||
code: 'RVW',
|
||||
name: '智能审稿',
|
||||
description: '基于AI的稿件自动审查系统,支持稿约规范性评估和方法学评估',
|
||||
icon: 'FileTextOutlined',
|
||||
is_active: true,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
code: 'PKB',
|
||||
name: '个人知识库',
|
||||
description: '个人文档管理与智能检索系统,支持多格式文档上传和语义搜索',
|
||||
icon: 'BookOutlined',
|
||||
is_active: true,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
code: 'ASL',
|
||||
name: '智能文献',
|
||||
description: '文献筛选与管理系统,支持批量导入、AI辅助筛选和全文复筛',
|
||||
icon: 'ReadOutlined',
|
||||
is_active: true,
|
||||
sort_order: 3,
|
||||
},
|
||||
{
|
||||
code: 'DC',
|
||||
name: '数据清洗',
|
||||
description: '智能数据清洗与整理工具,支持双模型提取和AI辅助数据处理',
|
||||
icon: 'DatabaseOutlined',
|
||||
is_active: true,
|
||||
sort_order: 4,
|
||||
},
|
||||
{
|
||||
code: 'IIT',
|
||||
name: 'IIT管理',
|
||||
description: 'IIT项目管理系统,支持REDCap集成和项目协作',
|
||||
icon: 'ProjectOutlined',
|
||||
is_active: true,
|
||||
sort_order: 5,
|
||||
},
|
||||
{
|
||||
code: 'AIA',
|
||||
name: '智能问答',
|
||||
description: 'AI智能问答助手,提供临床研究相关问题的智能解答',
|
||||
icon: 'RobotOutlined',
|
||||
is_active: true,
|
||||
sort_order: 6,
|
||||
},
|
||||
{
|
||||
code: 'SSA',
|
||||
name: '智能统计分析',
|
||||
description: 'AI驱动的智能统计分析系统,提供高级统计方法和自动化分析',
|
||||
icon: 'BarChartOutlined',
|
||||
is_active: true,
|
||||
sort_order: 7,
|
||||
},
|
||||
{
|
||||
code: 'ST',
|
||||
name: '统计工具',
|
||||
description: '统计分析工具集,提供常用统计方法和数据可视化功能',
|
||||
icon: 'LineChartOutlined',
|
||||
is_active: true,
|
||||
sort_order: 8,
|
||||
},
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始初始化 modules 表...\n');
|
||||
|
||||
for (const module of MODULES) {
|
||||
const result = await prisma.modules.upsert({
|
||||
where: { code: module.code },
|
||||
update: {
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
icon: module.icon,
|
||||
is_active: module.is_active,
|
||||
sort_order: module.sort_order,
|
||||
},
|
||||
create: module,
|
||||
});
|
||||
console.log(`✅ ${result.code} - ${result.name}`);
|
||||
}
|
||||
|
||||
console.log('\n========== 当前 modules 表数据 ==========\n');
|
||||
|
||||
const allModules = await prisma.modules.findMany({
|
||||
orderBy: { sort_order: 'asc' },
|
||||
});
|
||||
|
||||
console.table(allModules.map(m => ({
|
||||
代码: m.code,
|
||||
名称: m.name,
|
||||
描述: m.description?.substring(0, 30) + '...',
|
||||
状态: m.is_active ? '✅ 上线' : '❌ 下线',
|
||||
排序: m.sort_order,
|
||||
})));
|
||||
|
||||
console.log('\n✨ 初始化完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
@@ -233,3 +233,50 @@ export async function logout(
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的模块
|
||||
*
|
||||
* GET /api/v1/auth/me/modules
|
||||
*/
|
||||
export async function getUserModules(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
// SUPER_ADMIN 和 PROMPT_ENGINEER 可以访问所有模块
|
||||
if (request.user.role === 'SUPER_ADMIN' || request.user.role === 'PROMPT_ENGINEER') {
|
||||
const { moduleService } = await import('./module.service.js');
|
||||
const allModules = await moduleService.getAllModules();
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: allModules.map(m => m.code),
|
||||
});
|
||||
}
|
||||
|
||||
const { moduleService } = await import('./module.service.js');
|
||||
const result = await moduleService.getUserModules(request.user.userId);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: result.modules,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '获取用户模块失败';
|
||||
logger.error('获取用户模块失败', { error: message, userId: request.user?.userId });
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: 'InternalServerError',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FastifyRequest, FastifyReply, FastifyInstance, preHandlerHookHandler }
|
||||
import { jwtService } from './jwt.service.js';
|
||||
import type { DecodedToken } from './jwt.service.js';
|
||||
import { logger } from '../logging/index.js';
|
||||
import { moduleService } from './module.service.js';
|
||||
|
||||
/**
|
||||
* 扩展 Fastify Request 类型
|
||||
@@ -224,6 +225,64 @@ export const requireSameTenant: preHandlerHookHandler = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 模块访问检查中间件工厂
|
||||
*
|
||||
* 检查用户是否有权访问指定模块
|
||||
* 支持多租户模块权限合并
|
||||
*
|
||||
* @param moduleCode 模块代码 (RVW, PKB, ASL, DC, IIT, AIA)
|
||||
*
|
||||
* @example
|
||||
* fastify.post('/tasks', {
|
||||
* preHandler: [authenticate, requireModule('RVW')]
|
||||
* }, controller.createTask);
|
||||
*/
|
||||
export function requireModule(moduleCode: string): preHandlerHookHandler {
|
||||
return async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
// SUPER_ADMIN 可以访问所有模块
|
||||
if (request.user.role === 'SUPER_ADMIN') {
|
||||
return;
|
||||
}
|
||||
|
||||
// PROMPT_ENGINEER 可以访问所有模块(用于调试)
|
||||
if (request.user.role === 'PROMPT_ENGINEER') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否有权访问该模块
|
||||
const canAccess = await moduleService.canAccessModule(
|
||||
request.user.userId,
|
||||
moduleCode
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
logger.warn('[Auth] 模块访问被拒绝', {
|
||||
userId: request.user.userId,
|
||||
role: request.user.role,
|
||||
moduleCode,
|
||||
});
|
||||
|
||||
return reply.status(403).send({
|
||||
error: 'Forbidden',
|
||||
message: `您没有访问 ${moduleCode} 模块的权限,请联系管理员开通`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('[Auth] 模块访问已授权', {
|
||||
userId: request.user.userId,
|
||||
moduleCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册认证插件到 Fastify
|
||||
*/
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
import {
|
||||
import {
|
||||
loginWithPassword,
|
||||
loginWithVerificationCode,
|
||||
sendVerificationCode,
|
||||
getCurrentUser,
|
||||
getUserModules,
|
||||
changePassword,
|
||||
refreshToken,
|
||||
logout,
|
||||
@@ -120,6 +121,13 @@ export async function authRoutes(
|
||||
preHandler: [authenticate],
|
||||
}, getCurrentUser);
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的模块
|
||||
*/
|
||||
fastify.get('/me/modules', {
|
||||
preHandler: [authenticate],
|
||||
}, getUserModules);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
|
||||
@@ -25,11 +25,16 @@ export {
|
||||
requireRoles,
|
||||
requirePermission,
|
||||
requireSameTenant,
|
||||
requireModule,
|
||||
registerAuthPlugin,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
} from './auth.middleware.js';
|
||||
|
||||
// Module Service
|
||||
export { moduleService } from './module.service.js';
|
||||
export type { ModuleInfo, UserModulesResult } from './module.service.js';
|
||||
|
||||
// Auth Routes
|
||||
export { authRoutes } from './auth.routes.js';
|
||||
|
||||
|
||||
273
backend/src/common/auth/module.service.ts
Normal file
273
backend/src/common/auth/module.service.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 模块权限服务
|
||||
*
|
||||
* 管理用户可访问的模块,支持多租户模块权限合并
|
||||
*/
|
||||
|
||||
import { prisma } from '../../config/database.js';
|
||||
import { logger } from '../logging/index.js';
|
||||
|
||||
/**
|
||||
* 模块信息
|
||||
*/
|
||||
export interface ModuleInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户模块访问结果
|
||||
*/
|
||||
export interface UserModulesResult {
|
||||
modules: string[]; // 可访问的模块代码列表
|
||||
moduleDetails: ModuleInfo[]; // 模块详细信息
|
||||
tenantModules: { // 按租户分组的模块
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
isPrimary: boolean;
|
||||
modules: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
class ModuleService {
|
||||
/**
|
||||
* 获取所有可用模块
|
||||
*/
|
||||
async getAllModules(): Promise<ModuleInfo[]> {
|
||||
const modules = await prisma.modules.findMany({
|
||||
where: { is_active: true },
|
||||
orderBy: { sort_order: 'asc' },
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
description: true,
|
||||
icon: true,
|
||||
},
|
||||
});
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可访问的所有模块
|
||||
* 合并用户所属的所有租户的模块权限
|
||||
*/
|
||||
async getUserModules(userId: string): Promise<UserModulesResult> {
|
||||
try {
|
||||
// 1. 获取用户的主租户
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
tenant_id: true,
|
||||
tenants: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.warn('[ModuleService] 用户不存在', { userId });
|
||||
return { modules: [], moduleDetails: [], tenantModules: [] };
|
||||
}
|
||||
|
||||
// 2. 获取用户加入的其他租户
|
||||
const memberships = await prisma.tenant_members.findMany({
|
||||
where: { user_id: userId },
|
||||
select: {
|
||||
tenant_id: true,
|
||||
tenants: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 3. 构建租户列表(主租户 + 额外加入的租户)
|
||||
const tenantMap = new Map<string, { name: string; isPrimary: boolean }>();
|
||||
|
||||
// 主租户
|
||||
if (user.tenant_id && user.tenants) {
|
||||
tenantMap.set(user.tenant_id, {
|
||||
name: user.tenants.name,
|
||||
isPrimary: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 额外加入的租户
|
||||
for (const m of memberships) {
|
||||
if (!tenantMap.has(m.tenant_id)) {
|
||||
tenantMap.set(m.tenant_id, {
|
||||
name: m.tenants.name,
|
||||
isPrimary: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tenantIds = Array.from(tenantMap.keys());
|
||||
|
||||
if (tenantIds.length === 0) {
|
||||
logger.warn('[ModuleService] 用户没有关联任何租户', { userId });
|
||||
return { modules: [], moduleDetails: [], tenantModules: [] };
|
||||
}
|
||||
|
||||
// 4. 查询所有租户的已开通模块
|
||||
const tenantModulesData = await prisma.tenant_modules.findMany({
|
||||
where: {
|
||||
tenant_id: { in: tenantIds },
|
||||
is_enabled: true,
|
||||
OR: [
|
||||
{ expires_at: null },
|
||||
{ expires_at: { gt: new Date() } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
tenant_id: true,
|
||||
module_code: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 5. 按租户分组模块
|
||||
const tenantModulesGrouped: UserModulesResult['tenantModules'] = [];
|
||||
const modulesByTenant = new Map<string, string[]>();
|
||||
|
||||
for (const tm of tenantModulesData) {
|
||||
if (!modulesByTenant.has(tm.tenant_id)) {
|
||||
modulesByTenant.set(tm.tenant_id, []);
|
||||
}
|
||||
modulesByTenant.get(tm.tenant_id)!.push(tm.module_code);
|
||||
}
|
||||
|
||||
Array.from(tenantMap.entries()).forEach(([tenantId, tenantInfo]) => {
|
||||
tenantModulesGrouped.push({
|
||||
tenantId,
|
||||
tenantName: tenantInfo.name,
|
||||
isPrimary: tenantInfo.isPrimary,
|
||||
modules: modulesByTenant.get(tenantId) || [],
|
||||
});
|
||||
});
|
||||
|
||||
// 6. 合并所有模块(去重)
|
||||
const moduleSet = new Set(tenantModulesData.map(tm => tm.module_code));
|
||||
const allModuleCodes = Array.from(moduleSet);
|
||||
|
||||
// 7. 获取模块详细信息
|
||||
const moduleDetails = await prisma.modules.findMany({
|
||||
where: {
|
||||
code: { in: allModuleCodes },
|
||||
is_active: true,
|
||||
},
|
||||
orderBy: { sort_order: 'asc' },
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
description: true,
|
||||
icon: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('[ModuleService] 获取用户模块成功', {
|
||||
userId,
|
||||
tenantCount: tenantIds.length,
|
||||
moduleCount: allModuleCodes.length,
|
||||
});
|
||||
|
||||
return {
|
||||
modules: allModuleCodes,
|
||||
moduleDetails,
|
||||
tenantModules: tenantModulesGrouped,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[ModuleService] 获取用户模块失败', { userId, error });
|
||||
return { modules: [], moduleDetails: [], tenantModules: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权访问指定模块
|
||||
*/
|
||||
async canAccessModule(userId: string, moduleCode: string): Promise<boolean> {
|
||||
try {
|
||||
const { modules } = await this.getUserModules(userId);
|
||||
return modules.includes(moduleCode);
|
||||
} catch (error) {
|
||||
logger.error('[ModuleService] 检查模块权限失败', { userId, moduleCode, error });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户的已开通模块
|
||||
*/
|
||||
async getTenantModules(tenantId: string): Promise<string[]> {
|
||||
const modules = await prisma.tenant_modules.findMany({
|
||||
where: {
|
||||
tenant_id: tenantId,
|
||||
is_enabled: true,
|
||||
OR: [
|
||||
{ expires_at: null },
|
||||
{ expires_at: { gt: new Date() } },
|
||||
],
|
||||
},
|
||||
select: { module_code: true },
|
||||
});
|
||||
return modules.map(m => m.module_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置租户的模块配置
|
||||
*/
|
||||
async setTenantModules(
|
||||
tenantId: string,
|
||||
moduleConfigs: { code: string; enabled: boolean; expiresAt?: Date | null }[]
|
||||
): Promise<void> {
|
||||
for (const config of moduleConfigs) {
|
||||
// 先查询是否存在
|
||||
const existing = await prisma.tenant_modules.findUnique({
|
||||
where: {
|
||||
tenant_id_module_code: {
|
||||
tenant_id: tenantId,
|
||||
module_code: config.code,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// 更新
|
||||
await prisma.tenant_modules.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
is_enabled: config.enabled,
|
||||
expires_at: config.expiresAt,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 创建(使用 crypto.randomUUID 生成 id)
|
||||
const { randomUUID } = await import('crypto');
|
||||
await prisma.tenant_modules.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
tenant_id: tenantId,
|
||||
module_code: config.code,
|
||||
is_enabled: config.enabled,
|
||||
expires_at: config.expiresAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[ModuleService] 更新租户模块配置', {
|
||||
tenantId,
|
||||
moduleCount: moduleConfigs.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const moduleService = new ModuleService();
|
||||
|
||||
@@ -56,21 +56,43 @@ export async function listPrompts(
|
||||
|
||||
const templates = await promptService.listTemplates(module);
|
||||
|
||||
// 转换为 API 响应格式
|
||||
const result = templates.map(t => ({
|
||||
id: t.id,
|
||||
code: t.code,
|
||||
name: t.name,
|
||||
module: t.module,
|
||||
description: t.description,
|
||||
variables: t.variables,
|
||||
latestVersion: t.versions[0] ? {
|
||||
version: t.versions[0].version,
|
||||
status: t.versions[0].status,
|
||||
createdAt: t.versions[0].created_at,
|
||||
} : null,
|
||||
updatedAt: t.updated_at,
|
||||
}));
|
||||
// 转换为 API 响应格式,分别返回 ACTIVE 和 DRAFT 版本
|
||||
const result = templates.map(t => {
|
||||
const activeVersion = t.versions.find(v => v.status === 'ACTIVE');
|
||||
const draftVersion = t.versions.find(v => v.status === 'DRAFT');
|
||||
|
||||
// 调试日志
|
||||
console.log(`[PromptController] ${t.code} 版本数量: ${t.versions.length}`);
|
||||
console.log(` - ACTIVE: ${activeVersion ? 'v' + activeVersion.version : '无'}`);
|
||||
console.log(` - DRAFT: ${draftVersion ? 'v' + draftVersion.version : '无'}`);
|
||||
|
||||
return {
|
||||
id: t.id,
|
||||
code: t.code,
|
||||
name: t.name,
|
||||
module: t.module,
|
||||
description: t.description,
|
||||
variables: t.variables,
|
||||
activeVersion: activeVersion ? {
|
||||
version: activeVersion.version,
|
||||
status: activeVersion.status,
|
||||
createdAt: activeVersion.created_at,
|
||||
} : null,
|
||||
draftVersion: draftVersion ? {
|
||||
version: draftVersion.version,
|
||||
status: draftVersion.status,
|
||||
createdAt: draftVersion.created_at,
|
||||
} : null,
|
||||
latestVersion: t.versions[0] ? {
|
||||
version: t.versions[0].version,
|
||||
status: t.versions[0].status,
|
||||
createdAt: t.versions[0].created_at,
|
||||
} : null,
|
||||
updatedAt: t.updated_at,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('[PromptController] 返回数据示例:', JSON.stringify(result[0], null, 2));
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
@@ -155,7 +177,7 @@ export async function saveDraft(
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const { content, modelConfig, changelog } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
@@ -202,7 +224,7 @@ export async function publishPrompt(
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const userId = (request as any).user?.id;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
@@ -238,7 +260,7 @@ export async function rollbackPrompt(
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const { version } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
@@ -273,7 +295,7 @@ export async function setDebugMode(
|
||||
) {
|
||||
try {
|
||||
const { modules, enabled } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
@@ -317,7 +339,7 @@ export async function getDebugStatus(
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = (request as any).user?.id;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
@@ -416,3 +438,4 @@ export async function invalidateCache(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -98,3 +98,5 @@ export function getAllFallbackCodes(): string[] {
|
||||
return Object.keys(FALLBACK_PROMPTS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
testRender,
|
||||
invalidateCache,
|
||||
} from './prompt.controller.js';
|
||||
import { authenticate, requirePermission } from '../auth/auth.middleware.js';
|
||||
|
||||
// Schema 定义
|
||||
const listPromptsSchema = {
|
||||
@@ -156,68 +157,70 @@ const testRenderSchema = {
|
||||
* 注册 Prompt 管理路由
|
||||
*/
|
||||
export async function promptRoutes(fastify: FastifyInstance) {
|
||||
// 列表
|
||||
// 列表(需要认证 + prompt:view)
|
||||
fastify.get('/', {
|
||||
schema: listPromptsSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
|
||||
preHandler: [authenticate, requirePermission('prompt:view')],
|
||||
handler: listPrompts,
|
||||
});
|
||||
|
||||
// 详情
|
||||
// 详情(需要认证 + prompt:view)
|
||||
fastify.get('/:code', {
|
||||
schema: getPromptDetailSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
|
||||
preHandler: [authenticate, requirePermission('prompt:view')],
|
||||
handler: getPromptDetail,
|
||||
});
|
||||
|
||||
// 调试模式 - 获取状态(需要认证)
|
||||
// 注意:这个路由必须在 /:code 之前,否则会被 /:code 匹配
|
||||
fastify.get('/debug', {
|
||||
preHandler: [authenticate],
|
||||
handler: getDebugStatus,
|
||||
});
|
||||
|
||||
// 保存草稿(需要 prompt:edit)
|
||||
fastify.post('/:code/draft', {
|
||||
schema: saveDraftSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
preHandler: [authenticate, requirePermission('prompt:edit')],
|
||||
handler: saveDraft,
|
||||
});
|
||||
|
||||
// 发布(需要 prompt:publish)
|
||||
fastify.post('/:code/publish', {
|
||||
schema: publishSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
|
||||
preHandler: [authenticate, requirePermission('prompt:publish')],
|
||||
handler: publishPrompt,
|
||||
});
|
||||
|
||||
// 回滚(需要 prompt:publish)
|
||||
fastify.post('/:code/rollback', {
|
||||
schema: rollbackSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
|
||||
preHandler: [authenticate, requirePermission('prompt:publish')],
|
||||
handler: rollbackPrompt,
|
||||
});
|
||||
|
||||
// 调试模式 - 获取状态
|
||||
fastify.get('/debug', {
|
||||
// preHandler: [fastify.authenticate],
|
||||
handler: getDebugStatus,
|
||||
});
|
||||
|
||||
// 调试模式 - 设置(需要 prompt:debug)
|
||||
fastify.post('/debug', {
|
||||
schema: setDebugModeSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:debug')],
|
||||
preHandler: [authenticate, requirePermission('prompt:debug')],
|
||||
handler: setDebugMode,
|
||||
});
|
||||
|
||||
// 测试渲染
|
||||
// 测试渲染(需要 prompt:edit)
|
||||
fastify.post('/test-render', {
|
||||
schema: testRenderSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
preHandler: [authenticate, requirePermission('prompt:edit')],
|
||||
handler: testRender,
|
||||
});
|
||||
|
||||
// 清除缓存
|
||||
// 清除缓存(需要 prompt:edit)
|
||||
fastify.post('/:code/invalidate-cache', {
|
||||
schema: getPromptDetailSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
preHandler: [authenticate, requirePermission('prompt:edit')],
|
||||
handler: invalidateCache,
|
||||
});
|
||||
}
|
||||
|
||||
export default promptRoutes;
|
||||
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ export class PromptService {
|
||||
include: {
|
||||
versions: {
|
||||
orderBy: { version: 'desc' },
|
||||
take: 1,
|
||||
// 返回所有版本,让 controller 自己过滤 ACTIVE 和 DRAFT
|
||||
},
|
||||
},
|
||||
orderBy: { code: 'asc' },
|
||||
|
||||
@@ -67,3 +67,5 @@ export interface VariableValidation {
|
||||
extraVariables: string[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,14 @@ logger.info('✅ 认证路由已注册: /api/v1/auth');
|
||||
await fastify.register(promptRoutes, { prefix: '/api/admin/prompts' });
|
||||
logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
|
||||
|
||||
// ============================================
|
||||
// 【运营管理】租户管理模块
|
||||
// ============================================
|
||||
import { tenantRoutes, moduleRoutes } from './modules/admin/routes/tenantRoutes.js';
|
||||
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
|
||||
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
|
||||
logger.info('✅ 租户管理路由已注册: /api/admin/tenants, /api/admin/modules');
|
||||
|
||||
// ============================================
|
||||
// 【临时】平台基础设施测试API
|
||||
// ============================================
|
||||
|
||||
275
backend/src/modules/admin/controllers/tenantController.ts
Normal file
275
backend/src/modules/admin/controllers/tenantController.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 租户管理控制器
|
||||
*/
|
||||
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { tenantService } from '../services/tenantService.js';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import type {
|
||||
CreateTenantRequest,
|
||||
UpdateTenantRequest,
|
||||
UpdateTenantStatusRequest,
|
||||
ConfigureModulesRequest,
|
||||
TenantListQuery,
|
||||
} from '../types/tenant.types.js';
|
||||
|
||||
/**
|
||||
* 获取租户列表
|
||||
* GET /api/admin/tenants
|
||||
*/
|
||||
export async function listTenants(
|
||||
request: FastifyRequest<{ Querystring: TenantListQuery }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const result = await tenantService.listTenants(request.query);
|
||||
return reply.send({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 获取租户列表失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取租户列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户详情
|
||||
* GET /api/admin/tenants/:id
|
||||
*/
|
||||
export async function getTenant(
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const tenant = await tenantService.getTenantDetail(id);
|
||||
|
||||
if (!tenant) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: '租户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: tenant,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 获取租户详情失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取租户详情失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户
|
||||
* POST /api/admin/tenants
|
||||
*/
|
||||
export async function createTenant(
|
||||
request: FastifyRequest<{ Body: CreateTenantRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const tenant = await tenantService.createTenant(request.body);
|
||||
return reply.status(201).send({
|
||||
success: true,
|
||||
data: tenant,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 创建租户失败', { error: error.message });
|
||||
|
||||
if (error.message.includes('已存在')) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '创建租户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户信息
|
||||
* PUT /api/admin/tenants/:id
|
||||
*/
|
||||
export async function updateTenant(
|
||||
request: FastifyRequest<{ Params: { id: string }; Body: UpdateTenantRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const tenant = await tenantService.updateTenant(id, request.body);
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: tenant,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 更新租户失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '更新租户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户状态
|
||||
* PUT /api/admin/tenants/:id/status
|
||||
*/
|
||||
export async function updateTenantStatus(
|
||||
request: FastifyRequest<{ Params: { id: string }; Body: UpdateTenantStatusRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { status } = request.body;
|
||||
await tenantService.updateTenantStatus(id, status);
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: '状态更新成功',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 更新租户状态失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '更新租户状态失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户
|
||||
* DELETE /api/admin/tenants/:id
|
||||
*/
|
||||
export async function deleteTenant(
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
await tenantService.deleteTenant(id);
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: '租户已删除',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 删除租户失败', { error: error.message });
|
||||
|
||||
if (error.message.includes('无法删除')) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '删除租户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置租户模块
|
||||
* PUT /api/admin/tenants/:id/modules
|
||||
*/
|
||||
export async function configureModules(
|
||||
request: FastifyRequest<{ Params: { id: string }; Body: ConfigureModulesRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { modules } = request.body;
|
||||
|
||||
// 转换日期格式
|
||||
const moduleConfigs = modules.map(m => ({
|
||||
code: m.code,
|
||||
enabled: m.enabled,
|
||||
expiresAt: m.expiresAt ? new Date(m.expiresAt) : null,
|
||||
}));
|
||||
|
||||
await tenantService.configureModules(id, moduleConfigs);
|
||||
|
||||
// 返回更新后的模块配置
|
||||
const updatedModules = await tenantService.getTenantModules(id);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: updatedModules,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 配置租户模块失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '配置租户模块失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用模块列表
|
||||
* GET /api/admin/modules
|
||||
*/
|
||||
export async function listModules(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { moduleService } = await import('../../../common/auth/module.service.js');
|
||||
const modules = await moduleService.getAllModules();
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: modules,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 获取模块列表失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取模块列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的模块
|
||||
* GET /api/auth/me/modules
|
||||
*/
|
||||
export async function getUserModules(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
const { moduleService } = await import('../../../common/auth/module.service.js');
|
||||
const result = await moduleService.getUserModules(request.user.userId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: result.moduleDetails.map(m => m.code),
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[TenantController] 获取用户模块失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取用户模块失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
77
backend/src/modules/admin/routes/tenantRoutes.ts
Normal file
77
backend/src/modules/admin/routes/tenantRoutes.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 租户管理路由
|
||||
*
|
||||
* API前缀: /api/admin/tenants
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import * as tenantController from '../controllers/tenantController.js';
|
||||
import { authenticate, requirePermission } from '../../../common/auth/auth.middleware.js';
|
||||
|
||||
export async function tenantRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 租户管理 ====================
|
||||
|
||||
// 获取租户列表
|
||||
// GET /api/admin/tenants?type=&status=&search=&page=1&limit=20
|
||||
fastify.get('/', {
|
||||
preHandler: [authenticate, requirePermission('tenant:view')],
|
||||
handler: tenantController.listTenants,
|
||||
});
|
||||
|
||||
// 获取租户详情
|
||||
// GET /api/admin/tenants/:id
|
||||
fastify.get('/:id', {
|
||||
preHandler: [authenticate, requirePermission('tenant:view')],
|
||||
handler: tenantController.getTenant,
|
||||
});
|
||||
|
||||
// 创建租户
|
||||
// POST /api/admin/tenants
|
||||
fastify.post('/', {
|
||||
preHandler: [authenticate, requirePermission('tenant:create')],
|
||||
handler: tenantController.createTenant,
|
||||
});
|
||||
|
||||
// 更新租户信息
|
||||
// PUT /api/admin/tenants/:id
|
||||
fastify.put('/:id', {
|
||||
preHandler: [authenticate, requirePermission('tenant:edit')],
|
||||
handler: tenantController.updateTenant,
|
||||
});
|
||||
|
||||
// 更新租户状态
|
||||
// PUT /api/admin/tenants/:id/status
|
||||
fastify.put('/:id/status', {
|
||||
preHandler: [authenticate, requirePermission('tenant:edit')],
|
||||
handler: tenantController.updateTenantStatus,
|
||||
});
|
||||
|
||||
// 删除租户
|
||||
// DELETE /api/admin/tenants/:id
|
||||
fastify.delete('/:id', {
|
||||
preHandler: [authenticate, requirePermission('tenant:delete')],
|
||||
handler: tenantController.deleteTenant,
|
||||
});
|
||||
|
||||
// 配置租户模块
|
||||
// PUT /api/admin/tenants/:id/modules
|
||||
fastify.put('/:id/modules', {
|
||||
preHandler: [authenticate, requirePermission('tenant:edit')],
|
||||
handler: tenantController.configureModules,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块管理路由
|
||||
*
|
||||
* API前缀: /api/admin/modules
|
||||
*/
|
||||
export async function moduleRoutes(fastify: FastifyInstance) {
|
||||
// 获取所有可用模块列表
|
||||
// GET /api/admin/modules
|
||||
fastify.get('/', {
|
||||
preHandler: [authenticate, requirePermission('tenant:view')],
|
||||
handler: tenantController.listModules,
|
||||
});
|
||||
}
|
||||
|
||||
307
backend/src/modules/admin/services/tenantService.ts
Normal file
307
backend/src/modules/admin/services/tenantService.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 租户管理服务
|
||||
*/
|
||||
|
||||
import { prisma } from '../../../config/database.js';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { moduleService } from '../../../common/auth/module.service.js';
|
||||
import type {
|
||||
TenantInfo,
|
||||
TenantDetail,
|
||||
TenantModuleConfig,
|
||||
CreateTenantRequest,
|
||||
UpdateTenantRequest,
|
||||
TenantListQuery,
|
||||
PaginatedResponse,
|
||||
TenantStatus,
|
||||
} from '../types/tenant.types.js';
|
||||
|
||||
class TenantService {
|
||||
/**
|
||||
* 获取租户列表(分页)
|
||||
*/
|
||||
async listTenants(query: TenantListQuery): Promise<PaginatedResponse<TenantInfo>> {
|
||||
const { type, status, search } = query;
|
||||
const page = Number(query.page) || 1;
|
||||
const limit = Number(query.limit) || 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// 构建查询条件
|
||||
const where: any = {};
|
||||
if (type) where.type = type;
|
||||
if (status) where.status = status;
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ code: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
const total = await prisma.tenants.count({ where });
|
||||
|
||||
// 查询数据
|
||||
const tenants = await prisma.tenants.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { created_at: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
type: true,
|
||||
status: true,
|
||||
contact_name: true,
|
||||
contact_phone: true,
|
||||
contact_email: true,
|
||||
expires_at: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: tenants.map(t => ({
|
||||
id: t.id,
|
||||
code: t.code,
|
||||
name: t.name,
|
||||
type: t.type as any,
|
||||
status: t.status as any,
|
||||
contactName: t.contact_name,
|
||||
contactPhone: t.contact_phone,
|
||||
contactEmail: t.contact_email,
|
||||
expiresAt: t.expires_at,
|
||||
createdAt: t.created_at,
|
||||
updatedAt: t.updated_at,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户详情(含模块配置)
|
||||
*/
|
||||
async getTenantDetail(tenantId: string): Promise<TenantDetail | null> {
|
||||
const tenant = await prisma.tenants.findUnique({
|
||||
where: { id: tenantId },
|
||||
include: {
|
||||
tenant_modules: true,
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) return null;
|
||||
|
||||
// 获取所有可用模块
|
||||
const allModules = await moduleService.getAllModules();
|
||||
|
||||
// 构建模块配置列表
|
||||
const modules: TenantModuleConfig[] = allModules.map(m => {
|
||||
const tenantModule = tenant.tenant_modules.find(tm => tm.module_code === m.code);
|
||||
return {
|
||||
code: m.code,
|
||||
name: m.name,
|
||||
enabled: tenantModule?.is_enabled ?? false,
|
||||
expiresAt: tenantModule?.expires_at,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: tenant.id,
|
||||
code: tenant.code,
|
||||
name: tenant.name,
|
||||
type: tenant.type as any,
|
||||
status: tenant.status as any,
|
||||
contactName: tenant.contact_name,
|
||||
contactPhone: tenant.contact_phone,
|
||||
contactEmail: tenant.contact_email,
|
||||
expiresAt: tenant.expires_at,
|
||||
createdAt: tenant.created_at,
|
||||
updatedAt: tenant.updated_at,
|
||||
modules,
|
||||
userCount: tenant._count.users,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户
|
||||
*/
|
||||
async createTenant(data: CreateTenantRequest): Promise<TenantInfo> {
|
||||
const { randomUUID } = await import('crypto');
|
||||
const tenantId = randomUUID();
|
||||
|
||||
// 检查 code 是否已存在
|
||||
const existing = await prisma.tenants.findUnique({
|
||||
where: { code: data.code },
|
||||
});
|
||||
if (existing) {
|
||||
throw new Error(`租户代码 "${data.code}" 已存在`);
|
||||
}
|
||||
|
||||
// 创建租户
|
||||
const tenant = await prisma.tenants.create({
|
||||
data: {
|
||||
id: tenantId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
type: data.type as any,
|
||||
status: 'ACTIVE',
|
||||
contact_name: data.contactName,
|
||||
contact_phone: data.contactPhone,
|
||||
contact_email: data.contactEmail,
|
||||
expires_at: data.expiresAt ? new Date(data.expiresAt) : null,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// 如果指定了初始模块,创建模块配置
|
||||
if (data.modules && data.modules.length > 0) {
|
||||
for (const moduleCode of data.modules) {
|
||||
await prisma.tenant_modules.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
tenant_id: tenantId,
|
||||
module_code: moduleCode,
|
||||
is_enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[TenantService] 创建租户', {
|
||||
tenantId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
modules: data.modules,
|
||||
});
|
||||
|
||||
return {
|
||||
id: tenant.id,
|
||||
code: tenant.code,
|
||||
name: tenant.name,
|
||||
type: tenant.type as any,
|
||||
status: tenant.status as any,
|
||||
contactName: tenant.contact_name,
|
||||
contactPhone: tenant.contact_phone,
|
||||
contactEmail: tenant.contact_email,
|
||||
expiresAt: tenant.expires_at,
|
||||
createdAt: tenant.created_at,
|
||||
updatedAt: tenant.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户信息
|
||||
*/
|
||||
async updateTenant(tenantId: string, data: UpdateTenantRequest): Promise<TenantInfo> {
|
||||
const tenant = await prisma.tenants.update({
|
||||
where: { id: tenantId },
|
||||
data: {
|
||||
name: data.name,
|
||||
contact_name: data.contactName,
|
||||
contact_phone: data.contactPhone,
|
||||
contact_email: data.contactEmail,
|
||||
expires_at: data.expiresAt === null ? null : (data.expiresAt ? new Date(data.expiresAt) : undefined),
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('[TenantService] 更新租户', { tenantId, data });
|
||||
|
||||
return {
|
||||
id: tenant.id,
|
||||
code: tenant.code,
|
||||
name: tenant.name,
|
||||
type: tenant.type as any,
|
||||
status: tenant.status as any,
|
||||
contactName: tenant.contact_name,
|
||||
contactPhone: tenant.contact_phone,
|
||||
contactEmail: tenant.contact_email,
|
||||
expiresAt: tenant.expires_at,
|
||||
createdAt: tenant.created_at,
|
||||
updatedAt: tenant.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户状态
|
||||
*/
|
||||
async updateTenantStatus(tenantId: string, status: TenantStatus): Promise<void> {
|
||||
await prisma.tenants.update({
|
||||
where: { id: tenantId },
|
||||
data: {
|
||||
status: status as any,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('[TenantService] 更新租户状态', { tenantId, status });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户(软删除 - 标记为 SUSPENDED)
|
||||
*/
|
||||
async deleteTenant(tenantId: string): Promise<void> {
|
||||
// 检查是否有用户
|
||||
const userCount = await prisma.user.count({
|
||||
where: { tenant_id: tenantId },
|
||||
});
|
||||
|
||||
if (userCount > 0) {
|
||||
throw new Error(`无法删除租户:该租户下还有 ${userCount} 个用户`);
|
||||
}
|
||||
|
||||
// 软删除:标记为 SUSPENDED
|
||||
await prisma.tenants.update({
|
||||
where: { id: tenantId },
|
||||
data: {
|
||||
status: 'SUSPENDED',
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('[TenantService] 删除租户(软删除)', { tenantId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置租户模块
|
||||
*/
|
||||
async configureModules(
|
||||
tenantId: string,
|
||||
modules: { code: string; enabled: boolean; expiresAt?: Date | null }[]
|
||||
): Promise<void> {
|
||||
await moduleService.setTenantModules(tenantId, modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户的模块配置
|
||||
*/
|
||||
async getTenantModules(tenantId: string): Promise<TenantModuleConfig[]> {
|
||||
const allModules = await moduleService.getAllModules();
|
||||
const tenantModules = await prisma.tenant_modules.findMany({
|
||||
where: { tenant_id: tenantId },
|
||||
});
|
||||
|
||||
return allModules.map(m => {
|
||||
const tm = tenantModules.find(t => t.module_code === m.code);
|
||||
return {
|
||||
code: m.code,
|
||||
name: m.name,
|
||||
enabled: tm?.is_enabled ?? false,
|
||||
expiresAt: tm?.expires_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const tenantService = new TenantService();
|
||||
|
||||
107
backend/src/modules/admin/types/tenant.types.ts
Normal file
107
backend/src/modules/admin/types/tenant.types.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 租户管理类型定义
|
||||
*/
|
||||
|
||||
export type TenantType = 'HOSPITAL' | 'PHARMA' | 'INTERNAL' | 'PUBLIC';
|
||||
export type TenantStatus = 'ACTIVE' | 'SUSPENDED' | 'EXPIRED';
|
||||
|
||||
/**
|
||||
* 租户基本信息
|
||||
*/
|
||||
export interface TenantInfo {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
type: TenantType;
|
||||
status: TenantStatus;
|
||||
contactName?: string | null;
|
||||
contactPhone?: string | null;
|
||||
contactEmail?: string | null;
|
||||
expiresAt?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户详情(含模块配置)
|
||||
*/
|
||||
export interface TenantDetail extends TenantInfo {
|
||||
modules: TenantModuleConfig[];
|
||||
userCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户模块配置
|
||||
*/
|
||||
export interface TenantModuleConfig {
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
expiresAt?: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户请求
|
||||
*/
|
||||
export interface CreateTenantRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
type: TenantType;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
contactEmail?: string;
|
||||
expiresAt?: string;
|
||||
modules?: string[]; // 初始开通的模块代码列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户请求
|
||||
*/
|
||||
export interface UpdateTenantRequest {
|
||||
name?: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
contactEmail?: string;
|
||||
expiresAt?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户状态请求
|
||||
*/
|
||||
export interface UpdateTenantStatusRequest {
|
||||
status: TenantStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置租户模块请求
|
||||
*/
|
||||
export interface ConfigureModulesRequest {
|
||||
modules: {
|
||||
code: string;
|
||||
enabled: boolean;
|
||||
expiresAt?: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户列表查询参数
|
||||
*/
|
||||
export interface TenantListQuery {
|
||||
type?: TenantType;
|
||||
status?: TenantStatus;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
@@ -7,55 +7,56 @@ import * as projectController from '../controllers/projectController.js';
|
||||
import * as literatureController from '../controllers/literatureController.js';
|
||||
import * as screeningController from '../controllers/screeningController.js';
|
||||
import * as fulltextScreeningController from '../fulltext-screening/controllers/FulltextScreeningController.js';
|
||||
import { authenticate, requireModule } from '../../../common/auth/auth.middleware.js';
|
||||
|
||||
export async function aslRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 筛选项目路由 ====================
|
||||
|
||||
// 创建筛选项目
|
||||
fastify.post('/projects', projectController.createProject);
|
||||
fastify.post('/projects', { preHandler: [authenticate, requireModule('ASL')] }, projectController.createProject);
|
||||
|
||||
// 获取用户的所有项目
|
||||
fastify.get('/projects', projectController.getProjects);
|
||||
fastify.get('/projects', { preHandler: [authenticate, requireModule('ASL')] }, projectController.getProjects);
|
||||
|
||||
// 获取单个项目详情
|
||||
fastify.get('/projects/:projectId', projectController.getProjectById);
|
||||
fastify.get('/projects/:projectId', { preHandler: [authenticate, requireModule('ASL')] }, projectController.getProjectById);
|
||||
|
||||
// 更新项目
|
||||
fastify.put('/projects/:projectId', projectController.updateProject);
|
||||
fastify.put('/projects/:projectId', { preHandler: [authenticate, requireModule('ASL')] }, projectController.updateProject);
|
||||
|
||||
// 删除项目
|
||||
fastify.delete('/projects/:projectId', projectController.deleteProject);
|
||||
fastify.delete('/projects/:projectId', { preHandler: [authenticate, requireModule('ASL')] }, projectController.deleteProject);
|
||||
|
||||
// ==================== 文献管理路由 ====================
|
||||
|
||||
// 导入文献(JSON)
|
||||
fastify.post('/literatures/import', literatureController.importLiteratures);
|
||||
fastify.post('/literatures/import', { preHandler: [authenticate, requireModule('ASL')] }, literatureController.importLiteratures);
|
||||
|
||||
// 导入文献(Excel上传)
|
||||
fastify.post('/literatures/import-excel', literatureController.importLiteraturesFromExcel);
|
||||
fastify.post('/literatures/import-excel', { preHandler: [authenticate, requireModule('ASL')] }, literatureController.importLiteraturesFromExcel);
|
||||
|
||||
// 获取项目的文献列表
|
||||
fastify.get('/projects/:projectId/literatures', literatureController.getLiteratures);
|
||||
fastify.get('/projects/:projectId/literatures', { preHandler: [authenticate, requireModule('ASL')] }, literatureController.getLiteratures);
|
||||
|
||||
// 删除文献
|
||||
fastify.delete('/literatures/:literatureId', literatureController.deleteLiterature);
|
||||
fastify.delete('/literatures/:literatureId', { preHandler: [authenticate, requireModule('ASL')] }, literatureController.deleteLiterature);
|
||||
|
||||
// ==================== 筛选任务路由 ====================
|
||||
|
||||
// 获取筛选任务进度
|
||||
fastify.get('/projects/:projectId/screening-task', screeningController.getScreeningTask);
|
||||
fastify.get('/projects/:projectId/screening-task', { preHandler: [authenticate, requireModule('ASL')] }, screeningController.getScreeningTask);
|
||||
|
||||
// 获取筛选结果列表(分页)
|
||||
fastify.get('/projects/:projectId/screening-results', screeningController.getScreeningResults);
|
||||
fastify.get('/projects/:projectId/screening-results', { preHandler: [authenticate, requireModule('ASL')] }, screeningController.getScreeningResults);
|
||||
|
||||
// 获取单个筛选结果详情
|
||||
fastify.get('/screening-results/:resultId', screeningController.getScreeningResultDetail);
|
||||
fastify.get('/screening-results/:resultId', { preHandler: [authenticate, requireModule('ASL')] }, screeningController.getScreeningResultDetail);
|
||||
|
||||
// 提交人工复核
|
||||
fastify.post('/screening-results/:resultId/review', screeningController.reviewScreeningResult);
|
||||
fastify.post('/screening-results/:resultId/review', { preHandler: [authenticate, requireModule('ASL')] }, screeningController.reviewScreeningResult);
|
||||
|
||||
// ⭐ 获取项目统计数据(Week 4 新增)
|
||||
fastify.get('/projects/:projectId/statistics', screeningController.getProjectStatistics);
|
||||
fastify.get('/projects/:projectId/statistics', { preHandler: [authenticate, requireModule('ASL')] }, screeningController.getProjectStatistics);
|
||||
|
||||
// TODO: 启动筛选任务(Week 2 Day 2 已实现为同步流程,异步版本待实现)
|
||||
// fastify.post('/projects/:projectId/screening/start', screeningController.startScreening);
|
||||
@@ -63,19 +64,19 @@ export async function aslRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 全文复筛路由 (Day 5 新增) ====================
|
||||
|
||||
// 创建全文复筛任务
|
||||
fastify.post('/fulltext-screening/tasks', fulltextScreeningController.createTask);
|
||||
fastify.post('/fulltext-screening/tasks', { preHandler: [authenticate, requireModule('ASL')] }, fulltextScreeningController.createTask);
|
||||
|
||||
// 获取任务进度
|
||||
fastify.get('/fulltext-screening/tasks/:taskId', fulltextScreeningController.getTaskProgress);
|
||||
fastify.get('/fulltext-screening/tasks/:taskId', { preHandler: [authenticate, requireModule('ASL')] }, fulltextScreeningController.getTaskProgress);
|
||||
|
||||
// 获取任务结果(支持筛选和分页)
|
||||
fastify.get('/fulltext-screening/tasks/:taskId/results', fulltextScreeningController.getTaskResults);
|
||||
fastify.get('/fulltext-screening/tasks/:taskId/results', { preHandler: [authenticate, requireModule('ASL')] }, fulltextScreeningController.getTaskResults);
|
||||
|
||||
// 人工审核决策
|
||||
fastify.put('/fulltext-screening/results/:resultId/decision', fulltextScreeningController.updateDecision);
|
||||
fastify.put('/fulltext-screening/results/:resultId/decision', { preHandler: [authenticate, requireModule('ASL')] }, fulltextScreeningController.updateDecision);
|
||||
|
||||
// 导出Excel
|
||||
fastify.get('/fulltext-screening/tasks/:taskId/export', fulltextScreeningController.exportExcel);
|
||||
fastify.get('/fulltext-screening/tasks/:taskId/export', { preHandler: [authenticate, requireModule('ASL')] }, fulltextScreeningController.exportExcel);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,17 @@ import { jobQueue } from '../../../../common/jobs/index.js';
|
||||
import { splitIntoChunks, recommendChunkSize } from '../../../../common/jobs/utils.js';
|
||||
import * as xlsx from 'xlsx';
|
||||
|
||||
/**
|
||||
* 获取用户ID(从JWT Token中获取)
|
||||
*/
|
||||
function getUserId(request: FastifyRequest): string {
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export class ExtractionController {
|
||||
/**
|
||||
* 文件上传
|
||||
@@ -44,7 +55,7 @@ export class ExtractionController {
|
||||
});
|
||||
}
|
||||
|
||||
const userId = (request as any).userId || 'default-user';
|
||||
const userId = getUserId(request);
|
||||
const buffer = await data.toBuffer();
|
||||
const originalFilename = data.filename;
|
||||
const timestamp = Date.now();
|
||||
@@ -105,7 +116,7 @@ export class ExtractionController {
|
||||
}>, reply: FastifyReply) {
|
||||
try {
|
||||
const { fileKey, columnName } = request.body;
|
||||
const userId = (request as any).userId || 'default-user'; // TODO: 从auth middleware获取
|
||||
const userId = getUserId(request); // TODO: 从auth middleware获取
|
||||
|
||||
logger.info('[API] Health check request', { fileKey, columnName, userId });
|
||||
|
||||
@@ -194,7 +205,7 @@ export class ExtractionController {
|
||||
modelA = 'deepseek-v3',
|
||||
modelB = 'qwen-max'
|
||||
} = request.body;
|
||||
const userId = (request as any).userId || 'default-user';
|
||||
const userId = getUserId(request);
|
||||
|
||||
logger.info('[API] Create task request', {
|
||||
userId,
|
||||
|
||||
@@ -7,17 +7,20 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { extractionController } from '../controllers/ExtractionController.js';
|
||||
import { logger } from '../../../../common/logging/index.js';
|
||||
import { authenticate, requireModule } from '../../../../common/auth/auth.middleware.js';
|
||||
|
||||
export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
logger.info('[Routes] Registering DC Tool-B routes');
|
||||
|
||||
// 文件上传
|
||||
fastify.post('/upload', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: extractionController.uploadFile.bind(extractionController)
|
||||
});
|
||||
|
||||
// 健康检查
|
||||
fastify.post('/health-check', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -31,13 +34,14 @@ export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
handler: extractionController.healthCheck.bind(extractionController)
|
||||
});
|
||||
|
||||
// 获取模板列表
|
||||
// 获取模板列表(公开API)
|
||||
fastify.get('/templates', {
|
||||
handler: extractionController.getTemplates.bind(extractionController)
|
||||
});
|
||||
|
||||
// 创建提取任务
|
||||
fastify.post('/tasks', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -58,6 +62,7 @@ export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 查询任务进度
|
||||
fastify.get('/tasks/:taskId/progress', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
@@ -72,6 +77,7 @@ export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 获取验证网格数据
|
||||
fastify.get('/tasks/:taskId/items', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
@@ -94,6 +100,7 @@ export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 裁决冲突
|
||||
fastify.post('/items/:itemId/resolve', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
@@ -116,6 +123,7 @@ export async function registerToolBRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 导出结果
|
||||
fastify.get('/tasks/:taskId/export', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
|
||||
@@ -264,6 +264,8 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -214,6 +214,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,13 +21,23 @@ import { sessionService } from '../services/SessionService.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { prisma } from '../../../../config/database.js';
|
||||
|
||||
/**
|
||||
* 获取用户ID(从JWT Token中获取)
|
||||
*/
|
||||
function getUserId(request: FastifyRequest): string {
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface QuickActionRequest {
|
||||
sessionId: string;
|
||||
action: 'filter' | 'recode' | 'binning' | 'conditional' | 'dropna' | 'dedup' | 'compute' | 'pivot' | 'unpivot' | 'metric_time' | 'multi_metric_to_long' | 'multi_metric_to_matrix';
|
||||
params: any;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface QuickActionResponse {
|
||||
@@ -58,7 +68,7 @@ export class QuickActionController {
|
||||
|
||||
try {
|
||||
const { sessionId, action, params } = request.body;
|
||||
const userId = (request as any).userId || 'test-user-001';
|
||||
const userId = getUserId(request);
|
||||
|
||||
logger.info(`[QuickAction] 执行快速操作: action=${action}, sessionId=${sessionId}`);
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ import { dataProcessService } from '../services/DataProcessService.js';
|
||||
import { jobQueue } from '../../../../common/jobs/index.js';
|
||||
import * as xlsx from 'xlsx';
|
||||
|
||||
/**
|
||||
* 获取用户ID(从JWT Token中获取)
|
||||
*/
|
||||
function getUserId(request: FastifyRequest): string {
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
// ==================== 请求参数类型定义 ====================
|
||||
|
||||
interface SessionIdParams {
|
||||
@@ -69,9 +80,8 @@ export class SessionController {
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 获取用户ID(从请求中提取,实际部署时从JWT获取)
|
||||
// TODO: 从JWT token中获取userId
|
||||
const userId = (request as any).userId || 'test-user-001';
|
||||
// 4. 获取用户ID
|
||||
const userId = getUserId(request);
|
||||
|
||||
// 5. 创建Session(Postgres-Only架构 - 异步处理)
|
||||
const sessionResult = await sessionService.createSession(
|
||||
|
||||
@@ -268,6 +268,8 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import { sessionController } from '../controllers/SessionController.js';
|
||||
import { aiController } from '../controllers/AIController.js';
|
||||
import { streamAIController } from '../controllers/StreamAIController.js';
|
||||
import { quickActionController } from '../controllers/QuickActionController.js';
|
||||
import { authenticate, requireModule } from '../../../../common/auth/auth.middleware.js';
|
||||
|
||||
export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 测试路由(Day 1) ====================
|
||||
// ==================== 测试路由(Day 1)- 公开API ====================
|
||||
|
||||
// 测试Python服务健康检查
|
||||
fastify.get('/test/health', {
|
||||
@@ -33,41 +34,49 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 上传Excel文件创建Session
|
||||
fastify.post('/sessions/upload', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.upload.bind(sessionController),
|
||||
});
|
||||
|
||||
// 获取Session信息(元数据)
|
||||
fastify.get('/sessions/:id', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.getSession.bind(sessionController),
|
||||
});
|
||||
|
||||
// 获取预览数据(前100行)
|
||||
fastify.get('/sessions/:id/preview', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.getPreviewData.bind(sessionController),
|
||||
});
|
||||
|
||||
// 获取完整数据
|
||||
fastify.get('/sessions/:id/full', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.getFullData.bind(sessionController),
|
||||
});
|
||||
|
||||
// 删除Session
|
||||
fastify.delete('/sessions/:id', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.deleteSession.bind(sessionController),
|
||||
});
|
||||
|
||||
// 更新心跳(延长10分钟)
|
||||
fastify.post('/sessions/:id/heartbeat', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.updateHeartbeat.bind(sessionController),
|
||||
});
|
||||
|
||||
// ✨ 获取列的唯一值(用于数值映射)
|
||||
fastify.get('/sessions/:id/unique-values', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.getUniqueValues.bind(sessionController),
|
||||
});
|
||||
|
||||
// ✨ 获取Session状态(Postgres-Only架构 - 用于轮询)
|
||||
fastify.get('/sessions/:id/status', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.getSessionStatus.bind(sessionController),
|
||||
});
|
||||
|
||||
@@ -75,31 +84,37 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 生成代码(不执行)
|
||||
fastify.post('/ai/generate', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: aiController.generateCode.bind(aiController),
|
||||
});
|
||||
|
||||
// 执行代码
|
||||
fastify.post('/ai/execute', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: aiController.executeCode.bind(aiController),
|
||||
});
|
||||
|
||||
// 生成并执行(一步到位,带重试)
|
||||
fastify.post('/ai/process', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: aiController.process.bind(aiController),
|
||||
});
|
||||
|
||||
// 简单问答(不生成代码)
|
||||
fastify.post('/ai/chat', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: aiController.chat.bind(aiController),
|
||||
});
|
||||
|
||||
// 获取对话历史
|
||||
fastify.get('/ai/history/:sessionId', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: aiController.getHistory.bind(aiController),
|
||||
});
|
||||
|
||||
// ✨ 流式AI处理(新增)
|
||||
fastify.post('/ai/stream-process', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: streamAIController.streamProcess.bind(streamAIController),
|
||||
});
|
||||
|
||||
@@ -107,6 +122,7 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 导出Excel文件
|
||||
fastify.get('/sessions/:id/export', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: sessionController.exportData.bind(sessionController),
|
||||
});
|
||||
|
||||
@@ -114,11 +130,13 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 执行快速操作
|
||||
fastify.post('/quick-action', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.execute.bind(quickActionController),
|
||||
});
|
||||
|
||||
// 预览操作结果
|
||||
fastify.post('/quick-action/preview', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.preview.bind(quickActionController),
|
||||
});
|
||||
|
||||
@@ -126,16 +144,19 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 获取列的缺失值统计
|
||||
fastify.post('/fillna/stats', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.handleGetFillnaStats.bind(quickActionController),
|
||||
});
|
||||
|
||||
// 执行简单填补
|
||||
fastify.post('/fillna/simple', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.handleFillnaSimple.bind(quickActionController),
|
||||
});
|
||||
|
||||
// 执行MICE多重插补
|
||||
fastify.post('/fillna/mice', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.handleFillnaMice.bind(quickActionController),
|
||||
});
|
||||
|
||||
@@ -143,6 +164,7 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 检测指标-时间表转换模式
|
||||
fastify.post('/metric-time/detect', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.handleMetricTimeDetect.bind(quickActionController),
|
||||
});
|
||||
|
||||
@@ -150,6 +172,7 @@ export async function toolCRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// 检测多指标分组
|
||||
fastify.post('/multi-metric/detect', {
|
||||
preHandler: [authenticate, requireModule('DC')],
|
||||
handler: quickActionController.handleMultiMetricDetect.bind(quickActionController),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,21 +10,22 @@ import {
|
||||
retryFailed,
|
||||
getTemplates,
|
||||
} from '../controllers/batchController.js';
|
||||
import { authenticate, requireModule } from '../../../common/auth/auth.middleware.js';
|
||||
|
||||
export default async function batchRoutes(fastify: FastifyInstance) {
|
||||
// 执行批处理任务
|
||||
fastify.post('/batch/execute', executeBatch);
|
||||
fastify.post('/batch/execute', { preHandler: [authenticate, requireModule('PKB')] }, executeBatch);
|
||||
|
||||
// 获取任务状态
|
||||
fastify.get('/batch/tasks/:taskId', getTask);
|
||||
fastify.get('/batch/tasks/:taskId', { preHandler: [authenticate, requireModule('PKB')] }, getTask);
|
||||
|
||||
// 获取任务结果
|
||||
fastify.get('/batch/tasks/:taskId/results', getTaskResults);
|
||||
fastify.get('/batch/tasks/:taskId/results', { preHandler: [authenticate, requireModule('PKB')] }, getTaskResults);
|
||||
|
||||
// 重试失败的文档
|
||||
fastify.post('/batch/tasks/:taskId/retry-failed', retryFailed);
|
||||
fastify.post('/batch/tasks/:taskId/retry-failed', { preHandler: [authenticate, requireModule('PKB')] }, retryFailed);
|
||||
|
||||
// 获取所有预设模板
|
||||
// 获取所有预设模板(公开API,不需要认证)
|
||||
fastify.get('/batch/templates', getTemplates);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,3 +49,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import * as knowledgeBaseController from '../controllers/knowledgeBaseController.js';
|
||||
import * as documentController from '../controllers/documentController.js';
|
||||
import { authenticate, requireModule } from '../../../common/auth/auth.middleware.js';
|
||||
|
||||
export default async function knowledgeBaseRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 知识库管理 API ====================
|
||||
|
||||
// 创建知识库
|
||||
fastify.post('/knowledge-bases', knowledgeBaseController.createKnowledgeBase);
|
||||
fastify.post('/knowledge-bases', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.createKnowledgeBase);
|
||||
|
||||
// 获取知识库列表
|
||||
fastify.get('/knowledge-bases', knowledgeBaseController.getKnowledgeBases);
|
||||
fastify.get('/knowledge-bases', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.getKnowledgeBases);
|
||||
|
||||
// 获取知识库详情
|
||||
fastify.get('/knowledge-bases/:id', knowledgeBaseController.getKnowledgeBaseById);
|
||||
fastify.get('/knowledge-bases/:id', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.getKnowledgeBaseById);
|
||||
|
||||
// 更新知识库
|
||||
fastify.put('/knowledge-bases/:id', knowledgeBaseController.updateKnowledgeBase);
|
||||
fastify.put('/knowledge-bases/:id', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.updateKnowledgeBase);
|
||||
|
||||
// 删除知识库
|
||||
fastify.delete('/knowledge-bases/:id', knowledgeBaseController.deleteKnowledgeBase);
|
||||
fastify.delete('/knowledge-bases/:id', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.deleteKnowledgeBase);
|
||||
|
||||
// 检索知识库
|
||||
fastify.get('/knowledge-bases/:id/search', knowledgeBaseController.searchKnowledgeBase);
|
||||
fastify.get('/knowledge-bases/:id/search', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.searchKnowledgeBase);
|
||||
|
||||
// 获取知识库统计信息
|
||||
fastify.get('/knowledge-bases/:id/stats', knowledgeBaseController.getKnowledgeBaseStats);
|
||||
fastify.get('/knowledge-bases/:id/stats', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.getKnowledgeBaseStats);
|
||||
|
||||
// Phase 2: 获取文档选择(全文阅读模式)
|
||||
fastify.get('/knowledge-bases/:id/document-selection', knowledgeBaseController.getDocumentSelection);
|
||||
fastify.get('/knowledge-bases/:id/document-selection', { preHandler: [authenticate, requireModule('PKB')] }, knowledgeBaseController.getDocumentSelection);
|
||||
|
||||
// ==================== 文档管理 API ====================
|
||||
|
||||
// 上传文档
|
||||
fastify.post('/knowledge-bases/:kbId/documents', documentController.uploadDocument);
|
||||
fastify.post('/knowledge-bases/:kbId/documents', { preHandler: [authenticate, requireModule('PKB')] }, documentController.uploadDocument);
|
||||
|
||||
// 获取文档列表
|
||||
fastify.get('/knowledge-bases/:kbId/documents', documentController.getDocuments);
|
||||
fastify.get('/knowledge-bases/:kbId/documents', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocuments);
|
||||
|
||||
// 获取文档详情
|
||||
fastify.get('/documents/:id', documentController.getDocumentById);
|
||||
fastify.get('/documents/:id', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocumentById);
|
||||
|
||||
// Phase 2: 获取文档全文
|
||||
fastify.get('/documents/:id/full-text', documentController.getDocumentFullText);
|
||||
fastify.get('/documents/:id/full-text', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocumentFullText);
|
||||
|
||||
// 删除文档
|
||||
fastify.delete('/documents/:id', documentController.deleteDocument);
|
||||
fastify.delete('/documents/:id', { preHandler: [authenticate, requireModule('PKB')] }, documentController.deleteDocument);
|
||||
|
||||
// 重新处理文档
|
||||
fastify.post('/documents/:id/reprocess', documentController.reprocessDocument);
|
||||
fastify.post('/documents/:id/reprocess', { preHandler: [authenticate, requireModule('PKB')] }, documentController.reprocessDocument);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,43 +7,45 @@
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import * as reviewController from '../controllers/reviewController.js';
|
||||
import { authenticate, requireModule } from '../../../common/auth/auth.middleware.js';
|
||||
|
||||
export default async function rvwRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 任务管理 ====================
|
||||
|
||||
// 创建任务(上传稿件)
|
||||
// POST /api/v2/rvw/tasks
|
||||
fastify.post('/tasks', reviewController.createTask);
|
||||
fastify.post('/tasks', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.createTask);
|
||||
|
||||
// 获取任务列表
|
||||
// GET /api/v2/rvw/tasks?status=all|pending|completed&page=1&limit=20
|
||||
fastify.get('/tasks', reviewController.getTaskList);
|
||||
fastify.get('/tasks', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.getTaskList);
|
||||
|
||||
// 获取任务详情
|
||||
// GET /api/v2/rvw/tasks/:taskId
|
||||
fastify.get('/tasks/:taskId', reviewController.getTaskDetail);
|
||||
fastify.get('/tasks/:taskId', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.getTaskDetail);
|
||||
|
||||
// 获取审查报告
|
||||
// GET /api/v2/rvw/tasks/:taskId/report
|
||||
fastify.get('/tasks/:taskId/report', reviewController.getTaskReport);
|
||||
fastify.get('/tasks/:taskId/report', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.getTaskReport);
|
||||
|
||||
// 删除任务
|
||||
// DELETE /api/v2/rvw/tasks/:taskId
|
||||
fastify.delete('/tasks/:taskId', reviewController.deleteTask);
|
||||
fastify.delete('/tasks/:taskId', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.deleteTask);
|
||||
|
||||
// ==================== 运行审查 ====================
|
||||
|
||||
// 运行审查(选择智能体)
|
||||
// POST /api/v2/rvw/tasks/:taskId/run
|
||||
// Body: { agents: ['editorial', 'methodology'] }
|
||||
fastify.post('/tasks/:taskId/run', reviewController.runReview);
|
||||
fastify.post('/tasks/:taskId/run', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.runReview);
|
||||
|
||||
// 批量运行审查
|
||||
// POST /api/v2/rvw/tasks/batch/run
|
||||
// Body: { taskIds: [...], agents: ['editorial', 'methodology'] }
|
||||
fastify.post('/tasks/batch/run', reviewController.batchRunReview);
|
||||
fastify.post('/tasks/batch/run', { preHandler: [authenticate, requireModule('RVW')] }, reviewController.batchRunReview);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user