# 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 | | **场景** | 刚录完"人口学",系统只检查"年龄",**绝不会报"实验室检查缺失"的错** | ```typescript // 实时质控:只加载当前表单的规则 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)**:规则逻辑内部判断,未录入则静默跳过 | ```typescript // ✅ 正确:逻辑守卫模式 // 不要在 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 - **这展示了数据质量提升的过程** ```typescript // ✅ 正确:每次质控都新增记录 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(质控日志表) 质控结果存储,**仅新增,不覆盖**,保留完整审计轨迹。 ```prisma 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(录入汇总表) **一次处理,两个产出**:与质控同时更新,记录入组和录入进度。 ```prisma 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`**:首次录入时自动设置,后续不更新(入组时间锁定) - **`formStatus`**:JSONB 存储各表单完成状态,便于前端展示进度条 #### 3. iit_qc_project_stats(项目级汇总表) 用于 Dashboard 快速展示,避免每次 COUNT。 ```prisma 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(字段映射表)- 增强 ```prisma 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) ```typescript // 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 { 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 { 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` 参数过滤规则,一次处理更新两个表。 ```typescript // 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('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; }) { 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:全案定时质控,使用逻辑守卫处理依赖。** ```typescript // 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) ```typescript // 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 确保重试安全。 ```typescript // ✅ 录入汇总:使用 upsert await prisma.iitRecordSummary.upsert({ where: { projectId_recordId: { projectId, recordId } }, create: { enrolledAt: new Date(), ... }, update: { lastUpdatedAt: new Date(), ... }, }); ``` ### 2. pg-boss 防抖 ```typescript // WebhookController.ts await jobQueue.push('iit_quality_check', { projectId, recordId, triggeredBy: 'webhook' }, { singletonKey: `qc-${projectId}-${recordId}`, singletonSeconds: 300, // 5分钟防抖 } ); ``` ### 3. Payload 精简 ```typescript // ✅ 只存 ID,不存大数据 await jobQueue.push('iit_quality_check', { projectId: 'xxx', recordId: '10', triggeredBy: 'webhook', }); // ❌ 禁止存大数据 await jobQueue.push('iit_quality_check', { recordData: { ... }, // 禁止! }); ``` ### 4. 任务过期时间 ```typescript // 质控任务 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 更新双模式质控策略)