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

@@ -0,0 +1,67 @@
-- P0-3: eQuery 闭环 + 重大事件归档 + 项目 cron 配置
-- 1. eQuery 表AI 自动生成的电子质疑,具有完整生命周期)
CREATE TABLE IF NOT EXISTS iit_schema.equery (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id TEXT NOT NULL,
record_id TEXT NOT NULL,
event_id TEXT,
form_name TEXT,
field_name TEXT,
qc_log_id TEXT,
report_id TEXT,
query_text TEXT NOT NULL,
expected_action TEXT,
severity TEXT NOT NULL DEFAULT 'warning',
category TEXT,
status TEXT NOT NULL DEFAULT 'pending',
assigned_to TEXT,
responded_at TIMESTAMPTZ,
response_text TEXT,
response_data JSONB,
review_result TEXT,
review_note TEXT,
reviewed_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
closed_by TEXT,
resolution TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_iit_equery_project ON iit_schema.equery(project_id);
CREATE INDEX IF NOT EXISTS idx_iit_equery_project_status ON iit_schema.equery(project_id, status);
CREATE INDEX IF NOT EXISTS idx_iit_equery_record ON iit_schema.equery(record_id);
CREATE INDEX IF NOT EXISTS idx_iit_equery_assigned ON iit_schema.equery(assigned_to);
-- 2. 重大事件归档表SAE、重大方案偏离等长期临床资产
CREATE TABLE IF NOT EXISTS iit_schema.critical_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id TEXT NOT NULL,
record_id TEXT NOT NULL,
event_type TEXT NOT NULL,
severity TEXT NOT NULL DEFAULT 'critical',
title TEXT NOT NULL,
description TEXT NOT NULL,
detected_at TIMESTAMPTZ NOT NULL,
detected_by TEXT NOT NULL DEFAULT 'ai',
source_qc_log_id TEXT,
source_equery_id TEXT,
source_data JSONB,
status TEXT NOT NULL DEFAULT 'open',
handled_by TEXT,
handled_at TIMESTAMPTZ,
handling_note TEXT,
reported_to_ec BOOLEAN NOT NULL DEFAULT false,
reported_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_iit_critical_event_project ON iit_schema.critical_events(project_id);
CREATE INDEX IF NOT EXISTS idx_iit_critical_event_type ON iit_schema.critical_events(project_id, event_type);
CREATE INDEX IF NOT EXISTS idx_iit_critical_event_status ON iit_schema.critical_events(project_id, status);
-- 3. 项目表新增 cron 配置字段
ALTER TABLE iit_schema.projects ADD COLUMN IF NOT EXISTS cron_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE iit_schema.projects ADD COLUMN IF NOT EXISTS cron_expression TEXT;

View File

@@ -948,6 +948,8 @@ model IitProject {
redcapApiToken String @map("redcap_api_token")
redcapUrl String @map("redcap_url")
lastSyncAt DateTime? @map("last_sync_at")
cronEnabled Boolean @default(false) @map("cron_enabled")
cronExpression String? @map("cron_expression")
status String @default("active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1426,6 +1428,97 @@ model IitQcProjectStats {
@@schema("iit_schema")
}
/// eQuery 表 - AI 自动生成的电子质疑,具有完整生命周期
model IitEquery {
id String @id @default(uuid())
projectId String @map("project_id")
// 来源
recordId String @map("record_id")
eventId String? @map("event_id")
formName String? @map("form_name")
fieldName String? @map("field_name")
qcLogId String? @map("qc_log_id")
reportId String? @map("report_id")
// 质疑内容
queryText String @map("query_text") @db.Text
expectedAction String? @map("expected_action") @db.Text
severity String @default("warning")
category String?
// 状态机: pending → responded → reviewing → closed / reopened
status String @default("pending")
// CRC 回复
assignedTo String? @map("assigned_to")
respondedAt DateTime? @map("responded_at")
responseText String? @map("response_text") @db.Text
responseData Json? @map("response_data") @db.JsonB
// AI 复核
reviewResult String? @map("review_result")
reviewNote String? @map("review_note") @db.Text
reviewedAt DateTime? @map("reviewed_at")
// 关闭
closedAt DateTime? @map("closed_at")
closedBy String? @map("closed_by")
resolution String? @db.Text
// 时间线
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([projectId], map: "idx_iit_equery_project")
@@index([projectId, status], map: "idx_iit_equery_project_status")
@@index([recordId], map: "idx_iit_equery_record")
@@index([assignedTo], map: "idx_iit_equery_assigned")
@@map("equery")
@@schema("iit_schema")
}
/// 重大事件归档表 - SAE、重大方案偏离等长期临床资产
model IitCriticalEvent {
id String @id @default(uuid())
projectId String @map("project_id")
recordId String @map("record_id")
// 事件分类
eventType String @map("event_type")
severity String @default("critical")
// 事件内容
title String
description String @db.Text
detectedAt DateTime @map("detected_at")
detectedBy String @default("ai") @map("detected_by")
// 来源追溯
sourceQcLogId String? @map("source_qc_log_id")
sourceEqueryId String? @map("source_equery_id")
sourceData Json? @map("source_data") @db.JsonB
// 处理状态
status String @default("open")
handledBy String? @map("handled_by")
handledAt DateTime? @map("handled_at")
handlingNote String? @map("handling_note") @db.Text
// 上报追踪
reportedToEc Boolean @default(false) @map("reported_to_ec")
reportedAt DateTime? @map("reported_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([projectId], map: "idx_iit_critical_event_project")
@@index([projectId, eventType], map: "idx_iit_critical_event_type")
@@index([projectId, status], map: "idx_iit_critical_event_status")
@@map("critical_events")
@@schema("iit_schema")
}
model admin_operation_logs {
id Int @id @default(autoincrement())
admin_id String