Files
AIclinicalresearch/backend/src/modules/admin/iit-projects/iitProjectController.ts
HaHafeng 203846968c feat(iit): Complete CRA Agent V3.0 P0 milestone - autonomous QC pipeline
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
2026-02-26 13:28:08 +08:00

407 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
});
}
}