P0-1: Variable list sync from REDCap metadata P0-2: QC rule configuration with JSON Logic + AI suggestion P0-3: Scheduled QC + report generation + eQuery closed loop P0-4: Unified dashboard + AI stream timeline + critical events Backend: - Add IitEquery, IitCriticalEvent Prisma models + migration - Add cronEnabled/cronExpression to IitProject - Implement eQuery service/controller/routes (CRUD + respond/review/close) - Implement DailyQcOrchestrator (report -> eQuery -> critical events -> notify) - Add AI rule suggestion service - Register daily QC cron worker and eQuery auto-review worker - Extend QC cockpit with timeline, trend, critical events APIs - Fix timeline issues field compat (object vs array format) Frontend: - Create IIT business module with 6 pages (Dashboard, AI Stream, eQuery, Reports, Variable List + project config pages) - Migrate IIT config from admin panel to business module - Implement health score, risk heatmap, trend chart, critical event alerts - Register IIT module in App router and top navigation Testing: - Add E2E API test script covering 7 modules (46 assertions, all passing) Tested: E2E API tests 46/46 passed, backend and frontend verified Made-with: Cursor
407 lines
9.7 KiB
TypeScript
407 lines
9.7 KiB
TypeScript
/**
|
||
* IIT 项目管理控制器
|
||
*/
|
||
|
||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||
import { getIitProjectService, CreateProjectInput, UpdateProjectInput } from './iitProjectService.js';
|
||
import { prisma } from '../../../config/database.js';
|
||
import { logger } from '../../../common/logging/index.js';
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
interface ProjectIdParams {
|
||
id: string;
|
||
}
|
||
|
||
interface ListProjectsQuery {
|
||
status?: string;
|
||
search?: string;
|
||
}
|
||
|
||
interface TestConnectionBody {
|
||
redcapUrl: string;
|
||
redcapApiToken: string;
|
||
}
|
||
|
||
interface LinkKbBody {
|
||
knowledgeBaseId: string;
|
||
}
|
||
|
||
// ==================== 控制器函数 ====================
|
||
|
||
/**
|
||
* 获取项目列表
|
||
*/
|
||
export async function listProjects(
|
||
request: FastifyRequest<{ Querystring: ListProjectsQuery }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { status, search } = request.query;
|
||
const service = getIitProjectService(prisma);
|
||
const projects = await service.listProjects({ status, search });
|
||
|
||
return reply.send({
|
||
success: true,
|
||
data: projects,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('获取 IIT 项目列表失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取项目详情
|
||
*/
|
||
export async function getProject(
|
||
request: FastifyRequest<{ Params: ProjectIdParams }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const service = getIitProjectService(prisma);
|
||
const project = await service.getProject(id);
|
||
|
||
if (!project) {
|
||
return reply.status(404).send({
|
||
success: false,
|
||
error: '项目不存在',
|
||
});
|
||
}
|
||
|
||
return reply.send({
|
||
success: true,
|
||
data: project,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('获取 IIT 项目详情失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建项目
|
||
*/
|
||
export async function createProject(
|
||
request: FastifyRequest<{ Body: CreateProjectInput }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const input = request.body;
|
||
|
||
// 验证必填字段
|
||
if (!input.name) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: '项目名称为必填项',
|
||
});
|
||
}
|
||
|
||
if (!input.redcapUrl || !input.redcapProjectId || !input.redcapApiToken) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: 'REDCap 配置信息为必填项(URL、项目ID、API Token)',
|
||
});
|
||
}
|
||
|
||
const service = getIitProjectService(prisma);
|
||
const project = await service.createProject(input);
|
||
|
||
return reply.status(201).send({
|
||
success: true,
|
||
data: project,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('创建 IIT 项目失败', { error: message });
|
||
|
||
if (message.includes('连接测试失败')) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新项目
|
||
*/
|
||
export async function updateProject(
|
||
request: FastifyRequest<{ Params: ProjectIdParams; Body: UpdateProjectInput }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const input = request.body;
|
||
|
||
const service = getIitProjectService(prisma);
|
||
const project = await service.updateProject(id, input);
|
||
|
||
return reply.send({
|
||
success: true,
|
||
data: project,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('更新 IIT 项目失败', { error: message });
|
||
|
||
if (message.includes('连接测试失败')) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除项目
|
||
*/
|
||
export async function deleteProject(
|
||
request: FastifyRequest<{ Params: ProjectIdParams }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const service = getIitProjectService(prisma);
|
||
await service.deleteProject(id);
|
||
|
||
return reply.send({
|
||
success: true,
|
||
message: '删除成功',
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('删除 IIT 项目失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试 REDCap 连接(新配置)
|
||
*/
|
||
export async function testConnection(
|
||
request: FastifyRequest<{ Body: TestConnectionBody }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { redcapUrl, redcapApiToken } = request.body;
|
||
|
||
if (!redcapUrl || !redcapApiToken) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: '请提供 REDCap URL 和 API Token',
|
||
});
|
||
}
|
||
|
||
const service = getIitProjectService(prisma);
|
||
const result = await service.testConnection(redcapUrl, redcapApiToken);
|
||
|
||
return reply.send({
|
||
success: result.success,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('测试 REDCap 连接失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试项目的 REDCap 连接
|
||
*/
|
||
export async function testProjectConnection(
|
||
request: FastifyRequest<{ Params: ProjectIdParams }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const service = getIitProjectService(prisma);
|
||
const result = await service.testProjectConnection(id);
|
||
|
||
return reply.send({
|
||
success: result.success,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('测试项目连接失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 同步 REDCap 元数据
|
||
*/
|
||
export async function syncMetadata(
|
||
request: FastifyRequest<{ Params: ProjectIdParams }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const service = getIitProjectService(prisma);
|
||
const result = await service.syncMetadata(id);
|
||
|
||
return reply.send({
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('同步元数据失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取字段元数据列表
|
||
*/
|
||
export async function listFieldMetadata(
|
||
request: FastifyRequest<{
|
||
Params: ProjectIdParams;
|
||
Querystring: { formName?: string; search?: string };
|
||
}>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const { formName, search } = request.query as { formName?: string; search?: string };
|
||
|
||
const where: any = { projectId: id };
|
||
if (formName) {
|
||
where.formName = formName;
|
||
}
|
||
if (search) {
|
||
where.OR = [
|
||
{ fieldName: { contains: search, mode: 'insensitive' } },
|
||
{ fieldLabel: { contains: search, mode: 'insensitive' } },
|
||
];
|
||
}
|
||
|
||
const [fields, total] = await Promise.all([
|
||
prisma.iitFieldMetadata.findMany({
|
||
where,
|
||
orderBy: [{ formName: 'asc' }, { fieldName: 'asc' }],
|
||
}),
|
||
prisma.iitFieldMetadata.count({ where }),
|
||
]);
|
||
|
||
const forms = await prisma.iitFieldMetadata.findMany({
|
||
where: { projectId: id },
|
||
select: { formName: true },
|
||
distinct: ['formName'],
|
||
orderBy: { formName: 'asc' },
|
||
});
|
||
|
||
return reply.send({
|
||
success: true,
|
||
data: {
|
||
fields,
|
||
total,
|
||
forms: forms.map(f => f.formName),
|
||
},
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('获取字段元数据失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关联知识库
|
||
*/
|
||
export async function linkKnowledgeBase(
|
||
request: FastifyRequest<{ Params: ProjectIdParams; Body: LinkKbBody }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const { knowledgeBaseId } = request.body;
|
||
|
||
if (!knowledgeBaseId) {
|
||
return reply.status(400).send({
|
||
success: false,
|
||
error: '请提供知识库 ID',
|
||
});
|
||
}
|
||
|
||
const service = getIitProjectService(prisma);
|
||
await service.linkKnowledgeBase(id, knowledgeBaseId);
|
||
|
||
return reply.send({
|
||
success: true,
|
||
message: '关联成功',
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('关联知识库失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解除知识库关联
|
||
*/
|
||
export async function unlinkKnowledgeBase(
|
||
request: FastifyRequest<{ Params: ProjectIdParams }>,
|
||
reply: FastifyReply
|
||
) {
|
||
try {
|
||
const { id } = request.params;
|
||
const service = getIitProjectService(prisma);
|
||
await service.unlinkKnowledgeBase(id);
|
||
|
||
return reply.send({
|
||
success: true,
|
||
message: '解除关联成功',
|
||
});
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
logger.error('解除知识库关联失败', { error: message });
|
||
return reply.status(500).send({
|
||
success: false,
|
||
error: message,
|
||
});
|
||
}
|
||
}
|