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:
2026-02-07 21:56:11 +08:00
parent 0c590854b5
commit 5db4a7064c
74 changed files with 13383 additions and 2129 deletions

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