feat(iit): QC deep fix + V3.1 architecture plan + project member management

QC System Deep Fix:
- HardRuleEngine: add null tolerance + field availability pre-check (skipped status)
- SkillRunner: baseline data merge for follow-up events + field availability check
- QcReportService: record-level pass rate calculation + accurate LLM XML report
- iitBatchController: legacy log cleanup (eventId=null) + upsert RecordSummary
- seed-iit-qc-rules: null/empty string tolerance + applicableEvents config

V3.1 Architecture Design (docs only, no code changes):
- QC engine V3.1 plan: 5-level data structure (CDISC ODM) + D1-D7 dimensions
- Three-batch implementation strategy (A: foundation, B: bubbling, C: new engines)
- Architecture team review: 4 whitepapers reviewed + feedback doc + 4 critical suggestions
- CRA Agent strategy roadmap + CRA 4-tool explanation doc for clinical experts

Project Member Management:
- Cross-tenant member search and assignment (remove tenant restriction)
- IIT project detail page enhancement with tabbed layout (KB + members)
- IitProjectContext for business-side project selection
- System-KB route access control adjustment for project operators

Frontend:
- AdminLayout sidebar menu restructure
- IitLayout with project context provider
- IitMemberManagePage new component
- Business-side pages adapt to project context

Prisma:
- 2 new migrations (user-project RBAC + is_demo flag)
- Schema updates for project member management

Made-with: Cursor
This commit is contained in:
2026-03-01 15:27:05 +08:00
parent c3f7d54fdf
commit 0b29fe88b5
61 changed files with 6877 additions and 524 deletions

View File

@@ -0,0 +1,697 @@
# 质控引擎架构升级 — 三级数据结构与多模式触发技术设计
> **版本:** 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 DETData Entry TriggerWebhook 接收端,但:
- 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_reportsLLM 友好报告 + 人类可读报告)
```
---
## 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_check5 分钟防抖)
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<void> {
// 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<BatchResult> {
// 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<void> {
// 事件级:从 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
<qc_context project_id="xxx" project_name="test0207">
<summary>
- 通过率: 92.9%14 条记录13 条通过1 条失败)
- 事件覆盖: 42 个事件已质控39 个通过
- 严重问题: 1 | 警告: 0
</summary>
<critical_issues>
<record id="3">
<event name="基线期">
1. [exc_001] **排除标准检查**: 当前值 **1** (标准: 0)
</event>
</record>
</critical_issues>
<!-- 新增:事件维度统计 -->
<event_overview>
- 筛选期: 14/14 通过 (100%)
- 基线期: 13/14 通过 (92.9%)
- 随访1: 12/12 通过 (100%) <!-- 2条记录尚未到达随访1 -->
</event_overview>
</qc_context>
```
---
## 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 准确运行的根基。