Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md
HaHafeng 2481b786d8 deploy: Complete 0126-27 deployment - database upgrade, services update, code recovery
Major Changes:
- Database: Install pg_bigm/pgvector plugins, create test database
- Python service: v1.0 -> v1.1, add pymupdf4llm/openpyxl/pypandoc
- Node.js backend: v1.3 -> v1.7, fix pino-pretty and ES Module imports
- Frontend: v1.2 -> v1.3, skip TypeScript check for deployment
- Code recovery: Restore empty files from local backup

Technical Fixes:
- Fix pino-pretty error in production (conditional loading)
- Fix ES Module import paths (add .js extensions)
- Fix OSSAdapter TypeScript errors
- Update Prisma Schema (63 models, 16 schemas)
- Update environment variables (DATABASE_URL, EXTRACTION_SERVICE_URL, OSS)
- Remove deprecated variables (REDIS_URL, DIFY_API_URL, DIFY_API_KEY)

Documentation:
- Create 0126 deployment folder with 8 documents
- Update database development standards v2.0
- Update SAE deployment status records

Deployment Status:
- PostgreSQL: ai_clinical_research_test with plugins
- Python: v1.1 @ 172.17.173.84:8000
- Backend: v1.7 @ 172.17.173.89:3001
- Frontend: v1.3 @ 172.17.173.90:80

Tested: All services running successfully on SAE
2026-01-27 08:13:27 +08:00

31 KiB
Raw Permalink Blame History

REDCap对接技术方案与实施指南

版本: V1.0
创建日期: 2026-01-02
适用阶段: IIT Manager Agent MVP - Day 2
文档性质: 核心技术基石
重要程度:


📋 文档目标

本文档是IIT Manager Agent与REDCap集成的权威技术指南,明确:

  1. REDCap对接方式的技术选型
  2. Data Entry Trigger (DET) 的验证与配置
  3. REST API的使用方法
  4. 实时质控的完整架构
  5. Day 2的开发实施步骤

🎯 核心结论Executive Summary

技术选型DET实时触发 + REST API数据读写

不采用: External Module (EM)
原因: EM需要PHP开发、侵入REDCap源码、维护成本高、不适合外部系统集成

采用方案:

组件 技术 用途 实时性
Data Entry Trigger REDCap原生功能 数据保存时实时通知 0秒延迟
REST API (Export) HTTP + JSON 拉取数据、元数据 按需调用
REST API (Import) HTTP + JSON 回写质控意见Phase 2 按需调用
轮询机制 pg-boss定时任务 补充DET遗漏数据 30分钟

🔍 技术调研结果

调研1REDCap提供的对接方式

调研来源:

  • REDCap 15.8.0源码
  • External Module Framework官方文档
  • REDCap二次开发深度指南

发现的对接方式:

方式 技术栈 适用场景 我们是否适用
External Module PHP + Hook REDCap内部功能扩展、UI定制 不适用
REST API HTTP + JSON 外部系统集成、数据同步 适用
Data Entry Trigger Webhook 实时通知外部系统 适用

调研2Data Entry Trigger (DET) 验证

源码位置:

/var/www/html/redcap/redcap_v15.8.0/Classes/DataEntry.php
/var/www/html/redcap/redcap_v15.8.0/ProjectSetup/index.php
/var/www/html/redcap/redcap_v15.8.0/ControlCenter/modules_settings.php

关键代码:

// DataEntry.php 核心实现
global $data_entry_trigger_url, $data_entry_trigger_enabled;
if (!$data_entry_trigger_enabled || $data_entry_trigger_url == '') {
    return;
}
// 发送HTTP POST到配置的URL
$full_url = $pre_url . $data_entry_trigger_url;

验证结论: DET是REDCap原生功能真实存在且广泛使用


🏗️ 完整架构设计

架构图

┌─────────────────────────────────────────────────────┐
│  1. CRC 在 REDCap 中保存记录                         │
│     - 录入患者数据                                   │
│     - 点击"Save"按钮                                 │
└──────────────────┬──────────────────────────────────┘
                   ↓ 立即触发REDCap DET0秒延迟
┌─────────────────────────────────────────────────────┐
│  2. REDCap Data Entry Trigger (DET)                 │
│     POST → https://iit.xunzhengyixue.com/           │
│              api/v1/iit/webhooks/redcap             │
│     Payload:                                         │
│       - project_id: 16                               │
│       - record: 101                                  │
│       - instrument: demographics                     │
│       - redcap_event_name: baseline_arm_1            │
└──────────────────┬──────────────────────────────────┘
                   ↓ 1秒内
┌─────────────────────────────────────────────────────┐
│  3. Node.js Webhook接收器                           │
│     - 验证请求来源(可选)                           │
│     - 立即返回200 OK<100ms不阻塞REDCap        │
│     - 异步调用质控流程setImmediate               │
└──────────────────┬──────────────────────────────────┘
                   ↓ 异步处理
┌─────────────────────────────────────────────────────┐
│  4. REDCap REST API - exportRecords                 │
│     - 使用API Token认证                             │
│     - 拉取指定record_id的完整数据                   │
│     - 包含所有字段值、元数据                         │
└──────────────────┬──────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────────────────┐
│  5. 推送到质控队列                                   │
│     - jobQueue.send('iit:quality-check', record)    │
│     - 异步处理,不影响实时响应                       │
└──────────────────┬──────────────────────────────────┘
                   ↓ 由QualityCheckAgent处理Day 6
┌─────────────────────────────────────────────────────┐
│  6. AI质控分析                                       │
│     - 规则引擎验证                                   │
│     - AI推理检测异常                                 │
│     - 生成质控报告                                   │
└──────────────────┬──────────────────────────────────┘
                   ↓ 3-5秒内
┌─────────────────────────────────────────────────────┐
│  7. 多渠道反馈                                       │
│     A. 企业微信推送给CRC实时通知                 │
│     B. REDCap API importQueries写回质疑          │
│     C. IIT Manager前端显示待办事项               │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  补充定时轮询机制每30分钟                      │
│     - 防止DET遗漏数据                                │
│     - 使用dateRangeBegin增量拉取                    │
│     - 推送到相同的质控队列                           │
└─────────────────────────────────────────────────────┘

实时性对比

方案 延迟时间 我们的选择
纯轮询每5分钟 最高5分钟 不满足实时性要求
纯轮询每1分钟 最高1分钟 ⚠️ 资源浪费、延迟仍高
DET + API 0秒触发 + 3-5秒处理 最优方案
DET + 轮询补充 实时 + 可靠 最终架构

结论: CRC点击"保存" → 5秒内收到企业微信通知


📦 技术实现详解

1. Data Entry Trigger (DET) 配置

步骤1在Control Center启用DET

1. 登录REDCaphttp://localhost:8080/
2. 使用管理员账户Admin / Admin123!
3. 进入Control Center → "Additional Customizations"
4. 找到:"Data Entry Trigger" 选项
5. 设置为:"Enabled"
6. 保存配置

数据库字段: redcap_config.data_entry_trigger_enabled = 1

步骤2在项目中配置Webhook URL

1. 进入测试项目test0102 (PID 16)
2. 左侧菜单 → Project Setup
3. 找到:"Additional Customizations"
4. 展开:"Data Entry Trigger URL"
5. 填入Webhook URL
   - 开发环境http://localhost:3001/api/v1/iit/webhooks/redcap
   - 测试环境https://backend-dev.xunzhengyixue.com/api/v1/iit/webhooks/redcap
   - 生产环境https://iit.xunzhengyixue.com/api/v1/iit/webhooks/redcap
6. 保存

数据库字段: redcap_projects.data_entry_trigger_url

步骤3验证DET配置

使用RequestBin/ngrok验证

# 使用ngrok创建临时公网URL开发测试用
ngrok http 3001

# 将ngrok URL配置到REDCap DET
# 例如https://1234-abc-def.ngrok.io/api/v1/iit/webhooks/redcap

# 在REDCap中保存一条记录
# 检查ngrok控制台是否收到POST请求

DET的POST Payload格式

REDCap发送的请求

POST /api/v1/iit/webhooks/redcap HTTP/1.1
Host: iit.xunzhengyixue.com
Content-Type: application/x-www-form-urlencoded
User-Agent: REDCap/15.8.0

project_id=16
&record=101
&instrument=demographics
&redcap_event_name=baseline_arm_1
&demographics_complete=2
&redcap_url=http://localhost:8080/

字段说明:

字段 类型 说明 示例
project_id string REDCap项目ID "16"
record string 记录ID "101"
instrument string 表单名称 "demographics"
redcap_event_name string 事件名称(纵向研究) "baseline_arm_1"
[instrument]_complete string 表单完成状态0/1/2 "2"
redcap_url string REDCap实例URL "http://localhost:8080/"

2. REDCap REST API 使用

API端点

http://localhost:8080/api/

API认证Token

生成步骤:

1. 登录REDCap
2. 进入项目test0102 (PID 16)
3. 左侧菜单 → "API"
4. 点击 "Generate API Token"
5. 复制Token32位字符串
6. 保存到环境变量:
   REDCAP_API_TOKEN_TEST=YOUR_TOKEN_HERE

Token存储

  • 环境变量(推荐)
  • 数据库加密字段IitProject.redcapApiToken
  • 不要提交到Git

API方法1导出记录 (exportRecords)

用途: 拉取数据(支持增量同步)

请求示例curl

curl -X POST http://localhost:8080/api/ \
  -F "token=YOUR_API_TOKEN" \
  -F "content=record" \
  -F "format=json" \
  -F "type=flat" \
  -F "records[0]=101" \
  -F "records[1]=102" \
  -F "dateRangeBegin=2026-01-01 00:00:00"

参数说明:

参数 必填 说明 示例
token API Token "ABC123..."
content 固定值 "record"
format 返回格式 "json"
type 数据结构 "flat"
records 指定记录ID ["101", "102"]
fields 指定字段 ["age", "gender"]
dateRangeBegin 时间过滤(增量同步关键) "2026-01-01 00:00:00"
dateRangeEnd 结束时间 "2026-01-31 23:59:59"

响应示例:

[
  {
    "record_id": "101",
    "redcap_event_name": "baseline_arm_1",
    "first_name": "张",
    "last_name": "三",
    "age": "30",
    "gender": "1",
    "demographics_complete": "2"
  }
]

API方法2导出元数据 (exportMetadata)

用途: 获取字段定义、表单结构

请求示例:

curl -X POST http://localhost:8080/api/ \
  -F "token=YOUR_API_TOKEN" \
  -F "content=metadata" \
  -F "format=json"

响应示例:

[
  {
    "field_name": "age",
    "form_name": "demographics",
    "section_header": "",
    "field_type": "text",
    "field_label": "Age",
    "select_choices_or_calculations": "",
    "field_note": "",
    "text_validation_type_or_show_slider_number": "integer",
    "text_validation_min": "0",
    "text_validation_max": "120",
    "identifier": "",
    "branching_logic": "",
    "required_field": "y",
    "custom_alignment": "",
    "question_number": "",
    "matrix_group_name": "",
    "matrix_ranking": "",
    "field_annotation": ""
  }
]

API方法3导入记录 (importRecords) - Phase 2

用途: 回写数据如质控意见、AI建议

请求示例:

curl -X POST http://localhost:8080/api/ \
  -F "token=YOUR_API_TOKEN" \
  -F "content=record" \
  -F "format=json" \
  -F "type=flat" \
  -F "overwriteBehavior=normal" \
  -F 'data=[{"record_id":"101","ai_review":"数据异常"}]'

3. Node.js实现代码

3.1 RedcapAdapterAPI适配器

文件: backend/src/modules/iit-manager/adapters/RedcapAdapter.ts

import axios from 'axios';
import FormData from 'form-data';
import { logger } from '@/common/logging';

export interface RedcapExportOptions {
  records?: string[];
  fields?: string[];
  dateRangeBegin?: Date;
  dateRangeEnd?: Date;
}

export class RedcapAdapter {
  private baseUrl: string;
  private apiToken: string;
  private timeout: number;

  constructor(baseUrl: string, apiToken: string, timeout = 30000) {
    this.baseUrl = baseUrl.replace(/\/$/, ''); // 移除末尾斜杠
    this.apiToken = apiToken;
    this.timeout = timeout;
  }

  /**
   * 导出记录(支持增量同步)
   */
  async exportRecords(options: RedcapExportOptions = {}): Promise<any[]> {
    const formData = new FormData();
    formData.append('token', this.apiToken);
    formData.append('content', 'record');
    formData.append('format', 'json');
    formData.append('type', 'flat');

    // 指定记录ID
    if (options.records && options.records.length > 0) {
      options.records.forEach((recordId, index) => {
        formData.append(`records[${index}]`, recordId);
      });
    }

    // 指定字段
    if (options.fields && options.fields.length > 0) {
      options.fields.forEach((field, index) => {
        formData.append(`fields[${index}]`, field);
      });
    }

    // 时间过滤(增量同步关键)
    if (options.dateRangeBegin) {
      const dateStr = this.formatRedcapDate(options.dateRangeBegin);
      formData.append('dateRangeBegin', dateStr);
    }

    if (options.dateRangeEnd) {
      const dateStr = this.formatRedcapDate(options.dateRangeEnd);
      formData.append('dateRangeEnd', dateStr);
    }

    try {
      const response = await axios.post(
        `${this.baseUrl}/api/`,
        formData,
        {
          headers: formData.getHeaders(),
          timeout: this.timeout
        }
      );

      if (!Array.isArray(response.data)) {
        throw new Error('Invalid response format');
      }

      logger.info(`REDCap API: Exported ${response.data.length} records`);
      return response.data;

    } catch (error) {
      logger.error('REDCap API exportRecords failed:', error);
      throw new Error(`REDCap API error: ${error.message}`);
    }
  }

  /**
   * 导出元数据(字段定义)
   */
  async exportMetadata(): Promise<any[]> {
    const formData = new FormData();
    formData.append('token', this.apiToken);
    formData.append('content', 'metadata');
    formData.append('format', 'json');

    try {
      const response = await axios.post(
        `${this.baseUrl}/api/`,
        formData,
        {
          headers: formData.getHeaders(),
          timeout: this.timeout
        }
      );

      logger.info(`REDCap API: Exported ${response.data.length} metadata fields`);
      return response.data;

    } catch (error) {
      logger.error('REDCap API exportMetadata failed:', error);
      throw new Error(`REDCap API error: ${error.message}`);
    }
  }

  /**
   * 导入记录(回写数据)- Phase 2
   */
  async importRecords(records: any[]): Promise<void> {
    const formData = new FormData();
    formData.append('token', this.apiToken);
    formData.append('content', 'record');
    formData.append('format', 'json');
    formData.append('type', 'flat');
    formData.append('overwriteBehavior', 'normal');
    formData.append('data', JSON.stringify(records));

    try {
      await axios.post(
        `${this.baseUrl}/api/`,
        formData,
        {
          headers: formData.getHeaders(),
          timeout: this.timeout
        }
      );

      logger.info(`REDCap API: Imported ${records.length} records`);

    } catch (error) {
      logger.error('REDCap API importRecords failed:', error);
      throw new Error(`REDCap API error: ${error.message}`);
    }
  }

  /**
   * 格式化日期为REDCap格式YYYY-MM-DD HH:MM:SS
   */
  private formatRedcapDate(date: Date): string {
    return date
      .toISOString()
      .replace('T', ' ')
      .substring(0, 19);
  }
}

3.2 WebhookControllerWebhook接收器

文件: backend/src/modules/iit-manager/controllers/webhookController.ts

import { FastifyRequest, FastifyReply } from 'fastify';
import { prisma } from '@/config/database';
import { logger } from '@/common/logging';
import { jobQueue } from '@/common/jobs';
import { RedcapAdapter } from '../adapters/RedcapAdapter';

export class WebhookController {
  /**
   * 接收REDCap Data Entry Trigger Webhook
   * 
   * 性能要求:响应时间 < 100ms不阻塞REDCap
   */
  async handleRedcapWebhook(
    request: FastifyRequest,
    reply: FastifyReply
  ): Promise<void> {
    const startTime = Date.now();

    // 解析Webhook Payload
    const payload = request.body as Record<string, any>;
    const {
      project_id,
      record,
      instrument,
      redcap_event_name,
      redcap_url
    } = payload;

    // 基本验证
    if (!project_id || !record) {
      reply.code(400).send({
        error: 'Missing required fields: project_id, record'
      });
      return;
    }

    logger.info('REDCap DET webhook received:', {
      project_id,
      record,
      instrument,
      event: redcap_event_name
    });

    // 立即返回200不阻塞REDCap
    const responseTime = Date.now() - startTime;
    reply.code(200).send({
      status: 'received',
      timestamp: new Date().toISOString(),
      response_time_ms: responseTime
    });

    // 异步处理不阻塞HTTP响应
    setImmediate(async () => {
      try {
        await this.processWebhook({
          project_id,
          record,
          instrument,
          redcap_event_name,
          redcap_url
        });
      } catch (error) {
        logger.error('Webhook processing failed:', error);
        // 不抛出错误因为已经返回200了
      }
    });
  }

  /**
   * 异步处理Webhook
   */
  private async processWebhook(payload: {
    project_id: string;
    record: string;
    instrument: string;
    redcap_event_name?: string;
    redcap_url?: string;
  }): Promise<void> {
    const { project_id, record, instrument } = payload;

    // 1. 查找项目配置
    const project = await prisma.iitProject.findFirst({
      where: { redcapProjectId: project_id }
    });

    if (!project) {
      logger.warn(`Unknown REDCap project_id: ${project_id}`);
      return;
    }

    // 2. 幂等性检查(防止重复处理)
    const isDuplicate = await this.checkDuplicate(
      project.id,
      record,
      instrument
    );

    if (isDuplicate) {
      logger.info(`Duplicate webhook ignored: ${record}`);
      return;
    }

    // 3. 调用REDCap API拉取完整数据
    const adapter = new RedcapAdapter(
      project.redcapBaseUrl,
      project.redcapApiToken
    );

    const records = await adapter.exportRecords({
      records: [record]  // 只拉取这一条记录
    });

    if (records.length === 0) {
      logger.warn(`No data found for record: ${record}`);
      return;
    }

    // 4. 推送到质控队列
    await jobQueue.send('iit:quality-check', {
      projectId: project.id,
      record: records[0],
      trigger: 'webhook',
      instrument,
      timestamp: new Date().toISOString()
    });

    logger.info(`Webhook processed successfully: project=${project_id}, record=${record}`);

    // 5. 记录审计日志
    await prisma.iitAuditLog.create({
      data: {
        projectId: project.id,
        action: 'WEBHOOK_RECEIVED',
        targetType: 'RECORD',
        targetId: record,
        details: {
          instrument,
          trigger: 'redcap_det'
        }
      }
    });
  }

  /**
   * 幂等性检查防止5分钟内重复处理相同记录
   */
  private async checkDuplicate(
    projectId: string,
    recordId: string,
    instrument: string
  ): Promise<boolean> {
    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);

    const recentLog = await prisma.iitAuditLog.findFirst({
      where: {
        projectId,
        action: 'WEBHOOK_RECEIVED',
        targetId: recordId,
        createdAt: { gte: fiveMinutesAgo },
        details: {
          path: ['instrument'],
          equals: instrument
        }
      }
    });

    return recentLog !== null;
  }
}

3.3 SyncManager轮询补充

文件: backend/src/modules/iit-manager/services/SyncManager.ts

import { prisma } from '@/config/database';
import { logger } from '@/common/logging';
import { jobQueue } from '@/common/jobs';
import { RedcapAdapter } from '../adapters/RedcapAdapter';

export class SyncManager {
  /**
   * 初始化同步(注册定时任务)
   */
  async initializeSync(projectId: string, interval: string = '*/30 * * * *'): Promise<void> {
    // 注册定时任务每30分钟
    await jobQueue.schedule(
      'iit:redcap:poll',
      interval,
      { projectId }
    );

    logger.info(`Polling sync initialized for project ${projectId}, interval: ${interval}`);
  }

  /**
   * 处理轮询(拉取增量数据)
   */
  async handlePoll(projectId: string): Promise<void> {
    logger.info(`Starting poll sync for project ${projectId}`);

    try {
      // 1. 获取项目配置
      const project = await prisma.iitProject.findUnique({
        where: { id: projectId }
      });

      if (!project) {
        throw new Error(`Project not found: ${projectId}`);
      }

      // 2. 获取上次同步时间
      const lastSyncAt = project.lastSyncAt || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

      logger.info(`Last sync: ${lastSyncAt.toISOString()}`);

      // 3. 拉取增量数据
      const adapter = new RedcapAdapter(
        project.redcapBaseUrl,
        project.redcapApiToken
      );

      const records = await adapter.exportRecords({
        dateRangeBegin: lastSyncAt
      });

      logger.info(`Pulled ${records.length} records from REDCap`);

      // 4. 推送到质控队列
      for (const record of records) {
        await jobQueue.send('iit:quality-check', {
          projectId,
          record,
          trigger: 'polling',
          timestamp: new Date().toISOString()
        });
      }

      // 5. 更新同步时间
      await prisma.iitProject.update({
        where: { id: projectId },
        data: { lastSyncAt: new Date() }
      });

      logger.info(`Poll sync completed for project ${projectId}`);

    } catch (error) {
      logger.error(`Poll sync failed for project ${projectId}:`, error);
      throw error;
    }
  }

  /**
   * 停止同步(取消定时任务)
   */
  async stopSync(projectId: string): Promise<void> {
    // pg-boss不支持直接取消schedule需要在Worker中检查项目状态
    logger.info(`Sync stopped for project ${projectId}`);
  }
}

3.4 路由配置

文件: backend/src/modules/iit-manager/routes/index.ts

import { FastifyInstance } from 'fastify';
import { WebhookController } from '../controllers/webhookController';
import { SyncManager } from '../services/SyncManager';

export async function iitManagerRoutes(fastify: FastifyInstance) {
  const webhookController = new WebhookController();
  const syncManager = new SyncManager();

  // Webhook端点接收REDCap DET
  fastify.post('/webhooks/redcap', async (request, reply) => {
    return webhookController.handleRedcapWebhook(request, reply);
  });

  // 手动触发同步(测试用)
  fastify.post('/projects/:id/sync', async (request, reply) => {
    const { id } = request.params as { id: string };
    await syncManager.handlePoll(id);
    return { status: 'synced' };
  });
}

3.5 Worker注册

文件: backend/src/modules/iit-manager/index.ts

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

export async function initializeIITManager() {
  const syncManager = new SyncManager();

  // 注册轮询Worker
  await jobQueue.work('iit:redcap:poll', async (job) => {
    const { projectId } = job.data;
    await syncManager.handlePoll(projectId);
  });

  logger.info('IIT Manager initialized: Poll worker registered');
}

🧪 测试验证

测试1验证DET配置

步骤:

  1. 配置ngrokngrok http 3001
  2. 将ngrok URL配置到REDCap DET
  3. 在REDCap中保存一条记录
  4. 检查ngrok控制台是否收到POST请求

预期结果:

  • ngrok收到POST请求
  • Payload包含project_id、record等字段

测试2验证API Token

测试脚本: test-redcap-api.ts

import { RedcapAdapter } from './adapters/RedcapAdapter';

async function testRedcapAPI() {
  const adapter = new RedcapAdapter(
    'http://localhost:8080',
    process.env.REDCAP_API_TOKEN_TEST!
  );

  // 测试导出记录
  const records = await adapter.exportRecords();
  console.log('Records:', records);

  // 测试导出元数据
  const metadata = await adapter.exportMetadata();
  console.log('Metadata fields:', metadata.length);

  console.log('✅ REDCap API test passed!');
}

testRedcapAPI();

测试3端到端集成测试

测试脚本: test-redcap-integration.ts

async function testIntegration() {
  // 1. 创建测试项目
  const project = await prisma.iitProject.create({
    data: {
      name: 'test0102',
      redcapBaseUrl: 'http://localhost:8080',
      redcapApiToken: process.env.REDCAP_API_TOKEN_TEST!,
      redcapProjectId: '16',
      status: 'ACTIVE'
    }
  });

  // 2. 初始化同步
  const syncManager = new SyncManager();
  await syncManager.initializeSync(project.id);

  // 3. 手动触发一次轮询
  await syncManager.handlePoll(project.id);

  // 4. 模拟Webhook
  const webhookController = new WebhookController();
  await webhookController.handleRedcapWebhook(
    {
      body: {
        project_id: '16',
        record: '101',
        instrument: 'demographics'
      }
    } as any,
    {} as any
  );

  console.log('✅ Integration test passed!');
}

📋 Day 2实施清单

阶段1环境准备30分钟

  • 在REDCap Control Center启用DET功能
  • 在test0102项目生成API Token
  • 配置环境变量:
    REDCAP_BASE_URL=http://localhost:8080
    REDCAP_API_TOKEN_TEST=YOUR_TOKEN_HERE
    
  • 使用curl测试API是否可用

阶段2开发RedcapAdapter1.5小时)

  • 创建文件:adapters/RedcapAdapter.ts
  • 实现 exportRecords() 方法
  • 实现 exportMetadata() 方法
  • 实现 formatRedcapDate() 工具方法
  • 编写单元测试:RedcapAdapter.test.ts

阶段3开发WebhookController2小时

  • 创建文件:controllers/webhookController.ts
  • 实现 handleRedcapWebhook() 方法
  • 实现 processWebhook() 私有方法
  • 实现 checkDuplicate() 幂等性检查
  • 配置路由:POST /api/v1/iit/webhooks/redcap

阶段4开发SyncManager1.5小时)

  • 创建文件:services/SyncManager.ts
  • 实现 initializeSync() 方法
  • 实现 handlePoll() 方法
  • 实现 stopSync() 方法
  • 注册Workeriit:redcap:poll

阶段5集成测试1小时

  • 配置ngrok/RequestBin测试DET
  • 运行 test-redcap-api.ts
  • 运行 test-redcap-integration.ts
  • 验证端到端流程
  • 记录测试结果

阶段6文档更新30分钟

  • 更新MVP开发任务清单Day 2完成
  • 记录API Token和配置信息
  • 更新架构图
  • 提交Git

⚠️ 关键注意事项

1. DET配置要点

常见错误:

  • 忘记在Control Center启用DET全局开关
  • Webhook URL填写错误多了斜杠、少了路径
  • 本地开发环境无法接收外网Webhook

解决方案:

  • 使用ngrok创建临时公网URL测试
  • 开发环境可先用RequestBin验证Payload格式
  • 生产环境确保URL可访问防火墙、HTTPS

2. API Token安全

安全实践:

  • 存储在环境变量或数据库加密字段
  • 定期轮换Token
  • 最小权限原则(只开启需要的权限)
  • 不要提交到Git
  • 不要在日志中打印完整Token

3. 性能优化

Webhook响应时间

  • 必须 < 100ms立即返回200
  • 使用 setImmediate() 异步处理
  • 不要在Webhook中执行耗时操作

API调用频率

  • 增量拉取使用dateRangeBegin
  • 指定字段(减少数据量)
  • 合理设置timeout30秒

4. 错误处理

DET Webhook失败

  • REDCap会重试3次间隔1分钟
  • 如果仍失败REDCap会记录到日志
  • 我们的轮询机制可以补充遗漏的数据

API调用失败

  • 实现重试机制3次指数退避
  • 记录失败日志
  • 告警通知管理员

📊 成功验收标准

Day 2完成标准

  • REDCap API Token已生成并验证
  • RedcapAdapter可以拉取测试数据
  • DET已配置并成功接收Webhook
  • Webhook可以触发API拉取
  • 轮询机制可以定时运行
  • 数据推送到质控队列
  • 单元测试全部通过
  • 端到端集成测试通过

性能指标

  • Webhook响应时间 < 100ms
  • API调用成功率 > 99%
  • 端到端延迟 < 5秒DET触发 → 企微通知)

🔗 相关文档

  • MVP开发任务清单 MVP开发任务清单.md
  • 完整技术方案: IIT Manager Agent 完整技术开发方案 (V1.1).md
  • REDCap二次开发指南 ../../Redcap/03-API对接与开发/33-REDCap二次开发深度指南.md
  • REDCap部署手册 ../../Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md

📝 更新日志

日期 版本 更新内容 更新人
2026-01-02 V1.0 初始版本,完成技术调研和方案设计 AI Assistant

这是IIT Manager Agent的技术基石文档请妥善保管