SpringBoot+Vue医院病历系统源码解析:架构、CRUD与部署
简介面向毕业设计场景的医院病历管理系统完整项目后端基于 Java 与 SpringBoot 构建前端使用 Vue 和 ElementUI 实现MySQL 配合 MyBatisPlus 处理数据持久化适合计算机相关专业学生参考学习也可作为快速搭建管理系统的模板。资源包共 401 个文件以 91 个 Java 源码、40 个 Vue 组件、21 个 JS 脚本、12 个 XML 配置为主另含 SVG 图标、JPG/PNG 图片素材、CSS 样式、文档说明及一键安装启动打包脚本等压缩包整体仅 9.57MB。项目按前后端分离思路组织覆盖登录认证、用户管理、病历录入与查询、图片素材维护等常见模块代码提供 controller/service/mapper/entity 分层结构注释与命名清晰便于定位和二次开发同时附带摘要、目录、绪论、相关技术与系统分析章节方便对照毕业论文写作。资源包内还包含数据库配置和启动辅助脚本能有效减少本地环境搭建与项目联调的障碍。目前已有 178 人学习浏览适合作为毕业设计选题或 SpringBoot 整合 Vue 的实战练习。1. SpringBoot Vue 医院病历系统源码先看架构再谈跑通在 SpringBoot Vue 前后端分离方案成为中小型管理系统主流的今天一份能直接跑起来的医院病历系统源码价值不在于界面多精致而在于后端实体、Mapper、Controller、前端表单这条完整调用链如何在同一个工程里对上。对正在做同类管理系统的开发者而言这套基于 Java、SpringBoot、MyBatisPlus 与 MySQL 5.7 的项目已经把环境、依赖、路由、跨域这些非业务却绕不开的坑提前趟了一遍可以直接当脚手架参考。这套资源适合两类读者一类是拿它做课程设计或毕业设计的在校生想省掉从零搭环境的成本另一类是已经在写类似 Web 管理系统的从业者想确认自己的接口组织和查询写法有没有偏离主流。下面的拆解按技术选型、核心 CRUD、运行部署、排错定位、进阶改造五条线展开命令与代码均按 JDK 1.8、MySQL 5.7、Maven、SpringBoot Vue 这套环境复现。2. 技术选型拆解JDK 1.8、MySQL 5.7 与 MyBatisPlus 为什么是这套配置2.1 B/S 架构与前后端分离如何落到病历系统上医院病历系统是典型的 B/S 架构浏览器负责录入与展示服务器端 SpringBoot 接收请求并操作 MySQL 数据。SpringBoot 相比传统 SSM 的优势在于内嵌 Tomcat不用单独安装 Web 容器打好的 jar 包在装有 JDK 的机器上直接java -jar即可启动。前端选 Vue 配合 ElementUI 组件库首页的侧边菜单、顶部栏、面包屑导航都是组件化写法资源包里的 IndexAsideStatic.vue、IndexHeader.vue、BreadCrumbs.vue 对应的就是这几个模块。文件名里的.bak后缀说明作者在联调阶段保留过修改前副本这个细节后面会专门展开。选 Vue 而不是其他方案核心原因是这类增删改查系统的大部分页面都由列表、表单、分页组成ElementUI 在 Vue 2 生态里组件最全数据双向绑定让表单数据→请求体→后端实体之间几乎不需要手工拼接参数。后端暴露 REST 风格接口前端用 axios 异步调用两头职责清楚。这个选型不是追新而是刚好覆盖病历管理最常见的场景大量列表查询、表单录入、少数几个统计报表。2.2 MyBatisPlus 如何把单表 CRUD 从 XML 里解放出来传统 MyBatis 时代每张表都要维护一个 Mapper 接口和一份 XML 文件单表增删改查的 SQL 高度雷同。MyBatisPlus 在这个项目中的作用是在不改变 MyBatis 核心前提下靠泛型基类BaseMapperT把常见单表方法预置好selectById、selectList、selectPage、insert、deleteById 直接调用不需要写一条 SQL。遇到多表关联或统计报表再回到 XML 手写两者处于同一个 SqlSessionFactory 中切换成本很低。它的另一个价值是条件构造器。看一段 Service 层里按患者姓名和就诊时间区间筛病历的常见写法LambdaQueryWrapperMedicalRecord wrapper Wrappers.lambdaQuery(); wrapper.like(patientName ! null !patientName.isEmpty(), MedicalRecord::getPatientName, patientName) .ge(startTime ! null, MedicalRecord::getVisitTime, startTime) .le(endTime ! null, MedicalRecord::getVisitTime, endTime) .orderByDesc(MedicalRecord::getVisitTime); ListMedicalRecord list recordMapper.selectList(wrapper);核心是LambdaQueryWrapper的 lambda 方法引用写法第一个参数是 boolean 条件为 false 时整个片段不会拼进 SQL前端有没有传参后端都不需要写一长串 if-elsege、le对应 SQL 里的和orderByDesc指定排序字段。相比字符串形式的 QueryWrapperlambda 写法编译期就能发现字段名拼写错误后期改字段名时 IDE 也能同步重构。2.3 Maven 依赖结构与兼容性边界项目里的 1-install.bat 通常就是从 Maven 拉依赖开始的。pom.xml 中三个关键依赖分别是 spring-boot-starter-web、mybatis-plus-boot-starter 和 mysql-connector-java负责 Web 容器与请求解析、MyBatisPlus 自动配置、MySQL 驱动。以下是精简后的声明具体版本以本地 Maven 仓库中对应的 SpringBoot 2.x 版本为准dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency依赖声明之外另一个关键点是 Maven 仓库镜像。首次执行 install 会下载大量 jar如果本地 settings.xml 没配国内镜像下载过程可能卡在某个依赖上很久。做法是在 settings.xml 里加 mirror 节点mirrorOf 设为 centralurl 指向国内仓库地址然后再执行批处理脚本。这套环境有几组兼容性边界容易踩整理如下环境项推荐配置兼容性说明JDK1.8SpringBoot 2.x 编译目标一般在 8 以上但项目插件在 JDK 8 下验证最充分MySQL5.75.7 使用 com.mysql.jdbc.Driver若本机装的是 8.0驱动类换成 com.mysql.cj.jdbc.Driver 并加 serverTimezone开发工具IDEA / Eclipse资源里出现的 .classpath 是 Eclipse 工程文件IDEA 导入时直接选 Maven 工程再重设 JDK 即可用 IDEA 导入后先确认 Project SDK 为 1.8再把 Maven 路径指向自己解压的目录避免 IDEA 自带 Maven 与项目预期版本不一致。这一步做完后面跑脚本时才能把报错集中在代码本身而不是环境差异上。3. 病历核心模块从实体类到 ElementUI 表单的完整链路3.1 病历表字段设计与实体映射医院病历系统里最核心的实体是病历记录它同时被列表页、表单页、查询接口依赖。按论文和实际建表的常见风格病历信息表可以这样设计字段名字段类型说明idint主键自增patient_namevarchar(50)患者姓名patient_novarchar(30)病历号/就诊号diagnosisvarchar(255)诊断结果doctor_namevarchar(50)接诊医生departmentvarchar(50)科室visit_timedatetime就诊时间数据库列名与 Java 实体属性名的对应依赖 MyBatisPlus 默认开启的驼峰映射patient_name 自动对应 patientName不需要在 XML 里写 resultMap。实体类常见写法如下Data TableName(medical_record) public class MedicalRecord { TableId(type IdType.AUTO) private Integer id; private String patientName; private String patientNo; private String diagnosis; private String doctorName; private String department; private LocalDateTime visitTime; }TableName明确实体对应哪张表防止类名与表名翻译出现偏差TableId的IdType.AUTO表示主键由数据库自增生成插入时 id 传 null 即可。如果业务上把病历号当主键用字符串就把 id 字段类型改成 String生成策略换成INPUT或ASSIGN_ID由前端或后端自行赋值。LocalDateTime对应 MySQL 的 datetime本身没问题但个别旧版 mysql-connector-java 在读取时间类型时会抛异常。碰到这种报错不必改实体优先升级驱动版本或者检查连接 URL 里的时区参数。3.2 Service 与 Controller 的 REST 接口编排后端 Service 层直接继承 MyBatisPlus 的IServiceMedicalRecord就能拿到全套单表方法。Controller 再按 REST 风格暴露四个基础接口分页查列表、保存或更新、删除、按 id 取详情。下面这段 Controller 是这类管理系统最常见的组织方式RestController RequestMapping(/api/record) public class MedicalRecordController { Autowired private MedicalRecordService recordService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) int current, RequestParam(defaultValue 10) int size) { return Result.ok(recordService.page(new Page(current, size))); } PostMapping(/save) public Result save(RequestBody MedicalRecord record) { return Result.ok(recordService.saveOrUpdate(record)); } DeleteMapping(/delete/{id}) public Result delete(PathVariable Integer id) { return Result.ok(recordService.removeById(id)); } GetMapping(/detail/{id}) public Result detail(PathVariable Integer id) { return Result.ok(recordService.getById(id)); } }Controller 里不写业务逻辑只做参数接收和结果包装。recordService.page返回 IPage 对象内部自动拼接 limit 语句前端拿到 records 列表和 total 总数后直接渲染表格和分页器。saveOrUpdate会按主键是否为空自动区分 insert 和 update新增与修改共用同一个接口省掉两套逻辑。Result 是统一返回体一般包含 code、message、data 三个字段前端 axios 拦截器统一判断 code失败后弹出提示。很多项目会自己包装这个类但它确实让所有接口返回结构一致前端拿到 data 后不需要对 null 做各种判断。如果源码里没带按这三个字段补一个即可。3.3 Vue 前端的数据流表格渲染、表单提交与 axios 请求前端页面分列表区和编辑区。列表区用 el-table 绑定后端返回的 records编辑区用 el-form 收集输入提交成功后重新调加载方法让新数据立刻出现在表格里。这是管理端页面的标准数据闭环template div el-form :modelform inline el-input v-modelform.patientName placeholder患者姓名 stylewidth: 160px / el-button typeprimary clicksubmit保存/el-button /el-form el-table :datarecords border el-table-column proppatientName label患者姓名 width120 / el-table-column propdiagnosis label诊断结果 / el-table-column propdoctorName label接诊医生 width120 / el-table-column label操作 width120 template #default{ row } el-button typetext clickedit(row)编辑/el-button /template /el-table-column /el-table /div /template script import axios from axios export default { data() { return { form: {}, records: [] } }, async created() { await this.load() }, methods: { async load() { const { data } await axios.get(/api/record/page, { params: { current: 1, size: 10 } }) this.records data.data.records }, edit(row) { this.form { ...row } }, async submit() { await axios.post(/api/record/save, this.form) this.form {} await this.load() } } } /script#default{ row }是 ElementUI 表格列的作用域插槽写法从当前行解构出 row 对象点编辑就把整行数据浅拷贝进表单后续改动不会影响列表里尚未提交的数据。axios 请求路径/api/record/page之所以不写完整的 localhost 地址是因为开发期由 vite/webpack 的 devServer.proxy 把 /api 转发到 SpringBoot 的 8080 端口联调阶段不用反复改 baseURL部署时再交给 Nginx 做同源转发。医院病历涉及患者隐私演示数据不要用真实姓名和身份证号SQL 脚本里放一批脱敏的假数据就够了这个习惯从开发第一天就该养成。4. 把项目跑起来批处理脚本、数据库初始化与三处常见报错4.1 三个批处理脚本分别承担什么任务资源里的 1-install.bat、2-run.bat、3-build.bat 是三个阶段的入口很多人直接双击没反应是因为机器上还没装好 JDK、Maven、MySQL 和 Node。它们各自的职责大致如下1-install.bat首次运行前执行负责拉取 Maven 依赖并安装前端 node_modules常见内容是mvn install -DskipTests加npm install。2-run.bat开发期启动一个窗口跑 SpringBoot另一个窗口跑 Vue dev server常见写法是start backend cmd /k mvn spring-boot:run再执行npm run serve。3-build.bat上线前构建常见内容是mvn clean package -DskipTests前端产出 dist 目录后端产出 jar 包。从零开始运行时先手动确认四个命令可用不要直接跑脚本java -version mvn -v node -v npm -v这四个命令依次验证 JDK、Maven、Node、npm 是否在 PATH 里。任何一条提示不是内部或外部命令批处理执行到对应步骤就会中断。如果 npm install 卡住超过十分钟一般是默认源太慢可以在执行前先切镜像源npm config set registry https://registry.npmmirror.com切源之后重新删掉 node_modules 再执行 install速度会有明显改善。注意 Maven 首次拉依赖也很耗时同样建议先按第 2 章配好仓库镜像。4.2 数据库初始化与连接配置项目不能凭空运行需要先在 MySQL 中建库并导入初始化 SQL。用 Navicat 或 SQLyog 执行项目附带的 sql 文件即可命令行也可以直接导入mysql -uroot -p -h127.0.0.1 hospital_record.sql如果 sql 文件里已经包含 CREATE DATABASE 语句就不用手动建库如果没有先执行CREATE DATABASE hospital_record DEFAULT CHARSET utf8mb4;再导入。导入后至少确认用户表、病历表存在然后修改 SpringBoot 的数据库配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_record?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver连接 URL 里的参数值得逐个记useUnicode 和 characterEncoding 一起保证中文不乱码useSSLfalse 避免本地连接时的 SSL 握手告警serverTimezoneAsia/Shanghai 在 MySQL 8.0 驱动下必填5.7 不写也能跑但建议统一保留。driver-class-name 要和本地 jar 版本匹配5.x 驱动用 com.mysql.jdbc.Driver8.x 驱动必须换成 com.mysql.cj.jdbc.Driver写错启动阶段会直接报找不到驱动类。另外建议把连接账号单独建一个不要用 root数据库层面就能限制住误删表的操作。对于演示环境用普通账号加只读权限更稳妥。4.3 三种启动期最常见的报错与定位方法后端和前端分别启动后报错基本集中在下面四类可以直接对照排查现象排查方向处理方式8080 端口被占用是否有旧进程占用了 SpringBoot 默认端口netstat -ano 查端口taskkill 结束对应进程或改 server.port数据库连接失败MySQL 服务没启动、账号密码不对、库不存在确认 application.yml 的 url、username、password前端接口 404后端路由不存在或代理没生效浏览器直接访问 /api/record/page 看是否有 JSON 返回跨域 No Access-Control-Allow-Origin前端代理配置缺失检查 vue.config.js 的 devServer.proxy端口冲突最常发生在机器上已跑过其他 Tomcat 进程时用netstat -ano | findstr :8080定位到进程号后结束掉即可注意不要在服务器上乱杀进程。跨域问题要按顺序定位先在后端端口下直接访问接口能出 JSON 说明后端没问题再去看前端 proxy 是否把 /api 转发到了正确地址。代理配置常见这样写module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }devServer.port 是前端开发服务器端口target 是后端真实地址changeOrigin 会把请求头里的 Host 改写成目标地址避免后端因 Host 不一致拒绝请求。前端 axios 如果直接请求完整后端地址就需要后端额外配 CORS 过滤器用 proxy 后可以保持相对路径换环境部署时改动量最小。5. 进阶用.bak做版本回退用 LambdaQueryWrapper 做动态筛选5.1.bak文件比临时 Git 还直观资源包里那些IndexAsideStatic.vue.bak、update-password.vue.bak说明作者在改版时保留了修改前的文件副本。Vue 页面在联调期最容易改坏的就是导航和路由侧边菜单加一项但路由没配页面一点就白屏。这时候用 .bak 恢复是最快的一条路cp src/views/IndexAsideStatic.vue.bak src/views/IndexAsideStatic.vue不熟悉命令行的直接在资源管理器里复制 .bak 文件去掉末尾后缀覆盖回原目录再重启前端服务即可。这个做法比 Git 直观但只能恢复一版没有历史记录所以更合适在正式引入版本管理之前做临时保存。如果你正在迭代这套源码建议把所有 .bak 统一移到项目外的 backup 目录避免 IDE 文件树里出现一堆同名文件干扰检索。update-password.vue.bak 这个文件还暴露了一个排查线索项目里应当有修改密码页面如果前端页面存在但接口一直报错优先回后端看 UserController 的映射路径和前端请求路径是否一致再看 Spring Security 拦截规则有没有把该请求放行。对照 .bak 里的旧请求路径经常能一眼看出改动前后差异。5.2 动态筛选条件怎么交给 LambdaQueryWrapper病历列表最常见的查询是按患者姓名、科室、时间范围任意组合筛选。接口参数全部允许为空再用条件构造器拼装代码量最少且可读性最强Override public PageMedicalRecord searchRecords(String patientName, String department, LocalDateTime start, LocalDateTime end, int current, int size) { LambdaQueryWrapperMedicalRecord wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(patientName), MedicalRecord::getPatientName, patientName) .eq(StringUtils.hasText(department), MedicalRecord::getDepartment, department) .ge(start ! null, MedicalRecord::getVisitTime, start) .le(end ! null, MedicalRecord::getVisitTime, end) .orderByDesc(MedicalRecord::getVisitTime); return recordMapper.selectPage(new Page(current, size), wrapper); }每个条件第一个参数都是布尔值hasText 判断字符串非空start ! null 判断时间对象是否存在。条件为 false 时MyBatisPlus 会在生成 SQL 时自动跳过对应片段所以前端没传科室时不会拼出WHERE department 这种查不到数据的条件。返回 Page 对象后Controller 可以直接交给 Result 包装前端 el-pagination 把 current 和 size 传进来分页和筛选就联动起来了。与手拼 SQL 相比这套写法天然防注入值始终以占位符绑定不需要字符串拼接过滤条件。对于病历这类隐私数据后端在查询前还应加一层当前登录用户的角色校验普通用户只能查到自己科室或自己经手的记录而不是把 searchRecords 无条件暴露给所有账号。把页面跑通和把权限边界做好是两件事后者才是从源码能运行走向系统能交付的关键。本文还有配套的精品资源点击获取