# 质控引擎架构升级 — 三级数据结构与多模式触发技术设计 > **版本:** V1.0 > **日期:** 2026-03-01 > **定位:** 质控引擎核心架构升级,解决当前数据粒度不足、调度僵化、无法精准溯源的根本问题 > **关联文档:** > - [V3.0 全新开发计划](./V3.0全新开发计划.md) > - [CRA 智能质控 Agent 四大工具工作原理说明](../../08-对外输出报告/CRA智能质控Agent-四大工具工作原理说明.md) > - [CRA AI 替代工作梳理](../../09-技术评审报告/CRA%20AI%20替代工作梳理2.md) --- ## 1. 核心认知:研究方案 → 变量清单 → 质控规则 → 质控报告 在 IIT 临床研究中,质控体系不是一堆孤立的模块,而是一条**从研究方案到质控报告的完整链条**: ``` 研究方案(Protocol) │ 定义了:纳入/排除标准、访视窗口、用药规范、AE 监测要求 ▼ 变量清单(Data Dictionary / CRF 变量) │ 将方案中的每一条要求,具体化为 EDC 系统中可采集的字段 │ 例:方案规定"年龄 16-35 岁" → CRF 中 age 字段(数值型,范围 16-35) ▼ 质控规则(QC Rules) │ 将变量的合规要求,编码为机器可执行的逻辑 │ 例:{ "and": [{ ">=": ["age", 16] }, { "<=": ["age", 35] }] } ▼ 质控报告(QC Report) │ 将规则执行的结果,聚合为人和 AI 可理解的报告 │ 例:通过率 92.9%,1 条严重违规(3 号受试者排除标准不合规) ▼ 行动(eQuery / 告警 / 监查报告) 将报告中的问题,转化为具体的跟进动作 ``` **这四者是 1:1:1:1 的映射关系**——方案中的每一条要求,对应变量清单中的具体字段,对应一条或多条质控规则,最终体现在质控报告中的一条具体结论。 **当前问题**:我们的质控引擎在"规则执行"层面已经成熟(HardRuleEngine + SkillRunner),但在**数据存储粒度**和**调度灵活性**上存在显著不足,导致报告聚合困难、历史追溯低效、调度僵化。 --- ## 2. 问题诊断 ### 2.1 数据结构粒度不足 REDCap 中的数据天然具有三级结构: ``` Record(受试者记录) └── Event(访视事件,如:筛选期、基线期、随访 1、随访 2...) └── Variable(变量/字段,如:age、gender、lab_alt、consent_date...) ``` **但当前质控数据结构只有 1.5 级**: | 数据层级 | 当前是否有独立状态表 | 问题 | |---------|------------------|------| | **记录级(Record)** | `record_summary` 表,有 `latestQcStatus` | 有,但缺乏事件维度详情 | | **事件级(Event)** | **无独立表** | 事件状态藏在 `qc_logs` 日志里,查询需 `DISTINCT ON` 聚合,效率低、易出错 | | **变量级(Variable)** | **无独立表** | 变量级结果藏在 `qc_logs.issues` 的 JSON 数组里,无法直接 SQL 查询 | **实际后果**: 1. 之前出现的"通过率为 0%"的 Bug,根本原因之一就是缺少事件级状态表,报告聚合时把不同事件的旧数据混入计算 2. 无法回答"3 号受试者在基线期的 age 字段质控状态是什么"这样的精确查询 3. 每次生成报告都要从日志中做复杂的 SQL 聚合,性能差且容易遗漏 ### 2.2 定时质控调度僵化 当前定时质控**全局硬编码**为每天 08:00: ```typescript // 当前代码(iit-manager/index.ts) await jobQueue.schedule('iit_daily_qc', '0 0 * * *', {}, { tz: 'Asia/Shanghai' }); ``` **问题**: - 所有项目共享一个全局调度,无法按项目独立配置 - 不支持"每周一三五"、"每 6 小时"等灵活策略 - 数据库已预留 `cronEnabled` / `cronExpression` 字段,但代码未使用 ### 2.3 实时质控未激活 系统**已实现**了 REDCap DET(Data Entry Trigger)Webhook 接收端,但: - REDCap 端是否配置了 DET 取决于部署环境 - Webhook 处理逻辑写入的也是旧的 `qc_logs` 结构,同样存在粒度不足问题 --- ## 3. 设计目标 ### 3.1 三级质控状态体系 建立与 REDCap 数据结构 **1:1 对齐**的三级质控状态: ``` ┌──────────────────────────────────────────────────────────────────┐ │ Record 10 │ │ 记录级状态: FAIL(取所有事件中最严重的) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Event A: 筛选期 │ │ │ │ 事件级状态: PASS │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ age = 28 → QC: PASS (规则: 16-35岁) │ │ │ │ │ │ gender = 1 → QC: PASS (规则: 非空) │ │ │ │ │ │ consent = 1 → QC: PASS (规则: 已签署) │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Event B: 基线期 │ │ │ │ 事件级状态: FAIL │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ exclusion = 1 → QC: FAIL (规则: 应为0) │ │ │ │ │ │ lab_alt = 5.2 → QC: PASS (规则: <40) │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Event C: 随访 1 │ │ │ │ 事件级状态: PASS │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ followup_date = 2026-03-01 → QC: PASS │ │ │ │ │ │ vitals_bp = 120/80 → QC: PASS │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 3.2 多模式触发体系 | 触发模式 | 描述 | 粒度 | 延迟 | |---------|------|------|------| | **实时触发** | REDCap DET Webhook,录入即质控 | 单记录 × 单事件 × 单表单 | 秒级 | | **定时触发** | 按项目独立配置 Cron 策略 | 全量记录 × 全量事件 | 分钟级 | | **手动触发** | 用户点击"一键全量质控"或 AI 调用 | 全量或指定记录 | 分钟级 | ### 3.3 自底向上的聚合链 ``` qc_logs(审计日志,追加型,不删不改,保留完整历史) ↓ 每次质控后 upsert ↓ qc_field_status(变量级,保持最新状态) ↓ 自底向上聚合 ↓ qc_event_status(事件级,保持最新状态) ↓ 自底向上聚合 ↓ record_summary(记录级,保持最新状态) ↓ 自底向上聚合 ↓ qc_project_stats(项目级统计) ↓ 格式化生成 ↓ qc_reports(LLM 友好报告 + 人类可读报告) ``` --- ## 4. 数据库设计 ### 4.1 新增表:`qc_field_status`(变量级质控状态) > 每个 **project × record × event × field** 唯一一行,始终反映该变量的**最新**质控状态。 ```sql CREATE TABLE iit_schema.qc_field_status ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), project_id TEXT NOT NULL, record_id TEXT NOT NULL, event_id TEXT NOT NULL, -- REDCap event_name field_name TEXT NOT NULL, -- 变量名(REDCap field_name) -- 质控结果 status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING' | 'SKIPPED' rule_id TEXT, -- 触发的规则 ID rule_name TEXT, -- 规则名称(冗余,查询方便) severity TEXT, -- 'critical' | 'warning' | 'info' message TEXT, -- 质控结论描述 actual_value TEXT, -- 实际值 expected_value TEXT, -- 期望值/标准 -- 溯源 source_qc_log_id TEXT, -- 关联到 qc_logs 的具体记录 triggered_by TEXT NOT NULL, -- 'webhook' | 'cron' | 'manual' last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 唯一约束:每个变量只保留最新状态 CONSTRAINT unique_field_status UNIQUE (project_id, record_id, event_id, field_name), -- 时间戳 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 索引 CREATE INDEX idx_field_status_record ON iit_schema.qc_field_status (project_id, record_id); CREATE INDEX idx_field_status_event ON iit_schema.qc_field_status (project_id, record_id, event_id); CREATE INDEX idx_field_status_fail ON iit_schema.qc_field_status (project_id, status) WHERE status IN ('FAIL', 'WARNING'); CREATE INDEX idx_field_status_rule ON iit_schema.qc_field_status (project_id, rule_id); ``` **关键设计决策**: - `actual_value` 和 `expected_value` 用 `TEXT` 存储(非 JSONB),因为变量值可能是数字、日期、字符串等多种类型,统一为文本便于展示和比较 - `source_qc_log_id` 指向 `qc_logs` 表,支持从状态追溯到具体的质控执行记录 - `SKIPPED` 状态用于"字段在该事件中不存在"的情况(如随访期没有 age 字段) ### 4.2 新增表:`qc_event_status`(事件级质控状态) > 每个 **project × record × event** 唯一一行,状态 = 其下所有变量的最严重状态。 ```sql CREATE TABLE iit_schema.qc_event_status ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), project_id TEXT NOT NULL, record_id TEXT NOT NULL, event_id TEXT NOT NULL, -- REDCap event_name event_label TEXT, -- 事件显示名(如"筛选期") -- 聚合状态 status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING' fields_total INT NOT NULL DEFAULT 0, -- 已检查的变量总数 fields_passed INT NOT NULL DEFAULT 0, fields_failed INT NOT NULL DEFAULT 0, fields_warning INT NOT NULL DEFAULT 0, fields_skipped INT NOT NULL DEFAULT 0, -- 问题摘要(方便快速查询,不需要 JOIN field_status) top_issues JSONB DEFAULT '[]', -- 最严重的 N 条问题摘要 -- 关联的表单列表 forms_checked TEXT[] DEFAULT '{}', -- 该事件中参与质控的表单 -- 溯源 triggered_by TEXT NOT NULL, last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 唯一约束 CONSTRAINT unique_event_status UNIQUE (project_id, record_id, event_id), -- 时间戳 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 索引 CREATE INDEX idx_event_status_record ON iit_schema.qc_event_status (project_id, record_id); CREATE INDEX idx_event_status_fail ON iit_schema.qc_event_status (project_id, status) WHERE status IN ('FAIL', 'WARNING'); ``` ### 4.3 改造表:`record_summary`(记录级质控状态) > 在现有 `record_summary` 基础上新增事件维度统计字段: ```sql -- 新增列(在已有表上 ALTER) ALTER TABLE iit_schema.record_summary ADD COLUMN IF NOT EXISTS events_total INT DEFAULT 0, ADD COLUMN IF NOT EXISTS events_passed INT DEFAULT 0, ADD COLUMN IF NOT EXISTS events_failed INT DEFAULT 0, ADD COLUMN IF NOT EXISTS events_warning INT DEFAULT 0, ADD COLUMN IF NOT EXISTS fields_total INT DEFAULT 0, ADD COLUMN IF NOT EXISTS fields_passed INT DEFAULT 0, ADD COLUMN IF NOT EXISTS fields_failed INT DEFAULT 0, ADD COLUMN IF NOT EXISTS top_issues JSONB DEFAULT '[]'; ``` ### 4.4 保留表:`qc_logs`(审计日志,不变) `qc_logs` 继续作为**追加型审计日志**,每次质控执行都新增一行,永不删除、永不修改。它是完整的历史记录,用于: - 趋势分析(对比不同时间点的质控结果) - 审计追踪(谁在什么时间触发了什么质控) - 回溯调查(某个问题是何时首次被发现的) ### 4.5 完整数据模型关系图 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ iit_schema │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ qc_logs │ │ qc_field_ │ │ qc_event_ │ │ │ │ (审计日志) │◄────│ status │─────►│ status │ │ │ │ │ ref │ (变量级最新) │ 聚合 │ (事件级最新) │ │ │ │ 追加型 │ │ │ │ │ │ │ │ 不删不改 │ │ project_id │ │ project_id │ │ │ │ │ │ record_id │ │ record_id │ │ │ │ 每次质控 │ │ event_id │ │ event_id │ │ │ │ 一行记录 │ │ field_name │ │ │ │ │ │ │ │ status │ │ status │ │ │ │ │ │ rule_id │ │ fields_total │ │ │ │ │ │ actual_value │ │ fields_passed │ │ │ │ │ │ expected_val │ │ fields_failed │ │ │ └─────────────┘ └──────────────┘ └───────┬───────┘ │ │ │ 聚合 │ │ ▼ │ │ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ qc_reports │◄────│ qc_project_ │◄────│ record_ │ │ │ │ (LLM报告) │ 生成 │ stats │ 聚合 │ summary │ │ │ │ │ │ (项目级统计) │ │ (记录级最新) │ │ │ │ llm_report │ │ │ │ │ │ │ │ summary │ │ total_records │ │ latestQcStatus│ │ │ │ issues │ │ passed_records│ │ events_total │ │ │ │ │ │ passRate │ │ events_passed │ │ │ └──────────────┘ └───────────────┘ └───────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 5. 多模式触发设计 ### 5.1 实时触发(REDCap DET Webhook) **已有基础**:`WebhookController.ts` 已实现 DET 接收端。 **升级点**:Webhook 处理完毕后,不仅写 `qc_logs`,还需同步更新三级状态表。 ``` REDCap 保存表单 ↓ DET POST /api/v1/iit/webhooks/redcap ↓ 立即返回 200,异步处理 WebhookController.processWebhookAsync() ↓ pg-boss 队列(iit_quality_check,5 分钟防抖) ↓ QcExecutor.executeSingleRecord(projectId, recordId, eventId, formName) ├── 1. RedcapAdapter 拉取该记录该事件的最新数据 ├── 2. HardRuleEngine 逐规则执行 ├── 3. 结果写入 qc_logs(追加,审计) ├── 4. upsert qc_field_status(逐变量更新最新状态) ├── 5. upsert qc_event_status(聚合该事件所有变量状态) ├── 6. upsert record_summary(聚合该记录所有事件状态) └── 7. 刷新 qc_project_stats 和 qc_reports 缓存 ``` ### 5.2 定时触发(项目级 Cron 配置) **改造方案**: #### 5.2.1 数据库配置 `IitProject` 表已有 `cronEnabled` 和 `cronExpression` 字段,直接使用。 #### 5.2.2 前端配置界面 在项目管理页面新增"定时质控"配置面板: ``` ┌─────────────────────────────────────────────┐ │ 定时质控配置 │ │ │ │ ○ 关闭定时质控 │ │ ● 开启定时质控 │ │ │ │ 频率: │ │ ┌─────────────────────────────┐ │ │ │ ○ 每天 时间 [08:00]│ │ │ │ ○ 每周指定日期 │ │ │ │ ☑ 周一 ☐ 周二 ☑ 周三 │ │ │ │ ☐ 周四 ☑ 周五 ☐ 周六 ☐ 周日│ │ │ │ 时间 [08:00] │ │ │ │ ○ 每隔 N 小时 [6] 小时 │ │ │ │ ○ 高级(Cron 表达式) │ │ │ │ [0 8 * * 1,3,5] │ │ │ │ ⓘ Cron 表达式说明 │ │ │ └─────────────────────────────┘ │ │ │ │ 下次执行时间:2026-03-03 周一 08:00 │ │ │ │ [保存配置] │ └─────────────────────────────────────────────┘ ``` **Cron 表达式说明**(对临床专家的通俗解释): | 配置需求 | 对应表达式 | 含义 | |---------|-----------|------| | 每天 8:00 | `0 8 * * *` | 每天早上 8 点执行 | | 每周一 9:00 | `0 9 * * 1` | 每周一早上 9 点 | | 每周一三五 8:00 | `0 8 * * 1,3,5` | 周一、周三、周五早上 8 点 | | 每 6 小时 | `0 */6 * * *` | 每天 0:00, 6:00, 12:00, 18:00 | | 工作日每天 9:00 | `0 9 * * 1-5` | 周一到周五每天 9 点 | | 每天 8:00 和 20:00 | `0 8,20 * * *` | 早晚各一次 | > **说明**:大多数用户通过可视化界面选择即可,系统自动生成 Cron 表达式。只有高级用户需要直接填写表达式。 #### 5.2.3 后端调度改造 ```typescript // 改造后的逻辑(伪代码) async function registerProjectCrons() { const projects = await prisma.iitProject.findMany({ where: { cronEnabled: true, status: 'active' }, select: { id: true, cronExpression: true }, }); for (const project of projects) { const cronExpr = project.cronExpression || '0 0 * * *'; // 默认每天 08:00 await jobQueue.schedule( `iit_qc_${project.id}`, // 每个项目独立的任务名 cronExpr, { projectId: project.id }, { tz: 'Asia/Shanghai' } ); } } ``` ### 5.3 手动触发 - **前端入口**:质控驾驶舱"一键全量质控"按钮 - **AI 入口**:`check_quality` 工具(用户对话中说"帮我跑一下质控") - **执行流程**:与定时触发相同,复用 `QcExecutor.executeBatch()` --- ## 6. 质控执行器重构(QcExecutor) ### 6.1 核心流程 将当前分散在 `SkillRunner`、`iitBatchController`、`QcReportService` 中的质控执行逻辑,统一收归到 `QcExecutor` 服务中: ```typescript class QcExecutor { /** * 单记录质控(实时触发 / AI 调用) */ async executeSingleRecord( projectId: string, recordId: string, eventId: string, options?: { triggeredBy: 'webhook' | 'manual' } ): Promise { // 1. 从 REDCap 拉取该 record × event 的最新数据 // 2. 加载适用于该事件的质控规则 // 3. 逐规则执行 → 收集变量级结果 // 4. 写入 qc_logs(追加审计日志) // 5. upsert qc_field_status(逐变量) // 6. upsert qc_event_status(聚合事件) // 7. upsert record_summary(聚合记录) // 8. 更新 qc_project_stats // 9. 标记 qc_reports 缓存过期 } /** * 批量质控(定时触发 / 手动触发) */ async executeBatch( projectId: string, options?: { triggeredBy: 'cron' | 'manual' } ): Promise { // 1. 从 REDCap 拉取全量记录(按事件分开) // 2. 基线数据合并(将基线事件数据合并到后续事件) // 3. 逐 record × event 调用 executeSingleRecord 逻辑 // 4. 汇总批量结果 // 5. 强制刷新 qc_reports // 6. 推送通知(企微等) } /** * 自底向上聚合(内部方法) * field_status → event_status → record_summary → project_stats */ private async aggregateUpward( projectId: string, recordId: string, eventId: string ): Promise { // 事件级:从 qc_field_status 聚合 // status = 所有变量中最严重的状态 // fields_total = COUNT(*) // fields_passed = COUNT(status='PASS') // fields_failed = COUNT(status='FAIL') // 记录级:从 qc_event_status 聚合 // latestQcStatus = 所有事件中最严重的状态 // events_total = COUNT(*) // events_passed = COUNT(status='PASS') // 项目级:从 record_summary 聚合 // passRate = passed_records / total_records * 100 } } ``` ### 6.2 状态优先级 聚合时始终取"最严重的状态": ``` FAIL (3) > WARNING (2) > UNCERTAIN (1) > PASS (0) ``` 规则: - **事件状态** = 其下所有变量状态中最严重的 - **记录状态** = 其下所有事件状态中最严重的 - 只要有一个变量 FAIL,事件就是 FAIL - 只要有一个事件 FAIL,记录就是 FAIL ### 6.3 SKIPPED 处理 当某个变量在某个事件中不存在(如随访期没有 age 字段): - `qc_field_status` 中不写入该变量的记录(而非写 SKIPPED) - 该变量不参与事件级聚合计算 - 只有被规则实际检查过的变量才写入 `qc_field_status` --- ## 7. 报告生成优化 ### 7.1 当前问题 `QcReportService.aggregateStats()` 目前直接从 `qc_logs` 做 `DISTINCT ON` 聚合,效率低、逻辑复杂。 ### 7.2 优化后 有了三级状态表后,报告生成变得简单高效: ```typescript // 报告概览 — 直接从 record_summary 读取 const records = await prisma.iitRecordSummary.findMany({ where: { projectId }, }); const totalRecords = records.length; const passedRecords = records.filter(r => r.latestQcStatus === 'PASS').length; const passRate = (passedRecords / totalRecords * 100).toFixed(1); // 严重问题列表 — 直接从 qc_field_status 读取 const criticalIssues = await prisma.qcFieldStatus.findMany({ where: { projectId, status: 'FAIL', severity: 'critical' }, orderBy: { lastQcAt: 'desc' }, }); // 事件级统计 — 直接从 qc_event_status 读取 const eventStats = await prisma.qcEventStatus.findMany({ where: { projectId }, }); ``` **对比**: | 维度 | 改造前 | 改造后 | |------|-------|-------| | 聚合方式 | `DISTINCT ON` 从 qc_logs 实时聚合 | 直接 SELECT 状态表 | | 查询复杂度 | 复杂 SQL + JSON 解析 | 简单 WHERE 过滤 | | 性能 | 随日志增长线性下降 | 恒定(状态表大小固定) | | 正确性 | 容易被旧日志污染 | 始终是最新状态 | ### 7.3 LLM 报告增强 三级数据结构让 LLM 报告可以新增"事件维度"章节: ```xml - 通过率: 92.9%(14 条记录,13 条通过,1 条失败) - 事件覆盖: 42 个事件已质控,39 个通过 - 严重问题: 1 | 警告: 0 1. [exc_001] **排除标准检查**: 当前值 **1** (标准: 0) - 筛选期: 14/14 通过 (100%) - 基线期: 13/14 通过 (92.9%) - 随访1: 12/12 通过 (100%) ``` --- ## 8. 与四大工具的集成 ### 8.1 `read_report` — 直接受益 报告数据来源从"日志聚合"变为"状态表直查",响应更快、数据更准确。 新增支持按事件维度查询: - `section=event_overview`:各事件的通过率统计 - `section=record_detail&record_id=3`:某个记录的三级完整状态 ### 8.2 `look_up_data` — 可增加质控标注 查询原始数据时,可附带每个字段的质控状态: ```json { "record_id": "3", "age": { "value": "28", "qc_status": "PASS" }, "exclusion": { "value": "1", "qc_status": "FAIL", "message": "排除标准应为0" } } ``` ### 8.3 `check_quality` — 执行后更新三级状态 调用 `QcExecutor`,结果自动写入三级状态表。 ### 8.4 `search_knowledge` — 不受影响 知识库检索与质控数据结构无关,不需要改动。 --- ## 9. 实施计划 ### Phase 1:三级数据结构(2 天) | 任务 | 工作量 | 说明 | |------|--------|------| | 编写 Prisma Schema + Migration | 0.5 天 | 新增 `qc_field_status`、`qc_event_status`,改造 `record_summary` | | 实现 `QcExecutor` 服务 | 1 天 | 统一质控执行 + 三级状态更新逻辑 | | 改造 `iitBatchController` | 0.5 天 | 调用 `QcExecutor` 替代原有分散逻辑 | ### Phase 2:定时质控灵活配置(1 天) | 任务 | 工作量 | 说明 | |------|--------|------| | 后端:项目级 Cron 调度改造 | 0.5 天 | 从全局硬编码改为读取项目配置 | | 前端:定时质控配置面板 | 0.5 天 | 可视化选择 + 高级 Cron 输入 | ### Phase 3:报告生成优化 + 工具集成(1 天) | 任务 | 工作量 | 说明 | |------|--------|------| | 改造 `QcReportService` | 0.5 天 | 从状态表直查替代日志聚合 | | 改造 `ToolsService` | 0.5 天 | `read_report` 和 `check_quality` 适配新结构 | ### Phase 4:实时质控激活 + 验证(1 天) | 任务 | 工作量 | 说明 | |------|--------|------| | 改造 Webhook Worker | 0.5 天 | 处理逻辑接入 `QcExecutor` | | 端到端验证脚本 | 0.5 天 | 覆盖三种触发模式 + 三级数据一致性验证 | **总计:约 5 天** --- ## 10. 验收标准 | 验收项 | 标准 | |--------|------| | 三级数据一致性 | 执行质控后,`qc_field_status` → `qc_event_status` → `record_summary` 的聚合结果一致 | | 变量级查询 | 能直接查询"3 号受试者在基线期的 age 字段质控状态" | | 事件级查询 | 能直接查询"3 号受试者基线期的整体质控状态" | | 定时质控配置 | 不同项目可配置不同的 Cron 策略,互不影响 | | 实时质控 | REDCap 保存表单后,30 秒内三级状态表更新 | | 报告准确性 | 报告中的通过率与 `record_summary` 中的统计一致 | | 历史追溯 | `qc_logs` 保留完整历史,可追溯任意时间点的质控结果 | | 性能 | 报告生成时间 < 2 秒(100 条记录、500 个变量规模) | --- ## 11. 关键决策记录 | 决策 | 选择 | 理由 | |------|------|------| | 变量级状态是否独立建表 | 是,`qc_field_status` | 支持精准查询、避免 JSON 解析、支持索引 | | 事件级状态是否独立建表 | 是,`qc_event_status` | 消除日志聚合的复杂性和出错风险 | | `qc_logs` 是否保留 | 保留,追加型不变 | 审计追踪和趋势分析不可或缺 | | 聚合方向 | 自底向上(变量→事件→记录→项目) | 与 REDCap 数据结构一致,逻辑清晰 | | SKIPPED 变量处理 | 不写入 `qc_field_status` | 减少无意义数据,简化聚合逻辑 | | Cron 配置粒度 | 项目级 | 不同项目节奏不同(I 期 vs III 期) | | 前端 Cron 配置 | 可视化优先 + 高级模式 | 临床团队无需学习 Cron 语法 | | 实时质控防抖 | 5 分钟(pg-boss singleton) | 避免快速连续保存时重复执行 | | `QcExecutor` 统一入口 | 三种触发模式共用一个执行器 | 确保三级数据更新逻辑一致 | --- > **一句话总结**:研究方案定义了"查什么",变量清单定义了"查哪些字段",质控规则定义了"怎么查",三级数据结构记录了"查的结果",质控报告汇总了"结论是什么"——这条链路从头到尾 1:1 对齐,是 CRA Agent 准确运行的根基。