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
This commit is contained in:
2026-02-26 13:28:08 +08:00
parent 31b0433195
commit 203846968c
35 changed files with 7353 additions and 22 deletions

View File

@@ -286,6 +286,64 @@ export async function syncMetadata(
}
}
/**
* 获取字段元数据列表
*/
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,
});
}
}
/**
* 关联知识库
*/