feat(iit): Complete V3.1 QC engine + GCP business reports + AI timeline + bug fixes

V3.1 QC Engine:
- QcExecutor unified entry + D1-D7 dimension engines + three-level aggregation
- HealthScoreEngine + CompletenessEngine + ProtocolDeviationEngine + QcAggregator
- B4 flexible cron scheduling (project-level cronExpression + pg-boss dispatcher)
- Prisma migrations for qc_field_status, event_status, project_stats

GCP Business Reports (Phase A - 4 reports):
- D1 Eligibility: record_summary full list + qc_field_status D1 overlay
- D2 Completeness: data entry rate and missing rate aggregation
- D3/D4 Query Tracking: severity distribution from qc_field_status
- D6 Protocol Deviation: D6 dimension filtering
- 4 frontend table components + ReportsPage 5-tab restructure

AI Timeline Enhancement:
- SkillRunner outputs totalRules (33 actual rules vs 1 skill)
- iitQcCockpitController severity mapping fix (critical->red, warning->yellow)
- AiStreamPage expandable issue detail table with Chinese labels
- Event label localization (eventLabel from backend)

Business-side One-click Batch QC:
- DashboardPage batch QC button with SyncOutlined icon
- Auto-refresh QcReport cache after batch execution

Bug Fixes:
- dimension_code -> rule_category in 4 SQL queries
- D1 eligibility data source: record_summary full + qc_field_status overlay
- Timezone UTC -> Asia/Shanghai (QcReportService toBeijingTime helper)
- Pass rate calculation: passed/totalEvents instead of passed/totalRecords

Docs:
- Update IIT module status with GCP reports and bug fix milestones
- Update system status doc v6.6 with IIT progress

Tested: Backend compiles, frontend linter clean, batch QC verified
Made-with: Cursor
This commit is contained in:
2026-03-01 22:49:49 +08:00
parent 0b29fe88b5
commit 2030ebe28f
50 changed files with 8687 additions and 1492 deletions

View File

@@ -1139,14 +1139,17 @@ model IitQcReport {
/// 字段名映射字典表 - 解决 LLM 生成的字段名与 REDCap 实际字段名不一致的问题
model IitFieldMapping {
id String @id @default(uuid())
projectId String @map("project_id")
aliasName String @map("alias_name") // LLM 可能传的名称(如 "gender", "性别"
actualName String @map("actual_name") // REDCap 实际字段名(如 "sex"
fieldType String? @map("field_type") // 字段类型text, number, date, radio, checkbox
fieldLabel String? @map("field_label") // 字段显示标签
validation Json? @db.JsonB // 验证规则 { min, max, pattern, choices }
createdAt DateTime @default(now()) @map("created_at")
id String @id @default(uuid())
projectId String @map("project_id")
aliasName String @map("alias_name") // LLM 可能传的名称(如 "gender", "性别"
actualName String @map("actual_name") // REDCap 实际字段名(如 "sex"
fieldType String? @map("field_type") // 字段类型text, number, date, radio, checkbox
fieldLabel String? @map("field_label") // 字段显示标签
validation Json? @db.JsonB // 验证规则 { min, max, pattern, choices }
semanticLabel String? @map("semantic_label") // V3.1: 中文语义标签LLM 输出方向),如"谷丙转氨酶(ALT)"
formName String? @map("form_name") // V3.1: 所属表单名
ruleCategory String? @map("rule_category") // V3.1: 所属维度 D1-D7
createdAt DateTime @default(now()) @map("created_at")
@@unique([projectId, aliasName], map: "unique_iit_field_mapping")
@@index([projectId], map: "idx_iit_field_mapping_project")
@@ -1331,6 +1334,7 @@ model IitQcLog {
// 质控类型
qcType String @map("qc_type") // 'form' | 'holistic'
formName String? @map("form_name") // 单表质控时记录表单名
instanceId Int @default(1) @map("instance_id") // V3.1: 重复表单实例编号
// 核心结果
status String // 'PASS' | 'FAIL' | 'WARNING'
@@ -1396,6 +1400,28 @@ model IitRecordSummary {
// 更新次数(用于趋势分析)
updateCount Int @default(0) @map("update_count")
// V3.1: 事件级聚合
eventsTotal Int @default(0) @map("events_total")
eventsPassed Int @default(0) @map("events_passed")
eventsFailed Int @default(0) @map("events_failed")
eventsWarning Int @default(0) @map("events_warning")
// V3.1: 字段级聚合
fieldsTotal Int @default(0) @map("fields_total")
fieldsPassed Int @default(0) @map("fields_passed")
fieldsFailed Int @default(0) @map("fields_failed")
// V3.1: 维度计数D1-D7
d1Issues Int @default(0) @map("d1_issues")
d2Issues Int @default(0) @map("d2_issues")
d3Issues Int @default(0) @map("d3_issues")
d5Issues Int @default(0) @map("d5_issues")
d6Issues Int @default(0) @map("d6_issues")
d7Issues Int @default(0) @map("d7_issues")
// V3.1: 关键问题摘要
topIssues Json @default("[]") @db.JsonB @map("top_issues")
// 时间戳
createdAt DateTime @default(now()) @map("created_at")
@@ -1428,6 +1454,19 @@ model IitQcProjectStats {
// 录入进度统计
avgCompletionRate Float @default(0) @map("avg_completion_rate")
// V3.1: D1-D7 维度统计
d1PassRate Float @default(0) @map("d1_pass_rate")
d2PassRate Float @default(0) @map("d2_pass_rate")
d3PassRate Float @default(0) @map("d3_pass_rate")
d5PassRate Float @default(0) @map("d5_pass_rate")
d6PassRate Float @default(0) @map("d6_pass_rate")
d7PassRate Float @default(0) @map("d7_pass_rate")
// V3.1: 综合健康度0-100 加权评分)
healthScore Float @default(0) @map("health_score")
healthGrade String? @map("health_grade") // 'A' | 'B' | 'C' | 'D' | 'F'
dimensionDetail Json @default("{}") @db.JsonB @map("dimension_detail")
// 更新时间
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1436,6 +1475,80 @@ model IitQcProjectStats {
@@schema("iit_schema")
}
/// V3.1 字段级质控状态表 — 五级坐标Record → Event → Form → Instance → Field
/// 设计原则:每个字段最新一次 QC 结果UPSERT 语义
model IitQcFieldStatus {
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")
instanceId Int @default(1) @map("instance_id")
fieldName String @map("field_name")
status String // 'PASS' | 'FAIL' | 'WARNING'
ruleId String? @map("rule_id")
ruleName String? @map("rule_name")
ruleCategory String? @map("rule_category") // 'D1' | 'D2' | 'D3' | 'D5' | 'D6' | 'D7'
severity String? // 'critical' | 'warning' | 'info'
message String? @db.Text
actualValue String? @map("actual_value")
expectedValue String? @map("expected_value")
sourceQcLogId String? @map("source_qc_log_id")
triggeredBy String @map("triggered_by") // 'webhook' | 'cron' | 'manual'
lastQcAt DateTime @default(now()) @map("last_qc_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([projectId, recordId, eventId, formName, instanceId, fieldName], map: "uq_field_status")
@@index([projectId, recordId], map: "idx_fs_record")
@@index([projectId, recordId, eventId], map: "idx_fs_event")
@@index([projectId, status], map: "idx_fs_status")
@@index([projectId, ruleCategory], map: "idx_fs_category")
@@map("qc_field_status")
@@schema("iit_schema")
}
/// V3.1 事件级质控状态表 — 由 qc_field_status 聚合而来
/// 设计原则:每个 project × record × event 唯一一行UPSERT 语义
model IitQcEventStatus {
id String @id @default(uuid())
projectId String @map("project_id")
recordId String @map("record_id")
eventId String @map("event_id")
eventLabel String? @map("event_label")
status String // 最严重的子级状态
fieldsTotal Int @default(0) @map("fields_total")
fieldsPassed Int @default(0) @map("fields_passed")
fieldsFailed Int @default(0) @map("fields_failed")
fieldsWarning Int @default(0) @map("fields_warning")
d1Issues Int @default(0) @map("d1_issues")
d2Issues Int @default(0) @map("d2_issues")
d3Issues Int @default(0) @map("d3_issues")
d5Issues Int @default(0) @map("d5_issues")
d6Issues Int @default(0) @map("d6_issues")
d7Issues Int @default(0) @map("d7_issues")
formsChecked String[] @default([]) @map("forms_checked")
topIssues Json @default("[]") @db.JsonB @map("top_issues")
triggeredBy String @map("triggered_by")
lastQcAt DateTime @default(now()) @map("last_qc_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([projectId, recordId, eventId], map: "uq_event_status")
@@index([projectId, recordId], map: "idx_es_record")
@@index([projectId, status], map: "idx_es_status")
@@map("qc_event_status")
@@schema("iit_schema")
}
/// eQuery 表 - AI 自动生成的电子质疑,具有完整生命周期
model IitEquery {
// TODO: Tech Debt - DB 由 prisma db push 创建为 UUID 类型,未来大版本重构时统一为 String/TEXT
@@ -1446,6 +1559,7 @@ model IitEquery {
recordId String @map("record_id")
eventId String? @map("event_id")
formName String? @map("form_name")
instanceId Int @default(1) @map("instance_id") // V3.1: 重复表单实例编号
fieldName String? @map("field_name")
qcLogId String? @map("qc_log_id")
reportId String? @map("report_id")