feat(iit): Implement real-time quality control system
Summary: - Add 4 new database tables: iit_field_metadata, iit_qc_logs, iit_record_summary, iit_qc_project_stats - Implement pg-boss debounce mechanism in WebhookController - Refactor QC Worker for dual output: QC logs + record summary - Enhance HardRuleEngine to support form-based rule filtering - Create QcService for QC data queries - Optimize ChatService with new intents: query_enrollment, query_qc_status - Add admin batch operations: one-click full QC + one-click full summary - Create IIT Admin management module: project config, QC rules, user mapping Status: Code complete, pending end-to-end testing Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
348
backend/src/modules/admin/iit-projects/iitProjectController.ts
Normal file
348
backend/src/modules/admin/iit-projects/iitProjectController.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 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 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user