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

884 lines
32 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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` 参数过滤规则,一次处理更新两个表。
```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<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全案定时质控使用逻辑守卫处理依赖。**
```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 更新双模式质控策略)