资讯动态

临床研究EDC系统源码交付:数据模型、审计链与合规避坑指南

发布时间:2026/10/15 16:06:14 来源:尧图企业网站定制
简介这是一套临床研究领域 EDCElectronic Data Capture系统的完整 Java 源码面向需要搭建或二次开发临床试验数据采集平台的开发者也适合希望结合代码理解 CRF 表单设计、数据校验、疑问管理等流程的临床研究技术人员。压缩包共 6 个文件其中 4 个 Java 文件构成核心逻辑2 个 scc 文件为版本控制辅助文件包体仅 17KB。资源按 input 与 handquery 两个模块组织前者侧重受试者数据的录入入口后者实现灵活的自定义查询条件结构清晰便于快速定位。已有 1728 人学习/下载适合具备一定 Java 基础并了解临床研究业务流程的读者。通过这套源码学习者可以观察 EDC 系统从临床试验项目管理、CRF 采集到逻辑自动校验、疑问处理、PDF 导出、统计报表的完整功能脉络并直接以现有代码为起点扩展业务是一份小而精的学习与二次开发参考。1. 临床研究的数据采集管理EDC系统为什么“完整源代码”才是入场券临床研究的数据采集管理EDC系统最容易被低估的是“数据可信”这四个字病例报告表从纸质换成网页填写之后数据是否被改过、改的理由是否有记录、质疑是否闭环每一个问题都得有据可查。商业SaaS的EDC按研究数和账号数收年费两个三年期的多中心研究做下来订阅费累计往往已经够持有完整源代码私有化部署、二次开发、对接院内系统从此不再受制于人。这篇笔记写给准备自建或采购源码交付的团队包括CRO、数据管理外包、药企信息部门和做医疗信息化交付的开发者。我按实际交付EDC的顺序讲先拆数据模型再跑通最小闭环然后解决审计、签名、盲态这些合规硬模块最后给一份源码验收时要先执行的检查清单。2. EDC系统的骨架研究树、动态表单与表单状态机2.1 树形数据模型研究-中心-受试者-访视所有EDC系统的数据组织方式几乎一样一条临床试验是一个研究Study一个研究下面挂多个临床中心Site每个中心招募一批受试者Subject每个受试者按方案访视Visit每个访视里需要填写若干张病例报告表eCRF/Form。这套树形模型决定了权限怎么隔离、数据怎么聚合、导出怎么分组。权限在Site层隔离统计在Study层聚合表单实例挂在Subject Visit 之下。我一般会给前端提供一个聚合接口一次请求返回某个受试者的完整访视树。接口返回结构大致是{ studyNo: DEMO-001, siteNo: S001, subjectNo: 1001, visits: [ { visitCode: V01, visitName: 筛选期, visitDate: 2024-03-11, status: LOCKED, forms: [ { formCode: DEMOGRAPHY, title: 人口学, status: VERIFIED }, { formCode: INCLUSION, title: 入选标准, status: COMPLETED } ] }, { visitCode: V02, visitName: 治疗期第1天, visitDate: 2024-03-18, status: IN_PROGRESS, forms: [ { formCode: VITALS, title: 生命体征, status: DRAFT } ] } ] }这份JSON是列表页的核心数据源也是后续状态机、疑问管理、数据导出的入口。注意visitDate这个字段它是临床方案的访视日期而不是数据录入时间后面避坑章我会专门讲它为什么容易出错。树形模型在数据库里就是几张带外键的表真正难的地方是每个节点上的状态流转和权限控制而不是树的查询本身。2.2 eCRF表单引擎定义与实例分离用JSON描述一张病例报告表EDC和普通在线表单工具最大的区别是“定义”与“实例”分离。一张不良事件表AE表在方案里被定义一次但每个受试者每个访视都会生成一份独立的实例。修改表单定义不能影响已录入的数据所以在建库设计时表单定义Form Definition和表单实例Form Instance必须分开存储。表单定义我推荐直接用JSON描述而不是为每个字段建一张关系表。这样表单设计器、前端渲染、逻辑核查都可以共用同一份结构。一个最小AE页面的定义长这样{ formCode: AE, version: 1.2, fields: [ { code: ae_start_date, type: date, required: true, label: 开始日期 }, { code: ae_severity, type: radio, label: 严重程度, options: [ { value: MILD, label: 轻度 }, { value: MODERATE, label: 中度 }, { value: SEVERE, label: 重度 } ] }, { code: ae_action, type: textarea, maxLength: 200, label: 处理措施 } ], editChecks: [ { code: EC_AE_DATE, field: ae_start_date, operator: lte, compareTo: ae_end_date, message: 开始日期不能晚于结束日期, severity: ERROR } ] }severity三级是EDC的专用语义ERROR表示阻止保存WARNING表示允许保存但自动生成一条质疑QueryINFO只做提示不落质疑。这个设计直接影响后面保存接口的事务逻辑。JSON定义有个容易被忽视的点version字段必须保留表单改动不是直接覆盖而是发布一个新版本旧实例继续绑定旧版本这一点在避坑章会展开。2.3 数据状态机Draft → Completed → Verified → Locked每一步都要说清谁有权改表单实例不能像普通业务表一样随便更新它的状态决定了哪些角色可以编辑、哪些操作会产生审计记录。我常用的状态机如下状态含义允许的操作允许的角色DRAFT草稿录入中编辑、保存、提交CRC / 研究者COMPLETED已提交待核查只读CRA可发起SDVCRAIN_QUERY有未关闭的质疑回复质疑、编辑保存研究中心VERIFIEDCRA已完成源数据核查只读CRA / DMLOCKED锁库数据冻结解锁需电子签名DM / 项目管理员从DRAFT到COMPLETED是研究者提交从COMPLETED到VERIFIED是CRA核查通过从VERIFIED到LOCKED是数据管理员锁库。任何一次状态跳转都必须写审计日志LOCKED之后的数据修改必须走“解锁→修改→重新锁定”流程而且解锁和重新锁定都要电子签名。这套状态机看起来简单但它是EDC能通过稽查的核心依据。有些系统还保留了双录对比两个录入员分别录一遍再比对差异是同一套状态机上的附加流程并不影响主干。3. 最小EDC建库与一次保存的完整闭环3.1 最小建库脚本8张表把研究对象与eCRF数据串起来建库是整个EDC项目里最不能省的一步。我给出的最小DDL不追求覆盖全部功能而是把树形模型、表单定义、实例数据、质疑管理这四条主线一次打通后续加电子签名、稽查轨迹都是在这些表上扩展。CREATE TABLE study ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_no VARCHAR(50) NOT NULL UNIQUE, study_name VARCHAR(200) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT DRAFT -- DRAFT / ACTIVE / CLOSED ); CREATE TABLE site ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT NOT NULL, site_no VARCHAR(20) NOT NULL, site_name VARCHAR(100) NOT NULL, UNIQUE KEY uk_site (study_id, site_no), CONSTRAINT fk_site_study FOREIGN KEY (study_id) REFERENCES study(id) ); CREATE TABLE subject ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT NOT NULL, site_id BIGINT NOT NULL, subject_no VARCHAR(20) NOT NULL, enroll_date DATE, status VARCHAR(20) NOT NULL DEFAULT ENROLLED, UNIQUE KEY uk_subject (study_id, subject_no), CONSTRAINT fk_subject_site FOREIGN KEY (site_id) REFERENCES site(id) ); CREATE TABLE visit_definition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT NOT NULL, visit_code VARCHAR(20) NOT NULL, visit_name VARCHAR(100) NOT NULL, day_lower INT, -- 访视窗口下限如第21天 day_upper INT, -- 访视窗口上限如第35天 UNIQUE KEY uk_visit (study_id, visit_code), CONSTRAINT fk_visit_study FOREIGN KEY (study_id) REFERENCES study(id) ); CREATE TABLE form_definition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT NOT NULL, form_code VARCHAR(50) NOT NULL, title VARCHAR(200) NOT NULL, version INT NOT NULL DEFAULT 1, schema_json JSON NOT NULL, -- 2.2节的表单定义JSON status VARCHAR(20) NOT NULL DEFAULT DRAFT, UNIQUE KEY uk_form_def (study_id, form_code, version), CONSTRAINT fk_form_def_study FOREIGN KEY (study_id) REFERENCES study(id) ); CREATE TABLE form_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT NOT NULL, site_id BIGINT NOT NULL, subject_id BIGINT NOT NULL, visit_definition_id BIGINT NOT NULL, form_definition_id BIGINT NOT NULL, -- 绑定当时的表单定义版本 instance_status VARCHAR(20) NOT NULL DEFAULT DRAFT, data_json JSON, -- 该实例的录入数据快照 last_updated_by BIGINT, locked_by BIGINT, locked_at DATETIME, KEY idx_inst_subject (subject_id, visit_definition_id), CONSTRAINT fk_inst_form_def FOREIGN KEY (form_definition_id) REFERENCES form_definition(id) ); CREATE TABLE query ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_instance_id BIGINT NOT NULL, field_code VARCHAR(50), check_code VARCHAR(50), content VARCHAR(500) NOT NULL, severity VARCHAR(10) NOT NULL, -- ERROR / WARNING status VARCHAR(10) NOT NULL DEFAULT OPEN, -- OPEN / REPLY / CLOSED raised_by BIGINT, reply_content VARCHAR(500), reply_by BIGINT, reply_at DATETIME, close_by BIGINT, close_at DATETIME, CONSTRAINT fk_query_inst FOREIGN KEY (form_instance_id) REFERENCES form_instance(id) );几个关键参数说明subject表唯一索引建在(study_id, subject_no)防止同一个研究里重复录入受试者这是入组数据质量的底线。form_instance的form_definition_id指向具体的表单定义版本而不是“当前生效版本”这样旧数据永远用旧规则渲染和核查。data_json直接用MySQL 8的JSON类型放弃早期EDC常用的EAV实体-属性-值模式。EAV表在字段数量爆炸后查询极其痛苦JSON快照配合VARCHAR字段索引足以覆盖绝大多数场景。query表字段severity与2.2节JSON里的severity对应status必须走OPEN→REPLY→CLOSED闭环不允许从OPEN直接跳到CLOSED。3.2 保存接口一个事务里完成更新、审计、逻辑核查与质疑EDC里最核心的接口是“保存表单”。这个接口不只是把JSON写进data_json它必须在一个数据库事务里完成四件事更新数据、写审计日志、执行逻辑核查、生成必要的质疑。我给出的核心代码逻辑如下Transactional public SaveResult saveForm(Long formInstanceId, MapString, Object payload, User operator) { // 1. 行锁防止同一张表单被并发编辑 FormInstance inst formInstanceDao.selectForUpdate(formInstanceId); if (!StateMachine.canEdit(inst.getInstanceStatus(), operator.getRole())) { throw new ForbiddenException(当前状态不允许编辑); } // 2. 保存数据快照并推进状态 inst.setDataJson(payload); inst.setInstanceStatus(StateMachine.nextAfterEdit(inst.getInstanceStatus())); formInstanceDao.update(inst); // 3. 写字段级审计日志记录被修改的字段与前后值 MapString, Object oldData inst.getDataJson(); auditService.recordFieldChanges(formInstanceId, oldData, payload, operator); // 4. 执行逻辑核查 FormDefinition def formDefinitionDao.findById(inst.getFormDefinitionId()); ListQuery queries editCheckEngine.run(def.getSchemaJson(), payload); // 5. ERROR 级核查不通过则回滚 boolean hasError queries.stream().anyMatch(q - ERROR.equals(q.getSeverity())); if (hasError) { throw new EditCheckException(queries); } // WARNING 级核查通过但落库为 OPEN 状态的质疑 queryDao.saveAll(queries.stream() .filter(q - WARNING.equals(q.getSeverity())) .collect(Collectors.toList())); return SaveResult.ok(queries); }几个必须注意的细节selectForUpdate对应SQL里的SELECT ... FOR UPDATE只有拿到了行锁才能往下走。这样同一张表单的两个并发请求只会有一个成功另一个等锁超时后报错而不是互相覆盖。审计必须写在业务数据更新之后、事务提交之前这样任何人拿到的审计记录都与最终数据一致。ERROR级质疑触发异常后整个事务回滚前端会收到EditCheckException并把错误信息逐条展示给录入员WARNING级则允许保存质疑进入query表待研究中心回复。状态推进逻辑nextAfterEdit要区分DRAFT保存后仍是DRAFTIN_QUERY状态下回复质疑并保存后回到COMPLETED不允许在VERIFIED或LOCKED状态下直接调用这个接口。3.3 REST API边界表单渲染、保存、提交的核心路径后端接口不需要一开始就做全把下面这张表里的路径先跑通一个最小EDC就立住了。方法路径作用GET/api/studies/{studyId}/subjects/{subjectId}/visit-tree获取2.1节的访视树GET/api/form-definitions/{definitionId}/schema获取表单定义JSONGET/api/form-instances/{instanceId}获取实例数据合并schema后渲染PUT/api/form-instances/{instanceId}/data保存数据3.2节POST/api/form-instances/{instanceId}/submit提交表单DRAFT→COMPLETEDPOST/api/form-instances/{instanceId}/verifyCRA核查COMPLETED→VERIFIEDPOST/api/form-instances/{instanceId}/lock锁定VERIFIED→LOCKEDPOST/api/queries/{queryId}/reply研究中心回复质疑其中“获取实例数据”接口返回的不是原始data_json而是“定义 数据”合并后的渲染视图。常见做法是后端把schema_json里的字段和data_json里的值做一次merge前端拿到后直接渲染不需要自己拼装。这个接口的响应结构里我还建议带上instance_status和form_definition_id前端据此决定控件是否可编辑、是否需要显示锁定提示。4. 最难补的合规模块稽查轨迹、电子签名、权限与盲态4.1 稽查轨迹append-only哈希链哪怕数据库管理员也不能绕过稽查轨迹Audit Trail是EDC和普通业务系统最根本的区别。日志可以随便删的系统过不了任何一次临床数据稽查。我交付EDC时坚持用append-only表加哈希链审计日志只允许INSERT不允许UPDATE和DELETE并且每一条记录都带前后哈希让篡改历史记录变得可检测。CREATE TABLE audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_instance_id BIGINT NOT NULL, op_type VARCHAR(20) NOT NULL, -- CREATE / SAVE / SUBMIT / VERIFY / LOCK / UNLOCK / SIGN field_code VARCHAR(100), old_value TEXT, new_value TEXT, operator_id BIGINT NOT NULL, op_reason VARCHAR(200), -- 修改原因稽查必查 client_ip VARCHAR(45), op_ts DATETIME NOT NULL, prev_hash CHAR(64), -- 上一条记录哈希 cur_hash CHAR(64) -- 本条记录哈希 );哈希链的生成逻辑我放在一个独立服务里public void recordAudit(String formInstanceId, String opType, String fieldCode, String oldVal, String newVal, User operator, String reason) { String prevHash auditMapper.getLastHash(formInstanceId); if (prevHash null) { prevHash GENESIS; } String raw prevHash | operator.getId() | newVal | operator.getRole() | reason | System.currentTimeMillis() | salt; String curHash DigestUtils.sha256Hex(raw); auditMapper.insert(formInstanceId, opType, fieldCode, oldVal, newVal, operator.getId(), reason, prevHash, curHash); }注意两个设计取舍哈希链按form_instance_id维度串而不是全局一条链。原因是高并发下全局链需要串行化取“上一条哈希”会变成性能热点按表单实例分链则每个实例独立演进互不阻塞。salt放在独立的配置中心不落数据库。数据库管理员即使直接改数据也拿不到salt重算哈希。prev_hash为空记录第一条用固定字符串GENESIS。4.2 电子签名二次验密、数据哈希与签名理由电子签名不是“签名栏填个名字”它要能证明“谁在什么时间基于哪一份数据做了哪个操作”。我推荐的数据结构如下CREATE TABLE electronic_signature ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_instance_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, sign_action VARCHAR(20) NOT NULL, -- LOCK / UNLOCK / VERIFY / MODIFY sign_reason VARCHAR(200) NOT NULL, data_hash CHAR(64) NOT NULL, -- 签名瞬间data_json的SHA-256 signature_version VARCHAR(10) NOT NULL DEFAULT 1.0, created_at DATETIME NOT NULL );核心签名Java逻辑public Signature sign(String formInstanceId, String password, String reason) { // 1. 每次签名都必须重新验证账号密码不能因为已登录就跳过 User operator authService.verifyPassword(currentUserId(), password); // 2. 对当前表单数据取哈希 FormInstance inst formInstanceDao.findById(formInstanceId); String dataHash DigestUtils.sha256Hex(inst.getDataJson().toString()); // 3. 落签名记录并把签名事件写入审计链 Signature sig Signature.builder() .formInstanceId(formInstanceId) .operatorId(operator.getId()) .signAction(MODIFY) .signReason(reason) .dataHash(dataHash) .signatureVersion(1.0) .createdAt(now()) .build(); signatureDao.save(sig); auditService.recordAudit(formInstanceId, SIGN, null, null, dataHash, operator, reason); return sig; }用这个逻辑LCOK和UNLOCK操作前必须调用签名接口。data_hash绑定的是签名那一瞬间的完整数据快照之后任何人改动数据哈希都会对不上。如果机构要求更严还可以在这里扩展UKey证书指纹字段但用户名密码 数据哈希 签名理由已经能满足绝大多数电子记录规范。4.3 角色权限与盲态统计师与研究者不该看到同一份数据EDC的角色权限比普通后台系统细因为临床场景里“知情”是分层的。我常用以下角色矩阵角色录入回复质疑核查SDV锁库导出查看分组CRC临床协调员是是否否否否研究者PI是是否否否否CRA监查员否否是否是否数据管理员DM否否否是是否统计师否否否否只读否揭盲后放行盲态设计是这个模块里最容易被做漏的。受试者的实际用药分组随机分配结果不能直接存在subject表里我一般会单独建一张random_allocation表业务查询默认不JOIN它只有统计师在揭盲流程中通过独立接口和一次性令牌读取。EDC界面上的受试者列表更不能显示“A组/B组”这类字段否则监查员和研究者很容易通过界面推断出分组信息。权限这块还有一个边界CRA只能核查自己负责的中心的表单实例。数据管理员虽然全库可见但任何导出操作都要落审计日志。角色与中心的组合权限检查一般在Spring Security过滤器或者网关层统一处理不要在业务代码里散落判断。5. EDC自研避坑清单时区、生僻字、并发锁、表单版本与审计日志5.1 时区与临床日期分离访视窗口算错的第一个坑现象某个多中心研究录入的访视日期偶尔比实际日期早一天访视窗口判断全部乱掉。 原因应用服务器部署在UTC时区数据库datetime字段也按UTC存储前端new Date()又把字符串按本地时区解析导致录入的“临床日期”被时区偏移吞掉一天。 解决所有与方案相关的日期访视日期、AE开始日期、用药日期统一用DATE类型或LocalDate处理不经过时区转换真正的操作时间戳审计用的op_ts才用DATETIME存UTC。前端传输日期一律走yyyy-MM-dd字符串不传时间戳。访视窗口计算也基于临床日期而不是录入时间。5.2 utf8mb4与全半角生僻字核验与重复数据现象某受试者姓名包含生僻字“”四字节UTF-8字符录入保存报错而同一个名字用全角括号和半角括号各录一次系统判定为两条不同数据。 原因数据库排序规则还是老的utf8mb3无法存储四字节字符全角与半角的冒号、括号、数字在比较时被当成不同字符。 解决建库时显式指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci入库前对文本字段做NFKC规范化全角转半角、统一括号形态涉及查重的标识字段受试者编号、中心编号不要依赖数据库比较在应用层先规范化再写入。这个坑在数据导出到统计软件时还会再炸一次因为SAS对UTF-8的支持也有历史包袱。5.3 并发死锁跨表单逻辑核查与锁顺序现象某中心集中录入大批量数据时MySQL频繁报Deadlock found when trying to get lock; try restarting transaction1213错误。 原因保存接口先锁form_instance行再执行逻辑核查。核查经常要读同一受试者的其他表单比如AE表要对比病历表的日期这时会去读其他行两个并发事务获取锁的顺序不一致就产生了死锁。 解决我最终改了锁顺序——所有涉及同一受试者的保存请求先按subject_id维度获取一把“受试者锁”例如在subject表对应行执行SELECT ... FOR UPDATE再操作表单实例。同一受试者的所有表单编辑被串行化跨表单核查永远读得到最新数据也不会互相抢锁。代价是同一受试者的并发保存吞吐下降但临床录入场景本来就没有这个并发需求。5.4 表单版本发布旧实例必须绑定旧定义现象AE表单新增了一个必填字段结果历史已录入的1000多份AE实例全部出现必填校验ERROR质疑泛滥。 原因form_instance没有绑定form_definition_id版本核查引擎用的是“当前最新定义”老数据自然过不了新规则。 解决每次修改表单定义都发布新版本version自增已生成的表单实例继续指向旧版本新数据只能按新版本生成实例。逻辑核查引擎执行时根据实例绑定的form_definition_id去加载对应的schema_json而不是按form_code加载当前版本。上线前还要准备一个数据差异报告列出“旧版本必填、新版本新增必填”的字段由数据管理员决定是否回填。5.5 审计日志不可覆盖把append-only当成数据库规则现象某次数据修正脚本直接UPDATE了form_instance.data_json审计表里查不到任何记录稽查时完全无法自证清白。 原因早期实现把审计当成“业务层顺手写一下”没有在数据库层面对审计表做保护开发脚本或手动SQL可以绕过。 解决两个层面加固。第一数据库层面用触发器禁止对audit_log执行UPDATE和DELETE只允许INSERT第二审计写入与业务数据更新放在同一个本地事务里禁止任何异步写审计的“优化”异步一旦丢消息数据变更就没有记录了。我后来养成的习惯是任何能改数据的入口先问一句“这条路线的审计在哪一行落库”答不出来的代码不允许合入。6. 源码验收与进阶先跑审计链SQL再做SDTM导出与监查看板6.1 审计链完整性SQL拿到源码先跑这三条语句拿到任何一套EDC源码我第一件事不是看功能页面而是先验证数据基础。第一步是跑审计链完整性检查把所有断链记录一次性查出来SELECT a.id, a.form_instance_id, a.op_type, a.op_ts FROM audit_log a WHERE a.prev_hash ( SELECT b.cur_hash FROM audit_log b WHERE b.form_instance_id a.form_instance_id AND b.id a.id ORDER BY b.id DESC LIMIT 1 );第一条记录的prev_hash固定是GENESIS上面SQL里把它当作“与空集比较结果不同”处理所以第一条会一直出现在结果里。正确的跑法是执行后排除第一条再确认剩余结果为0行。这个查询不依赖业务逻辑只需要audit_log表结构是验收最小EDC源码最有效的验证语句。接着再跑两个检查一是query表不能存在OPEN超过90天且没回复的质疑二是form_instance.instance_status为LOCKED的数据其data_json哈希必须与最近一次电子签名记录的data_hash一致。6.2 从eCRF字段到标准数据集的映射一个可控的导出映射器EDC的数据最终要交给统计方行业里最通用的做法是映射到标准数据表模型SDTM。不建议在EDC业务代码里写死转换逻辑而是建一张映射配置表把每个SDTM变量对应到源表单字段和转换规则。映射表大概长这样目标域目标变量源表单源字段转换规则AEAESTDTCAEae_start_date日期转ISO8601AEAESEVAEae_severity字典映射MILD→1MODERATE→2SEVERE→3DMSUBJIDSUBJECTsubject_no直接复制DMRFSTDTCSUBJECTenroll_date日期转ISO8601导出程序读这张表逐字段处理核心逻辑就是一个for循环def map_to_sdtm(src_row, mapping_config): out_row {} for m in mapping_config: src_val src_row.get(m[source_field]) if m[transform] date_to_iso: out_row[m[target_variable]] parse_date(src_val).isoformat() elif m[transform] dictionary: out_row[m[target_variable]] DICT_MAP[m[source_dict]].get(src_val, src_val) else: out_row[m[target_variable]] src_val return out_row这里最重要的是把映射关系做成数据而不是代码统计标准更新时只需要改映射表不需要动程序。每一行导出都应该关联到源数据版本和导出时间戳审计上才能说清“这份数据是哪天从哪个库版本出来的”。6.3 中心化监查基于疑问率与录入时长的数据健康看板中心化监查是EDC进阶用法里投入产出比最高的一个方向。它的思路是不需要跑现场用SQL指标就能发现异常中心。我常用的第一张表是各中心疑问率统计SELECT s.site_no, COUNT(DISTINCT f.id) AS form_count, COUNT(q.id) AS open_query_count, ROUND(COUNT(q.id) * 1.0 / NULLIF(COUNT(DISTINCT f.id), 0), 2) AS query_rate FROM site s JOIN subject sub ON s.id sub.site_id JOIN form_instance f ON sub.id f.subject_id LEFT JOIN query q ON f.id q.form_instance_id AND q.status OPEN GROUP BY s.site_no ORDER BY query_rate DESC;疑问率超过某个中心平均值两倍的站点通常意味着培训不到位或CRF页面设计有歧义。再配合“录入耗时”指标同一张表单从创建到提交的间隔就能把数据质量管控从事后检查变成事中预警。我第一次交付EDC源码时把精力全花在报表模块结果上线第二天就发现历史数据被脚本改过而审计里找不到记录。那次之后我定下一个规矩任何EDC改造动工前先让审计链校验SQL跑一遍并且是绿的再做新功能。顺序反着来后面全是坑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑