资讯动态

开源EDC系统:Spring Boot+Vue实现临床试验数据采集与稽查追踪

发布时间:2026/10/18 17:45:17 来源:尧图企业网站定制
简介一份完整的临床试验数据采集管理系统源代码面向需要搭建或研究电子数据采集系统的开发者、临床研究相关技术人员。系统覆盖试验项目全生命周期管理支持病例报告表在线设计、数据录入与逻辑自动校验、疑问及时处理、统计报表生成以及数据导出PDF等关键操作形成从试验配置到数据审核的闭环。压缩包共6个文件其中4个Java源文件构成核心业务逻辑按功能划分为输入和手动查询两大模块分别处理数据输入和手动查询另有2个辅助配置文件包体仅17KB代码量紧凑且无冗余依赖。目前已有1729人浏览学习适合作为入门临床试验信息化系统的参考样例或在此基础上扩展定制快速落地一套可用的数据管理基础框架。1. EDC系统到底是什么一次纸质CRF引发的数据灾难我最早接触EDC是被一个做临床监查的朋友拉去救火他们有一个项目还在用纸版的病例报告表CRF数据录入靠CRC手工填写然后邮寄到公司打字员再录到Excel里。结果某一中心的数据录入结果和化验单原始数据差了十几个单位质疑表发出去大半个月才回来。我陪他们重新梳理了一遍才发现问题根源不是某个CRA不认真而是整个数据链路根本没有版本控制和审计痕迹。那次之后我就意识到一套好的EDC系统不只是把纸质表单搬到网页上它解决的是临床试验数据采集里的三大痛点数据什么时候被谁改了、改之前的值是什么、为什么改。这份提供完整源代码的EDC系统正好把这三件事都做成了可配置的模块化功能适合临床研究团队、独立开发者以及想做临床试验管理定制系统的朋友直接作为基座来研究或二次开发。2. 总体架构拆解从病例报告表到稽查日志数据流转的完整链路2.1 技术栈与部署形态前后端分离Spring Boot Vue这份EDC系统采用的是前后端分离的经典架构。后端以Spring Boot为主构建了RESTful API接口前端使用Vue 2 Element UI实现站点页面。数据库使用MySQL缓存层配合Redis完成用户会话和数据查询优化。整套代码压缩包拿出来后端约有三万个Java文件前端包含一百多个Vue组件和若干配对的JS模块部署形态就是标准的Web应用不需要额外安装客户端。前后端分离带来的直接好处是不同角色可以并行开发。比如数据录入界面需要频繁调整布局前端团队改组件不影响后端服务而建立稽查轨迹、权限模型这类核心逻辑后端可以独立迭代。在这份源码里前端通过axios封装了一个统一的请求对象所有接口都走/api前缀然后由后端网关用Api注解统一管理接口描述生成的Swagger文档正好用来对应临床试验的数据字典。2.2 核心模块划分权限、建库、录入、质疑、报表从功能模块上看这个EDC系统几乎覆盖了临床试验数据采集的所有日常环节。首先是权限与账号管理系统内置系统管理员、项目经理、临床监查员CRA、数据录入员CRC、数据管理员DM五类角色每类角色能看到的界面和可执行的操作都不相同。其次是建库模块它支持创建一个新的研究项目然后在项目下建立不同的中心研究中心、访视Visit和病例报告表CRF。录入管理是日常使用最频繁的模块。一个试验周期里患者需要经历筛选期、治疗期、随访期等多个访视点每个访视点对应一张或多张CRF表。系统中录入页根据配置动态生成同时支持必填校验、范围校验和逻辑跳转。数据进来之后会有质疑管理模块数据管理员根据规则自动生成质疑录入员收到质疑后修改数据或回复原因整个质疑闭环在系统里留存记录。最后是报表模块支持按中心、按访视、按数据完成率生成统计图表导出Excel给监查团队使用。2.3 数据模型设计研究、访视、数据点、稽查日志的关联关系EDC系统的核心数据模型可以归纳为“项目-中心-受试者-访视-表单-字段”的层级关系。在设计数据库中我把关键表拆分如下表名说明关键字段trial_project研究项目id, project_name, statustrial_center研究中心id, project_id, center_nametrial_subject受试者id, project_id, subject_codetrial_visit访视阶段id, project_id, visit_name, visit_ordertrial_form表单定义id, visit_id, form_name, form_versiontrial_field字段定义id, form_id, field_name, field_type, validation_ruletrial_data采集到的数据id, subject_id, field_id, field_value, modified_bytrial_audit_log稽查日志log_id, operator, operation_type, target_type, target_id, old_value, new_value, timestamp其中trial_data是数据存储的核心表它的设计不是经典的“一行一条记录”而是“一个字段一条记录”。这种EAV模式实体-属性-值在EDC场景里非常实用因为每个项目的CRF表单都不一样不可能给每个项目建一张物理表。好处是建库灵活、无需改表结构代价是查询时需要做大量的关联操作所以在trial_field表里冗余了form_id和visit_id查询时直接通过字段ID定位数据点避免每次都从项目层层联表。trial_audit_log是所有稽查功能的基础。每一次数据的增、删、改操作都会向这张表写入一条日志记录操作人、操作类型、目标对象、旧值、新值和时间戳。这样当监查员需要查看某个数据点的历史轨迹时不需要回查源代码只要按target_id查询这张表就能还原整个修改链路。日志表的数据量增长很快所以源码里使用了按月分表的策略项目运行满一年后旧日志自动迁移到归档库。3. 核心模块实现一个表单字段从创建到录入的完整链路3.1 动态表单引擎前端如何根据后端配置渲染CRF字段动态表单是本系统最值得读的模块。后端用trial_field表存储字段定义前端收到字段列表后根据字段的field_type渲染对应的输入组件。我举个例子如果在建库时配置了一个“出生日期”字段那么后端会返回{ fieldId: 1024, fieldName: birth_date, fieldLabel: 出生日期, fieldType: date, isRequired: true, validationRules: { maxDate: today } }前端拿到这个JSON后会走一个通用的表单渲染组件。源码中FormGenerator.vue的核心逻辑如下template el-form :modelformData :rulesformRules refdynamicForm el-form-item v-forfield in fields :keyfield.fieldId :labelfield.fieldLabel :propfield.fieldName el-date-picker v-iffield.fieldType date v-modelformData[field.fieldName] typedate :disabledfield.isReadOnly :picker-optionsfield.pickerOptions /el-date-picker el-input-number v-else-iffield.fieldType number v-modelformData[field.fieldName] :minfield.minValue :maxfield.maxValue/el-input-number el-select v-else-iffield.fieldType dropdown v-modelformData[field.fieldName] el-option v-foroption in field.options :keyoption.value :labeloption.label :valueoption.value/el-option /el-select el-input v-else v-modelformData[field.fieldName]/el-input /el-form-item /el-form /template这个组件的设计思路是不写死任何表单所有CRF页面都是由后端返回的字段配置动态生成。fieldType决定了控件类型validationRules在前端通过适配器转换成Element UI的表单校验规则。这样做的好处很直接新项目建库时不需要改前端代码只要在后台配置字段前端就会自动展示出对应输入控件。3.2 后端CRUD创建研究、设计表单、保存数据的实现后端Controller层遵循了标准的Restful风格。创建研究的接口定义如下PostMapping(/project) public Result createProject(RequestBody Valid ProjectCreateDTO dto, RequestAttribute(userId) Long userId) { TrialProject project new TrialProject(); project.setProjectName(dto.getProjectName()); project.setOrganizationName(dto.getOrganizationName()); project.setStatus(DRAFT); project.setCreatedBy(userId); projectService.save(project); // 初始化默认角色项目经理、数据管理员 roleService.initDefaultRoles(project.getId()); return Result.success(project.getId()); }这里的RequestAttribute(userId)是从拦截器中解析token后放入的保证创建人会被自动记录。ProjectCreateDTO是一个参数接收对象包含项目名称、申办方、项目编号等字段通过Valid做参数校验比如项目名称不能为空、编号格式必须符合正则规则。参数校验不通过时全局异常处理器会统一返回错误格式前端据此在页面上给出提示。保存数据点的接口是系统中最频繁调用的接口之一。它的实现要兼顾更新和稽查日志的写入PostMapping(/data/save) public Result saveFieldData(RequestBody DataSaveDTO dto, RequestAttribute(userId) Long userId) { TrialData existing dataService.getBySubjectAndField(dto.getSubjectId(), dto.getFieldId()); String newValue dto.getFieldValue(); if (existing null) { TrialData data new TrialData(); data.setSubjectId(dto.getSubjectId()); data.setFieldId(dto.getFieldId()); data.setFieldValue(newValue); data.setModifiedBy(userId); data.setModifiedTime(new Date()); dataService.save(data); auditService.log(CREATE, trial_data, data.getId(), null, newValue, userId); } else { String oldValue existing.getFieldValue(); existing.setFieldValue(newValue); existing.setModifiedBy(userId); existing.setModifiedTime(new Date()); dataService.update(existing); auditService.log(UPDATE, trial_data, existing.getId(), oldValue, newValue, userId); } return Result.success(); }这段代码有一个设计细节值得注意DataSaveDTO里面携带了subjectId和fieldId两个维度这样就能精确定位到某一个具体数据点。对于初次保存是CREATE第二次开始就是UPDATE每次操作都把oldValue和newValue通过auditService写入日志。有一点需要提醒这里的保存操作没有加锁因为单条字段保存并不需要严格的事务隔离后续如果做批量保存就必须使用乐观锁或者分布式锁来避免互相覆盖。3.3 稽查轨迹与电子签名保证数据可信度的两把锁临床试验数据要求“可溯源、可还原”稽查轨迹模块就是这个要求的落地。AuditService封装了日志记录的通用方法所有需要审计的操作都通过它来写日志。下面是核心方法public void log(String operationType, String targetType, Long targetId, String oldValue, String newValue, Long operatorId) { TrialAuditLog log new TrialAuditLog(); log.setOperatorId(operatorId); log.setOperationType(operationType); log.setTargetType(targetType); log.setTargetId(targetId); log.setOldValue(oldValue); log.setNewValue(newValue); log.setOperateTime(new Date()); log.setIpAddress(requestUtil.getClientIp()); auditLogMapper.insert(log); }operateTime字段使用了数据库的DATETIME类型其中精度到秒满足临床研究的监管要求。ipAddress来自请求Util类是为了追溯操作来源在多中心联合使用的时候也很重要能通过IP看出是哪个中心的网段。电子签名在EDC里体现为二次确认操作当用户提交整套CRF时系统会要求输入登录密码后台将该操作与用户身份绑定生成一条带时间戳和用户ID的签名记录。常见的一个坑是很多系统只在页面上做校验后端接口没有强制电子签名。但这个系统的后端会在提交CRF时检查AccessToken和当前用户密码的二次校验两把锁同时通过才允许正式提交。这个逻辑值得二次开发时保留别为了省事删掉。4. 项目快速启动指南从下载压缩包到本地跑起来4.1 开发环境准备JDK、Node、MySQL、Redis缺一不可在本地跑这套EDC系统环境准备是第一步也是很多人最容易在起步阶段翻车的环节。我建议先检查这四样东西JDK 1.8以上推荐JDK 8因为源码大多数依赖对8兼容最稳Maven 3.6以上后端用Maven管理依赖Node.js 12以上前端构建依赖npmMySQL 5.7以上Redis 5.0以上其中MySQL需要注意配置characterEncodingutf8mb4因为CRF表里可能出现生僻字和特殊符号如果使用默认的latin1写入中文数据时会直接变成乱码。Redis没有特殊要求本地起一个默认端口就能跑。在这份源码的application.yml里默认配置了本地连接信息spring: datasource: url: jdbc:mysql://localhost:3306/edc_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379这里的serverTimezone也值得说道。EDC系统涉及时间戳比较多如果MySQL时区设置不对后端写入的Date对象会和数据库存储的时间相差8个小时。建议在JDBC连接串里显式指定serverTimezoneAsia/Shanghai这样前后端读取时间都是一致的。4.2 数据库初始化建库脚本与种子数据源码压缩包里会附带一个sql目录里面有两个脚本schema.sql和seed_data.sql。前者是库表结构后者是初始数据。我建议先执行schema再执行seed顺序不能反不然外键依赖会报错。使用命令行导入的过程很简单mysql -uroot -p edc_system schema.sql mysql -uroot -p edc_system seed_data.sql执行完毕后可以看到库里自动生成了几十张表除了核心的trial_*表外还包括sys_user、sys_role、sys_permission这些权限管理相关的表。seed_data.sql里内置了一个测试项目和两个测试账号一个账号是项目经理角色一个账号是数据录入员角色初始密码统一是admin123。登录后台后就能直接看到测试项目的数据。4.3 启动后端服务Maven打包与启动参数后端是一个标准的Spring Boot工程在项目根目录执行以下命令即可mvn clean package -DskipTests java -jar target/edc-admin.jar --spring.profiles.activelocal-DskipTests是为了跳过生成测试报告节省打包时间。--spring.profiles.activelocal指定使用application-local.yml这个配置文件文件里定义了开发环境的数据库连接和日志级别。如果启动时报端口占用可以在启动参数后追加--server.port8081来指定新端口。启动成功的标志是日志里出现Started EdcAdminApplication in x seconds同时访问http://localhost:8080/swagger-ui.html能看到接口文档。这里要留意Swagger接口界面的文件名是swagger-ui.html不是doc.html第一次搜索时容易摸不着头脑。4.4 启动前端服务npm install与dev server前端目录名一般是edc-web打开终端进入该目录后npm install npm run servenpm install在安装依赖时如果报错大概率是网络问题或者Node版本太高导致依赖兼容失败。我一般用npm install --registryhttps://registry.npmmirror.com --unsafe-perm来强制走镜像并提升权限。前端的开发服务默认跑在8080端口但配置文件里通常把接口代理到9090端口这个代理配置在vue.config.js文件里module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }这里的/api前缀是后端统一接口前缀代理的9090就是后端服务实际运行的端口。如果你在本地把后端端口改成了8081那么这里的target也要同步修改否则前端会有接口找不到的错误。启动完成后浏览器访问http://localhost:8080能看到登录页这时整个系统就通了。5. 常见问题避坑部署和二次开发时最容易翻车的五个点5.1 问题一接口返回中文乱码页面显示一片问号现象前端登录后页面顶栏的菜单名和项目名称全部变成????但英文正常。原因数据库连接串里没有配置characterEncodingutf8mb4导致JDBC写入和读取时使用了MySQL默认的latin1字符集中文在存取过程中已经被截断成了问号事后无论怎么改前端编码都没用。解决修改application.yml中的JDBC连接串明确加上useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai然后重启后端服务。最关键的是要在启动服务器之前确认MySQL的默认字符集是否为utf8mb4否则改完连接串仍然会出现乱码。最好在创建数据库时就用CREATE DATABASE edc_system CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci来建库。5.2 问题二前端接口404请求无法到达后端现象页面能正常显示登录框但输入账号后点击登录控制台里报出POST http://localhost:8080/api/login 404。原因大部分情况是前端代理跨域配置不生效。因为前端跑在8080端口后端跑在9090端口浏览器默认不允许跨端口直接请求所以必须依赖代理转发。当代理配置里的target端口写错或者路径前缀/api与后端实际接口前缀不一致时接口就会404。解决检查vue.config.js中的target是否指向后端端口同时确认后端Controller类上的类级RequestMapping(/api)注解存在。修改代理配置后npm run serve会自动重启开发服务器等两分钟再试。如果还不行就在浏览器直接访问http://localhost:9090/api/login如果这里也404说明后端接口路由有问题优先排查后端。5.3 问题三稽查日志丢失历史修改记录查不到现象在数据录入页修改了一个字段查询审计日志时却没有任何记录。原因我看过的不少二次开发版本里开发者为了简化查询逻辑直接改数据库字段值绕过了AuditService。比如用SQL执行update trial_data set field_value x where id y这种操作日志必然丢失。还有就是在Service层把auditService.log方法放在事务提交之前但事务回滚后日志也一起没了。解决所有数据修改必须走dataService.saveFieldData方法不要在Service里自己调dataService.update更不要直接写SQL。审计日志写入和业务数据更新需要放在同一个事务中但注意日志记录不能在事务回滚时丢失所以要么使用独立事务器要么先写日志再执行更新。源码里默认把日志和业务操作放在一个事务里回滚时会同时回滚实际项目中我更推荐把日志写入放到事务提交后的回调中确保日志一定落库。5.4 问题四随机化分组并发时出现重复分配现象多个中心同时为受试者分配随机号结果两个受试者拿到了同一个随机号。原因原本的随机化分配逻辑是先查最大编号再加一不是原子操作。在并发场景下两个请求同时读到当前最大编号为100然后先后更新成101后面那个请求又查到了101再加一变成102结果前一条记录没回滚就出现了重复。这个问题的典型表现是随机号表里出现相同记录。解决给随机号分配增加数据库唯一约束unique(project_id, subject_no)并在分配号码时改用SELECT ... FOR UPDATE锁住随机号父行保证同一时间只有一个请求能执行分配逻辑。改造后并发场景下后到的请求会被阻塞等待直到前一个事务提交拿到最新编号继续分配。5.5 问题五电子签名时间戳错误导致签名无效现象用户提交CRF后审计日志中记录的时间比实际提交时间早了8小时。原因电子签名的时间取的是后端系统时间而服务器的时区设置是UTC没有改成Asia/Shanghai。当MySQL的serverTimezone是Asia/Shanghai时数据库存储的时间已经正确但签名记录使用的是Java虚拟机的默认时区两个时间就出现了偏移。解决在启动后端时添加JVM参数-Duser.timezoneAsia/Shanghai并在application.yml中统一设置spring.jackson.time-zone: GMT8。同时检查服务器的时区是否已经通过timedatectl set-timezone Asia/Shanghai设置好。这三处全部对齐后电子签名的时间戳才能真正反映操作发生的时间。6. 二次开发改造思路把EDC系统改造成适合你试验的形态6.1 支持eCRF模板导入导出拿到这份EDC源码后最值得做的一次改造是支持eCRF模板导入导出。现在临床试验方案变更频繁每调整一次CRF设计就要在后台手动建字段非常低效。我给这个系统加了一个ExcelTemplateService让项目经理可以下载一个模板Excel在Excel里维护表单名、字段名、字段类型、校验规则然后一键导入到建库模块。具体实现并不复杂。后端使用EasyPOI读取Excel的第一张工作表每一行对应一个trial_field记录读取完毕后先校验必填列再将数据批量写成数据库。导出功能则是反向操作查询出当前访视的所有表单按固定表头生成Excel文件。这个改造的核心价值是让建库工作从“操作表单后台”变成“编辑Excel表格”对新项目需求最友好也是监查团队最容易接受的协作方式。6.2 对接医学编码词典临床数据里大量出现不良事件和既往用药的记录这些文本通常需要映射到标准医学词典比如MedDRA和Who Drug。默认源码里这些字段只能手动输入原始文本没有编码建议功能。我改造时在trial_field类型中增加了coded_text并在后端TermSuggestionController中接了一个词典搜索接口录入员在输入不良事件名称时前端会发起请求返回近义词列表。这个改造的要点在于编码词典的匹配不是完全精确的字符串匹配而是需要做到前缀匹配因为“腹痛”和“腹部疼痛”都可能是同一标准词的不同表达方式。建议在词典表中增加一列search_key预先对标准词做同义词扩充这样查询时的命中率能提高很多。另外编码结果要保存两个字段一个是原始文本一个是编码后的标准词ID这样监查时既能看到原始记录也能知道编码映射到了哪里。6.3 通过配置化提升系统适应性这份EDC系统本身已经有很好的配置基础字段类型、校验规则、角色权限都是数据库驱动的但我在二次开发时仍然加了一个“模块开关”的机制。每个临床试验研究的特点不一样有的项目需要逻辑跳转有的项目不需要有的需要盲态数据审核有的只需要非盲态。我在项目表里增加了一个feature_config文本字段用JSON格式存储当前项目的功能开关列表。前端在加载项目时读取这个JSON动态决定是否渲染某个功能按钮。后端则根据开关拦截对应接口的请求。这个改造的本质是想让一套代码适配多个试验场景避免为不同项目维护多个分支版本。从维护角度看配置化比硬编码好得多项目上线后不需要再出补丁包只需要在后台更新开关即可。写到最后我还是要把自己的习惯分享给你每一次改这段EDC代码之前我都要先跑一遍稽查日志导出确认所有修改记录按时间线对齐然后再动手动代码。因为这个系统的核心价值就是让人相信数据可信如果连日志都断了整个项目的存在意义也就没了。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑