Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/06-实时质控系统开发计划.md
HaHafeng 5db4a7064c 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>
2026-02-07 21:56:11 +08:00

32 KiB
Raw Blame History

IIT Manager Agent 实时质控系统开发计划

文档版本: v1.1
创建日期: 2026-02-07
最后更新: 2026-02-07
参考文档:

  • docs/02-通用能力层/Postgres-Only异步任务处理指南.md
  • docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.9 补充:业务规则与数据治理细则.md

📋 概述

背景

当前 IIT Manager Agent 的质控功能存在以下问题:

  1. 无持久化:每次查询需要实时调用 REDCap API 并执行质控规则
  2. 规则管理繁琐:需要手动配置所有质控规则
  3. 缺乏审计信息:不知道数据谁录入、何时录入
  4. AI响应慢:每次都要实时查询和计算

目标

建立一套完整的实时质控系统:

  1. 字段自动同步:从 REDCap 元数据自动同步字段并生成默认规则
  2. 实时质控存储:每次数据录入后自动执行质控并存储结果
  3. 录入汇总跟踪:同步记录入组时间、录入人、表单完成状态
  4. 分级告警:严重问题立即推送,非紧急问题汇总推送
  5. AI快速响应AI 直接查询质控结果表,无需实时计算

🎯 核心设计原则

双模式质控策略Skill 分层配置)

针对 "录了3张表" vs "录了10张表" 的不同质控需求,采用 Skill 分层配置策略,无需配置复杂的前置条件。

策略 A单表实时质控 (Real-time / Form-based)

属性 说明
触发 Webhook (DET)
上下文 instrument 参数限制了范围
逻辑 系统自动只加载 formName 匹配的规则,无需人工配置 preconditions
场景 刚录完"人口学",系统只检查"年龄"绝不会报"实验室检查缺失"的错
// 实时质控:只加载当前表单的规则
async executeFormQc(projectId: string, recordId: string, instrument: string) {
  // ✅ 根据 instrument 自动过滤规则
  const rules = await getRulesByForm(projectId, instrument);
  
  // 只质控这张表单涉及的字段
  return executeRules(rules, record);
}

策略 B全案定时质控 (Daily Batch / Holistic)

属性 说明
触发 Cron Job (每日凌晨)
上下文 全量数据
逻辑 加载跨表规则Cross-Form Logic
依赖处理 逻辑守卫 (Logic Guard):规则逻辑内部判断,未录入则静默跳过
// ✅ 正确:逻辑守卫模式
// 不要在 JSON 里配 dependencies: ['lab_test']
// 而是直接在规则逻辑里写判断
const crossFormRule = {
  id: 'age_lab_consistency',
  name: '年龄与实验室结果一致性检查',
  logic: {
    "if": [
      // 逻辑守卫:如果 lab_test 表单未完成,直接跳过(返回 true = 通过)
      { "!": { "var": "lab_test_complete" } },
      true,  // 未录入 = 跳过,不报错
      // 否则执行实际检查
      { "and": [
        { ">=": [{ "var": "age" }, 18] },
        { "<=": [{ "var": "creatinine" }, 1.5] }
      ]}
    ]
  }
};

质控日志记录策略:仅新增,不覆盖

原则:每次质控都 新增记录,而不是覆盖。

原因

  1. 审计轨迹 (Audit Trail)

    • 调出周一的 Log发现当时 age 字段是空的(规则没触发)
    • 周五补录了 16(触发了规则)
    • 这能证明系统是清白的
  2. 趋势分析

    • P001 患者在入组初期有 10 个错误
    • 经过 3 次修改后,错误降为 0
    • 这展示了数据质量提升的过程
// ✅ 正确:每次质控都新增记录
await prisma.iitQcLog.create({
  data: {
    projectId,
    recordId,
    formName: instrument,
    status,
    issues,
    ruleVersion,
    triggeredBy,
    createdAt: new Date(),  // 每次都是新记录
  }
});

// ❌ 错误:覆盖模式会丢失历史
await prisma.iitQcLog.upsert({ ... });  // 不要用 upsert

🏗️ 架构设计

整体架构

┌──────────────────────────────────────────────────────────────────────────┐
│                           CRC 在 REDCap 录入数据                           │
└─────────────────────────────────────┬────────────────────────────────────┘
                                      │
                                      ▼ DET Webhook (含 instrument 参数)
┌──────────────────────────────────────────────────────────────────────────┐
│  同步层 (WebhookController)                                               │
│  - 接收 HTTP 请求                                                         │
│  - 校验签名                                                               │
│  - 防抖检查 (pg-boss singletonKey)                                        │
│  - 推入队列(携带 instrument 参数)                                        │
│  - 立即返回 200 OK (< 10ms)                                               │
└─────────────────────────────────────┬────────────────────────────────────┘
                                      │
                                      ▼ pg-boss 队列
┌──────────────────────────────────────────────────────────────────────────┐
│  异步层 (QcWorker) - 一次处理,两个产出                                    │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────────┐│
│  │ 1. 拉取记录数据 (RedcapAdapter)                                       ││
│  └──────────────────────────────────────────────────────────────────────┘│
│                         │                                                │
│           ┌─────────────┴─────────────┐                                  │
│           ▼                           ▼                                  │
│  ┌─────────────────────┐    ┌─────────────────────┐                      │
│  │ 2a. 更新录入汇总表   │    │ 2b. 执行单表质控     │                      │
│  │  iit_record_summary │    │  (只评估当前表单规则) │                      │
│  │  - 入组时间         │    │  根据 instrument 过滤 │                      │
│  │  - 录入人           │    └─────────────────────┘                      │
│  │  - 表单完成状态     │              │                                  │
│  └─────────────────────┘              ▼                                  │
│           │                 ┌─────────────────────┐                      │
│           │                 │ 2c. 新增质控日志     │                      │
│           │                 │  iit_qc_logs        │                      │
│           │                 │  (仅新增,不覆盖)    │                      │
│           │                 └─────────────────────┘                      │
│           │                           │                                  │
│           └─────────────┬─────────────┘                                  │
│                         ▼                                                │
│           ┌─────────────────────────────────────────────────────────────┐│
│           │ 3. 主动干预                                                  ││
│           │  - 🔴 RED: 立即推送企业微信                                   ││
│           │  - 🟡 YELLOW: 存入每日摘要队列                               ││
│           │  - 🟢 GREEN: 仅记录日志                                      ││
│           └─────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────┘
                                      │
            ┌─────────────────────────┴─────────────────────────┐
            ▼                                                   ▼
┌───────────────────────────────┐               ┌───────────────────────────────┐
│  每日定时任务 (Cron Job)       │               │  AI Agent 查询层               │
│  - 全案定时质控(跨表规则)    │               │  - 查询 iit_qc_logs毫秒级   │
│  - 逻辑守卫模式处理依赖       │               │  - 查询 iit_record_summary      │
│  - 生成每日摘要报告           │               │  - 查询 iit_qc_project_stats   │
└───────────────────────────────┘               └───────────────────────────────┘

📦 数据库设计

新增表

1. iit_qc_logs质控日志表

质控结果存储,仅新增,不覆盖,保留完整审计轨迹。

model IitQcLog {
  id          String   @id @default(uuid())
  projectId   String   @map("project_id")
  recordId    String   @map("record_id")
  eventId     String?  @map("event_id")
  
  // 质控类型
  qcType      String   @map("qc_type")  // 'form' | 'holistic'
  formName    String?  @map("form_name") // 单表质控时记录表单名
  
  // 核心结果
  status      String   // 'PASS' | 'FAIL' | 'WARNING'
  
  // 字段级详情 (JSONB)
  // 格式: [{ field: "age", rule: "range_check", level: "RED", message: "..." }, ...]
  issues      Json     @default("[]")
  
  // 规则统计
  rulesEvaluated  Int  @default(0) @map("rules_evaluated")  // 实际评估的规则数
  rulesSkipped    Int  @default(0) @map("rules_skipped")    // 逻辑守卫跳过的规则数
  rulesPassed     Int  @default(0) @map("rules_passed")
  rulesFailed     Int  @default(0) @map("rules_failed")
  
  // 规则版本(用于历史追溯)
  ruleVersion String   @map("rule_version")
  
  // 入排标准检查(全案质控时填充)
  inclusionPassed   Boolean? @map("inclusion_passed")
  exclusionPassed   Boolean? @map("exclusion_passed")
  
  // 审计信息
  triggeredBy  String   @map("triggered_by")  // 'webhook' | 'manual' | 'batch'
  createdAt    DateTime @default(now()) @map("created_at")
  
  // 索引 - 支持历史查询和趋势分析
  @@index([projectId, recordId, createdAt])  // 查询某记录的质控历史
  @@index([projectId, status, createdAt])    // 查询某状态的质控记录
  @@index([projectId, qcType, createdAt])    // 按质控类型查询
  @@map("iit_qc_logs")
  @@schema("iit_schema")
}

设计说明

  • 无唯一约束:同一 projectId + recordId 可以有多条记录
  • qcType:区分单表实时质控 (form) 和全案定时质控 (holistic)
  • rulesSkipped:逻辑守卫跳过的规则数,用于分析数据完整度

2. iit_record_summary录入汇总表

一次处理,两个产出:与质控同时更新,记录入组和录入进度。

model IitRecordSummary {
  id          String   @id @default(uuid())
  projectId   String   @map("project_id")
  recordId    String   @map("record_id")
  
  // 入组信息
  enrolledAt     DateTime? @map("enrolled_at")     // 首次录入时间 = 入组时间
  enrolledBy     String?   @map("enrolled_by")     // 入组录入人REDCap username
  
  // 最新录入信息
  lastUpdatedAt  DateTime  @map("last_updated_at")
  lastUpdatedBy  String?   @map("last_updated_by")
  lastFormName   String?   @map("last_form_name")  // 最后更新的表单
  
  // 表单完成状态 (JSONB)
  // 格式: { "demographics": 2, "baseline": 1, "visit1": 0 }
  // 0=未开始, 1=进行中, 2=完成
  formStatus     Json      @default("{}") @map("form_status")
  
  // 数据完整度
  totalForms     Int       @default(0) @map("total_forms")
  completedForms Int       @default(0) @map("completed_forms")
  completionRate Float     @default(0) @map("completion_rate")  // 0-100%
  
  // 最新质控状态(冗余存储,查询更快)
  latestQcStatus    String?   @map("latest_qc_status")  // 'PASS' | 'FAIL' | 'WARNING'
  latestQcAt        DateTime? @map("latest_qc_at")
  
  // 更新次数(用于趋势分析)
  updateCount    Int       @default(0) @map("update_count")
  
  // 时间戳
  createdAt      DateTime  @default(now()) @map("created_at")
  updatedAt      DateTime  @updatedAt @map("updated_at")
  
  // 唯一约束 - 每个记录只有一条汇总
  @@unique([projectId, recordId])
  @@index([projectId, enrolledAt])
  @@index([projectId, latestQcStatus])
  @@index([projectId, completionRate])
  @@map("iit_record_summary")
  @@schema("iit_schema")
}

设计说明

  • 使用 upsert:与 iit_qc_logs 不同,汇总表使用覆盖模式
  • enrolledAt:首次录入时自动设置,后续不更新(入组时间锁定)
  • formStatusJSONB 存储各表单完成状态,便于前端展示进度条

3. iit_qc_project_stats项目级汇总表

用于 Dashboard 快速展示,避免每次 COUNT。

model IitQcProjectStats {
  id          String   @id @default(uuid())
  projectId   String   @unique @map("project_id")
  
  // 汇总统计
  totalRecords    Int  @default(0) @map("total_records")
  passedRecords   Int  @default(0) @map("passed_records")
  failedRecords   Int  @default(0) @map("failed_records")
  warningRecords  Int  @default(0) @map("warning_records")
  
  // 入排标准统计
  inclusionMet    Int  @default(0) @map("inclusion_met")
  exclusionMet    Int  @default(0) @map("exclusion_met")
  
  // 录入进度统计
  avgCompletionRate Float @default(0) @map("avg_completion_rate")
  
  // 更新时间
  updatedAt   DateTime @updatedAt @map("updated_at")
  
  @@map("iit_qc_project_stats")
  @@schema("iit_schema")
}

4. iit_field_mapping字段映射表- 增强

model IitFieldMapping {
  id          String   @id @default(uuid())
  projectId   String   @map("project_id")
  
  // REDCap 字段信息
  fieldName   String   @map("field_name")
  fieldLabel  String   @map("field_label")
  fieldType   String   @map("field_type")
  formName    String   @map("form_name")
  
  // 验证规则(从 REDCap 元数据提取)
  validation       String?  @map("validation")
  validationMin    String?  @map("validation_min")
  validationMax    String?  @map("validation_max")
  choices          String?  @map("choices")
  required         Boolean  @default(false)
  
  // 别名LLM 友好名称)
  alias       String?
  
  // 规则来源标记
  ruleSource  String?  @map("rule_source")  // 'auto' | 'manual'
  
  // 时间戳
  syncedAt    DateTime @default(now()) @map("synced_at")
  
  // 唯一约束
  @@unique([projectId, fieldName])
  @@index([projectId])
  @@index([projectId, formName])  // 按表单查询字段
  @@map("iit_field_mapping")
  @@schema("iit_schema")
}

🔧 核心模块设计

1. 字段同步服务 (FieldSyncService)

// backend/src/modules/iit-manager/services/FieldSyncService.ts

export class FieldSyncService {
  /**
   * 从 REDCap 同步字段元数据
   * 
   * 流程:
   * 1. 调用 RedcapAdapter.exportMetadata()
   * 2. 解析元数据,提取字段信息(含 formName
   * 3. 更新 iit_field_mapping 表
   * 4. 自动生成默认质控规则(标记 ruleSource = 'auto'
   */
  async syncFields(projectId: string): Promise<SyncResult> {
    const adapter = await getRedcapAdapter(projectId);
    const metadata = await adapter.exportMetadata();
    
    for (const field of metadata) {
      await prisma.iitFieldMapping.upsert({
        where: { projectId_fieldName: { projectId, fieldName: field.field_name } },
        create: {
          projectId,
          fieldName: field.field_name,
          fieldLabel: field.field_label,
          fieldType: field.field_type,
          formName: field.form_name,  // ⭐ 关键:用于单表质控规则匹配
          validation: field.text_validation_type_or_show_slider_number,
          validationMin: field.text_validation_min,
          validationMax: field.text_validation_max,
          choices: field.select_choices_or_calculations,
          required: field.required_field === 'y',
        },
        update: { /* ... */ },
      });
    }
  }

  /**
   * 自动生成默认质控规则
   * 
   * 根据 REDCap 元数据自动生成:
   * - 范围规则from validation_min/max
   * - 必填规则from required
   * - 枚举规则from choices
   * 
   * 规则携带 formName用于单表质控时过滤
   */
  async generateDefaultRules(projectId: string): Promise<GeneratedRule[]> {
    const fields = await prisma.iitFieldMapping.findMany({ where: { projectId } });
    const rules: GeneratedRule[] = [];
    
    for (const field of fields) {
      // 范围规则
      if (field.validationMin || field.validationMax) {
        rules.push({
          id: `${field.fieldName}_range`,
          name: `${field.fieldLabel}范围检查`,
          formName: field.formName,  // ⭐ 规则绑定到表单
          source: 'auto',
          logic: { /* JSON Logic */ },
        });
      }
    }
    
    return rules;
  }
}

2. 质控 Worker (QcWorker)

关键改动:根据 instrument 参数过滤规则,一次处理更新两个表。

// backend/src/modules/iit-manager/workers/qcWorker.ts

import { jobQueue } from '@/common/jobs';
import { logger } from '@/common/logging';

interface QcJob {
  projectId: string;
  recordId: string;
  eventId?: string;
  instrument?: string;  // ⭐ Webhook 携带的表单名
  username?: string;    // ⭐ REDCap 录入人
  triggeredBy: 'webhook' | 'manual' | 'batch';
}

export function registerQcWorker() {
  logger.info('[QcWorker] Registering worker');

  jobQueue.process<QcJob>('iit_quality_check', async (job) => {
    const { projectId, recordId, eventId, instrument, username, triggeredBy } = job.data;

    logger.info('[QcWorker] Processing', { jobId: job.id, recordId, instrument });

    try {
      // 1. 从 REDCap 获取记录数据
      const adapter = await getRedcapAdapter(projectId);
      const record = await adapter.getRecordById(recordId);

      // ========== 产出1更新录入汇总表 ==========
      await updateRecordSummary({
        projectId,
        recordId,
        username,
        formName: instrument,
        record,
      });

      // ========== 产出2执行单表质控 ==========
      // ⭐ 策略 A根据 instrument 过滤规则
      const rules = instrument 
        ? await getRulesByForm(projectId, instrument)  // 只加载当前表单的规则
        : await getAllRules(projectId);                // batch 任务加载全部规则

      const ruleEngine = createHardRuleEngine(rules);
      const qcResult = ruleEngine.execute(recordId, record);

      // 3. 新增质控日志(仅新增,不覆盖)
      await prisma.iitQcLog.create({
        data: {
          projectId,
          recordId,
          eventId,
          qcType: instrument ? 'form' : 'holistic',
          formName: instrument,
          status: qcResult.overallStatus,
          issues: qcResult.results.filter(r => r.status !== 'PASS'),
          rulesEvaluated: qcResult.summary.totalRules,
          rulesSkipped: qcResult.summary.skipped || 0,
          rulesPassed: qcResult.summary.passed,
          rulesFailed: qcResult.summary.failed,
          ruleVersion: await getRuleVersion(projectId),
          triggeredBy,
        }
      });

      // 4. 更新汇总统计
      await updateProjectStats(projectId);

      // 5. 主动干预
      await handleAlerts(projectId, recordId, qcResult);

      logger.info('[QcWorker] ✅ Job completed', { jobId: job.id });

      return { success: true, recordId, status: qcResult.overallStatus };
    } catch (error: any) {
      logger.error('[QcWorker] ❌ Job failed', { 
        jobId: job.id, 
        error: error.message 
      });
      throw error;
    }
  });

  logger.info('[QcWorker] ✅ Worker registered: iit_quality_check');
}

/**
 * 更新录入汇总表
 */
async function updateRecordSummary(params: {
  projectId: string;
  recordId: string;
  username?: string;
  formName?: string;
  record: Record<string, any>;
}) {
  const { projectId, recordId, username, formName, record } = params;
  
  // 获取现有汇总
  const existing = await prisma.iitRecordSummary.findUnique({
    where: { projectId_recordId: { projectId, recordId } }
  });
  
  // 计算表单完成状态
  const formStatus = calculateFormStatus(record);
  const totalForms = Object.keys(formStatus).length;
  const completedForms = Object.values(formStatus).filter(s => s === 2).length;
  
  await prisma.iitRecordSummary.upsert({
    where: { projectId_recordId: { projectId, recordId } },
    create: {
      projectId,
      recordId,
      enrolledAt: new Date(),        // 首次录入 = 入组时间
      enrolledBy: username,
      lastUpdatedAt: new Date(),
      lastUpdatedBy: username,
      lastFormName: formName,
      formStatus,
      totalForms,
      completedForms,
      completionRate: (completedForms / totalForms) * 100,
      updateCount: 1,
    },
    update: {
      // 入组时间不更新
      lastUpdatedAt: new Date(),
      lastUpdatedBy: username,
      lastFormName: formName,
      formStatus,
      completedForms,
      completionRate: (completedForms / totalForms) * 100,
      updateCount: { increment: 1 },
    },
  });
}

3. 每日定时质控服务 (DailyQcService)

策略 B全案定时质控使用逻辑守卫处理依赖。

// backend/src/modules/iit-manager/services/DailyQcService.ts

export class DailyQcService {
  /**
   * 每日凌晨执行全案质控
   * 
   * 特点:
   * - 加载所有规则,包括跨表规则
   * - 使用逻辑守卫处理依赖表单未完成的情况
   */
  async executeHolisticQc(projectId: string) {
    const records = await redcapAdapter.exportRecords();
    
    for (const record of records) {
      // 推入队列,分批处理
      await jobQueue.push('iit_quality_check', {
        projectId,
        recordId: record.record_id,
        triggeredBy: 'batch',
        // 不传 instrument加载全部规则
      }, {
        singletonKey: `batch-qc-${projectId}-${record.record_id}`,
      });
    }
  }
}

// 跨表规则示例:使用逻辑守卫
const crossFormRules = [
  {
    id: 'age_lab_consistency',
    name: '年龄与实验室结果一致性检查',
    formName: null,  // 跨表规则,不绑定单一表单
    logic: {
      "if": [
        // 逻辑守卫:检查依赖表单是否完成
        { "!": { "var": "lab_test_complete" } },
        { "__skip": true },  // 返回特殊标记,表示跳过
        // 实际检查逻辑
        { "and": [
          { ">=": [{ "var": "age" }, 18] },
          { "<=": [{ "var": "creatinine" }, 1.5] }
        ]}
      ]
    }
  }
];

4. 告警服务 (AlertService)

// backend/src/modules/iit-manager/services/AlertService.ts

export class AlertService {
  /**
   * 处理告警分级
   */
  async handleAlerts(projectId: string, recordId: string, qcResult: QCResult) {
    const redIssues = qcResult.results.filter(r => r.severity === 'error');
    const yellowIssues = qcResult.results.filter(r => r.severity === 'warning');

    // 🔴 RED: 立即推送
    if (redIssues.length > 0) {
      await this.sendImmediateAlert(projectId, recordId, redIssues);
    }

    // 🟡 YELLOW: 存入每日摘要队列
    if (yellowIssues.length > 0) {
      await this.addToDailyDigest(projectId, recordId, yellowIssues);
    }

    // 🟢 GREEN: 只记录日志,不推送
  }

  /**
   * 立即推送企业微信告警
   */
  private async sendImmediateAlert(
    projectId: string,
    recordId: string,
    issues: QCRuleResult[]
  ) {
    const message = this.formatAlertMessage(recordId, issues);
    const piUserId = await this.getProjectPiUserId(projectId);
    await wechatService.sendTextMessage(piUserId, message);
  }

  /**
   * 每日摘要定时发送17:00
   */
  async sendDailyDigest(projectId: string) {
    // 由定时任务触发
  }
}

---

## 📋 开发阶段

### Phase 1: 字段同步与自动规则(预计 2 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 1.1 | 创建 `iit_field_mapping` 表迁移(含 `formName` 字段) | P0 |  |
| 1.2 | 实现 `FieldSyncService.syncFields()` | P0 |  |
| 1.3 | 实现 `FieldSyncService.generateDefaultRules()`(规则绑定 formName | P0 |  |
| 1.4 | 在运营端添加"同步字段"按钮 | P1 |  |
| 1.5 | 规则中添加 `source`  `formName` 字段 | P1 |  |

### Phase 2: 实时质控与录入汇总(预计 3 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 2.1 | 创建 `iit_qc_logs` 表迁移(支持仅新增模式) | P0 |  |
| 2.2 | 创建 `iit_record_summary` 表迁移 | P0 |  |
| 2.3 | 创建 `iit_qc_project_stats` 表迁移 | P0 |  |
| 2.4 | 重构 `WebhookController`:传递 `instrument`  `username` 参数 | P0 |  |
| 2.5 | 实现 `QcWorker`(一次处理两个产出) | P0 |  |
| 2.6 | 实现 `updateRecordSummary()` 函数 | P0 |  |
| 2.7 | 实现 `getRulesByForm()` 函数(根据表单过滤规则) | P0 |  |
| 2.8 | 实现 `updateProjectStats()` 函数 | P1 |  |
| 2.9 | 添加 JSONB 索引优化查询性能 | P1 |  |

### Phase 3: AI Agent 集成(预计 2 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 3.1 | 新增 `queryQcLogs()` 方法(查询质控历史) | P0 |  |
| 3.2 | 新增 `getRecordSummary()` 方法(查询录入汇总) | P0 |  |
| 3.3 | 新增 `getProjectQcStats()` 方法(查询项目汇总) | P0 |  |
| 3.4 | 修改 `ChatService`:优先查询质控表和汇总表 | P0 |  |
| 3.5 | 支持"哪些记录有问题"等汇总查询 | P1 |  |
| 3.6 | 支持"记录10质控历史"等趋势查询 | P1 |  |
| 3.7 | 支持"最近入组的患者"等录入查询 | P1 |  |

### Phase 4: 每日定时质控(预计 2 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 4.1 | 实现 `DailyQcService.executeHolisticQc()` | P0 |  |
| 4.2 | 实现跨表规则的逻辑守卫模式 | P0 |  |
| 4.3 | 添加定时任务(凌晨 2:00 执行全案质控) | P0 |  |
| 4.4 | 实现每日摘要报告生成 | P1 |  |

### Phase 5: 主动干预(预计 2 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 5.1 | 实现 `AlertService.handleAlerts()` | P0 |  |
| 5.2 | 实现紧急告警立即推送 | P0 |  |
| 5.3 | 实现每日摘要队列 | P1 |  |
| 5.4 | 添加定时任务(17:00 发送每日摘要) | P1 |  |
| 5.5 | 告警级别可配置化 | P2 |  |

### Phase 6: 批量回溯(预计 1 天)

| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 6.1 | 实现 `BatchQcJob`(批量质控任务) | P1 |  |
| 6.2 | 分片执行(每批 50 条,流控) | P1 |  |
| 6.3 | 运营端添加"重新质控"按钮 | P2 |  |

---

## 🛡️ 安全规范

### 1. 幂等性保证

**质控日志表(仅新增模式)**:通过 pg-boss 防抖保证不重复执行。

```typescript
// ✅ 质控日志:仅新增,不需要 upsert
// 幂等性由 pg-boss singletonKey 保证
await prisma.iitQcLog.create({
  data: { projectId, recordId, ... }
});

录入汇总表(覆盖模式):使用 upsert 确保重试安全。

// ✅ 录入汇总:使用 upsert
await prisma.iitRecordSummary.upsert({
  where: { projectId_recordId: { projectId, recordId } },
  create: { enrolledAt: new Date(), ... },
  update: { lastUpdatedAt: new Date(), ... },
});

2. pg-boss 防抖

// WebhookController.ts
await jobQueue.push('iit_quality_check', 
  { projectId, recordId, triggeredBy: 'webhook' },
  {
    singletonKey: `qc-${projectId}-${recordId}`,
    singletonSeconds: 300,  // 5分钟防抖
  }
);

3. Payload 精简

// ✅ 只存 ID不存大数据
await jobQueue.push('iit_quality_check', {
  projectId: 'xxx',
  recordId: '10',
  triggeredBy: 'webhook',
});

// ❌ 禁止存大数据
await jobQueue.push('iit_quality_check', {
  recordData: { ... },  // 禁止!
});

4. 任务过期时间

// 质控任务 15 分钟过期
await jobQueue.push('iit_quality_check', data, {
  expireInSeconds: 15 * 60,
});

// 批量任务 1 小时过期
await jobQueue.push('iit_batch_qc', data, {
  expireInSeconds: 60 * 60,
});

📊 性能预期

指标 当前 优化后 改善
Webhook 响应时间 2-5秒 < 50ms -99%
AI 查询响应时间 3-5秒调REDCap < 200ms查本地表 -95%
批量质控 1000条 不支持 ~5分钟分片 新增
并发 Webhook 可能重复执行 防抖去重 修复

检查清单

数据库

  • iit_field_mapping 表已创建(含 formName 字段)
  • iit_qc_logs 表已创建(支持仅新增模式)
  • iit_record_summary 表已创建
  • iit_qc_project_stats 表已创建
  • JSONB 索引已添加

双模式质控

  • 单表质控:根据 instrument 过滤规则
  • 全案质控:逻辑守卫模式处理依赖
  • 质控规则绑定 formName

异步处理

  • QcWorker 已注册
  • 队列名称使用下划线(iit_quality_check
  • Worker 在 jobQueue.start() 之后注册
  • 防抖配置已添加

安全规范

  • 质控日志:仅新增,不覆盖
  • 录入汇总:使用 upsert
  • Payload只存 ID
  • 错误处理:直接 throw error

功能完整性

  • 字段同步功能可用
  • 实时质控存储正常
  • 录入汇总更新正常
  • AI 查询质控表正常
  • AI 查询录入汇总正常
  • 告警推送正常
  • 每日定时质控正常

趋势分析

  • 可查询记录的质控历史
  • 可分析数据质量提升过程
  • 审计轨迹完整

维护者: IIT Manager Agent 开发团队
最后更新: 2026-02-07
文档状态: 已完成v1.1 更新双模式质控策略)