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:
2026-01-13 07:34:30 +08:00
parent 5523ef36ea
commit d595037316
51 changed files with 3550 additions and 287 deletions

View File

@@ -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,
});
}
}

View File

@@ -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
*/

View File

@@ -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);
/**
* 修改密码
*/

View File

@@ -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';

View 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();

View File

@@ -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(
}
}

View File

@@ -98,3 +98,5 @@ export function getAllFallbackCodes(): string[] {
return Object.keys(FALLBACK_PROMPTS);
}

View File

@@ -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;

View File

@@ -376,7 +376,7 @@ export class PromptService {
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
// 返回所有版本,让 controller 自己过滤 ACTIVE 和 DRAFT
},
},
orderBy: { code: 'asc' },

View File

@@ -67,3 +67,5 @@ export interface VariableValidation {
extraVariables: string[];
}

View File

@@ -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
// ============================================

View 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 || '获取用户模块失败',
});
}
}

View 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,
});
}

View 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();

View 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;
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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',

View File

@@ -264,6 +264,8 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -214,6 +214,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -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}`);

View File

@@ -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. 创建SessionPostgres-Only架构 - 异步处理)
const sessionResult = await sessionService.createSession(

View File

@@ -268,6 +268,8 @@ export const streamAIController = new StreamAIController();

View File

@@ -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),
});
}

View File

@@ -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);
}

View File

@@ -49,3 +49,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -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);
}

View File

@@ -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);
}