简介:基于SpringBoot+MyBatis+MySQL+Maven+Vue实现的医院急诊系统,是一套面向高校毕业设计或课程实训的完整前后端分离项目。系统包含管理员与用户两类角色,覆盖医生信息、科室管理、紧急预约、病房管理、费用管理、健康码管理、论坛及资讯等模块,既支持日常急诊业务演示,也便于二次开发扩展。压缩包约17.41MB,共852个文件,主要包含后端源码、前端页面组件、数据库脚本、交互逻辑、页面样式及必要配置;其中数据库脚本可直接导入MySQL,启动脚本辅助一键运行,部署门槛低。配套源码结构完整,含菜单权限控制、页面交互与数据表设计,并附带毕业论文文档,整体目录按前端、后端、数据库与文档分区,便于审阅与二次开发。目前已有52人学习下载,适合需要系统化掌握主流前后端框架联动开发、完整项目生命周期实践的在读学生与初级开发者,也可作为毕业设计快速落地模板。
1. 医院急诊系统这个标题,实际是一份 JavaWeb 全栈交付物的说明书
拿到“基于SpringBoot+MyBatis+MySQL+Maven+Vue实现的医院急诊系统”这类标题时,大多数人以为“导入项目、点启动、截图交差”就结束了,真正动手后才发现卡点全在环境一致性和流程细节上:MySQL 连不上、前端白屏、Maven 依赖下不动、分诊排序规则和普通门诊完全不一样。这个标题对应的是一个标准的 JavaWeb 全栈工程:后端 SpringBoot+MyBatis 负责接口和业务,前端 Vue 负责页面,MySQL 存数据,Maven 管依赖和打包。它适合正在做毕设或课设的在校生,也适合第一次接 JavaWeb 全栈小项目的初级工程师,用来演示急诊科的分诊、挂号、收费、药房发药整条链路。难度不在改业务,而在把数据库脚本、后端配置、前端代理这三件事对齐。
2. 技术栈拆解:这套组合为什么是课设与接单的稳妥答案
2.1 SpringBoot + MyBatis:自动配置与可控 SQL 怎么分工
选 SpringBoot 是因为它把 Spring 的配置大部分自动化了,一个spring-boot-starter-web就带起内嵌 Tomcat,不用再去部署外置容器。MyBatis 则是典型的半自动 ORM,SQL 自己写、结果映射用配置文件配,这和 JPA 那种“帮你生成 SQL”的思路完全不同。急诊系统的查询条件非常散:按患者姓名、科室、分诊级别、时间段、状态筛选是常态,用 MyBatis 的动态 SQL 在 XML 里拼where条件,比 JPA 的 Specification 更直观,答辩时也更好讲。
MyBatis 有几个面试和项目里都会碰到的点,这里提前说清楚。一级缓存是 SqlSession 级别的,默认开启,同一个 SqlSession 里两次相同查询不会真正查库;二级缓存默认关闭,如果要开,得在 mapper XML 里加<cache/>,同时实体类要实现序列化。急诊系统里我最不建议开二级缓存,因为分诊状态、叫号状态变化极频繁,缓存命中率低,还容易读到脏数据。另外还有一个 TypeHandler 的概念,比如数据库里存0/1/2,Java 里想映射成分诊级别枚举,或者存 JSON 字符串想映射成对象,都可以自定义 TypeHandler 来实现,课设里用到的频率不高,但属于“会了这个能加分”的知识点。
MyBatis 的 Mapper 接口和 XML 之间靠 namespace 和接口全限定名绑定,SpringBoot 里只需要在启动类加@MapperScan("com.xxx.mapper"),或者每个接口上加@Mapper。我一般推荐用@MapperScan,少写注解,也避免漏标导致启动时报“找不到 bean”。
2.2 Vue 前端:路由与权限页面的常规搭法
这类系统前端基本是 Vue 2 配 Element UI,或者 Vue 3 配 Element Plus。拿到项目先看package.json里依赖版本:如果里面有"element-ui": "^2.x",那一定是 Vue 2;如果是"element-plus",就要求 Vue 3。混着用是白屏高发原因,后面避坑部分会细说。
前端页面围绕角色来设计,常见是登录页、医生工作台、护士分诊台、药房发药、管理员后台这几块。登录之后由后端返回角色信息和菜单权限,前端用 Vue Router 的addRoute动态挂载路由。这种动态路由方案比把全部路由写在静态配置里更好:不同角色看不到自己没有权限的菜单,接口层再做一层拦截,防止有人直接访问路径绕过前端校验。
axios 封装是每一套这种项目都有的公共部分。一般约定所有接口请求带/api前缀,通过request.js里的拦截器统一加 token,响应拦截器里统一处理 401 跳登录。后端如果返回{ code: 200, data: ... }这种结构,前端也要在拦截器里先剥掉外层。前后端联调时大量的所谓 bug,其实是这里的数据结构没对齐。
2.3 Maven 在项目里的真实位置:依赖、镜像与打包
Maven 在这个工程里管两件事:依赖下载和构建打包。pom.xml里的spring-boot-starter-parent决定了 SpringBoot 版本,常见的课设比较稳的组合是 SpringBoot 2.7.x 配 JDK 8/11;如果标题里的“配套软件”给你的是 JDK 17,而后端是 SpringBoot 3.x,那又是另一套写法。遇到版本对不上,优先看pom.xml里 parent 版本是多少。
Maven 下载依赖慢是新手最容易卡住的地方。默认中央仓库在国外,国内机器的速度很难让人接受,解决办法是改~/.m2/settings.xml,配置阿里云镜像。这是公开的前端仓库镜像地址,不涉及任何网络策略问题,单纯是加速。配置之后mvn clean package -DskipTests基本能在几分钟内完成。
打包阶段用了spring-boot-maven-plugin的repackage目标,打出来的 jar 是可直接java -jar运行的胖 jar,不需要外部 Tomcat。这也是这套技术栈能“一个 jar 跑后端”的原因。要注意区分mvn package和mvn install:单机跑项目用package就够,install是往本地仓库装依赖库用的,课设阶段基本用不到。
3. 把工程跑起来:建库、启动后端、联调前端的三步走
3.1 先过数据库这一关:建库脚本与 MySQL 连接参数
拿到项目包后我习惯先把标题里说的“数据库”部分搞定,也就是那几份.sql脚本。常见做法是用 MySQL 命令行直接建库导数据,而不是一上来就双击.sql。因为双击打开再复制粘贴,很容易因为编码不对产生中文乱码,命令行的SOURCE方式能暴露编码问题但不会静默写坏数据。
# 进入 MySQL 命令行(Windows 下同样适用) mysql -u root -p # 创建急诊系统数据库,utf8mb4 是中文项目的默认选择 CREATE DATABASE emergency_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换到目标库 USE emergency_system; # 导入项目里提供的 SQL 脚本,路径按实际位置改 SOURCE C:/deploy/sql/emergency_system.sql;这里有几个参数值得较真。utf8mb4比utf8多支持 emoji 和生僻字,现在几乎所有新项目都应该用utf8mb4。脚本里的COLLATE utf8mb4_general_ci是排序规则,ci 结尾表示 case-insensitive,也就是查询时不区分大小写,适合患者姓名这种查询场景。SOURCE后面跟的是本机文件路径,如果是从网上下载的代码包,SQL 文件很可能带了 BOM 头或 UTF-8 编码以外的格式,导入后中文变成乱码,这时候用记事本另存为 UTF-8 再导一次就好。
导入完成后检查一下最核心的几张表。急诊系统和普通门诊不一样的地方,在于一定有一张分诊记录表,里面通常会有分诊级别字段(比如I/II/III/IV,I 级最危重),还有叫号状态字段。如果发现表结构里没有这类字段,说明标题里的“急诊”只是包装,后面要做的定制就在这里。
3.2 后端启动:改对 application.yml 再 mvn package
后端启动前,重点检查src/main/resources/application.yml,一般有三处要动:数据库连接串、MyBatis 的 mapper 配置、端口号。下面这份配置是急诊系统这类项目最常见的样子。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/emergency_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.emergency.entity configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpluseSSL=false是 MySQL 8.x 驱动必须加的,不加就会报 SSL 连接错误;serverTimezone=Asia/Shanghai解决时区差 8 小时的问题。characterEncoding=utf8和driver-class-name里的cj都是连接阶段的细节,缺一个就可能在启动时报编码或驱动类不存在。mapper-locations指向 XML 所在目录,后面第 5 章会讲这个配置不对的典型症状。
改完配置就可以打包启动了,命令很简单:
# 跳过测试,课设项目的单元测试经常依赖特殊环境,跳过最省心 mvn clean package -DskipTests # 进入 target 目录运行生成的 jar java -jar target/emergency-system-0.0.1-SNAPSHOT.jar启动后看日志确认“Started Application in x.xx seconds”,然后访问http://localhost:8080探一下。如果后端接口路径带/api,直接访问根路径会 404,这不算问题。-DskipTests和-Dmaven.test.skip=true有区别:前者编译测试代码但不运行,后者连测试代码都不编译;拿到陌生项目时我一般用前者,避免因为测试代码编译失败导致整个包打不出来。
3.3 前端联调:npm 依赖、代理与打包进 SpringBoot
前端项目一般是一个独立目录,命令行进入后先安装依赖。如果npm install太慢,加--registry=https://registry.npmmirror.com指向国内镜像。装完依赖后启动开发服务器,重点看vue.config.js,里面的代理配置决定前端请求怎么找到后端。
// vue.config.js 开发环境代理配置 const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, // 如果后端接口没有 /api 前缀,这里需要去掉 pathRewrite: { '^/api': '' } } } } })这段配置让前端开发服务器监听 3000 端口,凡是以/api开头的请求都转发到后端 8080。pathRewrite是最容易踩的:如果后端的 Controller 里写的是@RequestMapping("/login"),而前端请求的是/api/login,就必须去掉/api前缀,否则后端返回 404;如果后端接口本身就带/api,这个 rewrite 要去掉,否则后端也 404。判断依据很简单,打开后端源码看一眼 Controller 类上的@RequestMapping注解即可。
联调通过后,如果要布置成“一个 SpringBoot 应用跑前后端”,常见做法是npm run build把前端产物.dist目录里的文件复制到src/main/resources/static下,再重新mvn package。这里有个坑:Vue 路由如果是 history 模式,直接访问http://localhost:8080/register这种子路径会 404,因为后端没有对应的 controller。要么改用 hash 模式,要么在后端加一个转发控制器把所有非接口路径指回index.html。开发阶段建议保留跨域方案,部署阶段再考虑合并,别一开始就混在一起排错。
4. 按急诊流程定制:分诊、权限、挂号并发三处重点改造
4.1 分诊多条件查询:MyBatis 动态 SQL 与急诊排序规则
把工程跑通之后,真正要动脑子的是怎么让它像“急诊系统”,而不只是把表名换成患者。急诊和普通门诊的最大差异是接诊顺序不由先来后到决定,而是按分诊级别排序。所以分诊列表的查询,不能简单ORDER BY create_time,要做到危重患者排在前面,同时还要支持护士按姓名、科室、日期、状态筛选。
这种多条件查询在 MyBatis 里用动态 SQL 实现是标准做法。下面这个查询方法覆盖了分诊台最常见的一屏:
<select id="listTriage" resultType="TriageInfo" parameterType="map"> SELECT patient_name, triage_level, status, create_time FROM triage_info <where> <if test="patientName != null and patientName != ''"> AND patient_name LIKE CONCAT('%', #{patientName}, '%') </if> <if test="deptId != null"> AND dept_id = #{deptId} </if> <if test="status != null"> AND status = #{status} </if> <if test="beginDate != null and endDate != null"> AND create_time BETWEEN #{beginDate} AND #{endDate} </if> </where> ORDER BY <choose> <when test="levelOrder == 1"> FIELD(triage_level, 'I', 'II', 'III', 'IV') </when> <otherwise> create_time DESC </otherwise> </choose> </select><where>标签会自动处理条件为空时多余的AND,避免“第一个条件为空,SQL 变成WHERE AND”这种语法错误。LIKE CONCAT('%', #{patientName}, '%')不直接写LIKE '%${patientName}%',是为了防止 SQL 注入,#{}会生成预编译占位符。FIELD(triage_level, 'I', 'II', 'III', 'IV')是 MySQL 自带的排序函数,按给定顺序返回权重,I 级排最前。这里的triage_level就是急诊分诊的核心字段,如果项目原本的表结构里没有,就要新增。
4.2 医生/护士/药房/管理员:动态路由与接口拦截
一个完整的急诊系统至少有四类账号:医生看诊、护士分诊、药房发药、管理员维护基础数据。前端菜单要按角色区分,后端接口也不能只靠前端藏路由,必须做一层拦截。
前端实现动态路由的逻辑大概是这样:登录接口返回用户的角色列表和菜单列表,前端根据这些数据用router.addRoute()逐个添加路由。这样普通护士的浏览器里根本不存在“药品管理”这个路由,按 F12 去改 localStorage 里的菜单数据也只是自欺欺人,因为后端接口会拒绝。更稳妥的做法是后端生成菜单树,前端只负责渲染。
后端拦截器是权限控制的兜底。一个精简的实现如下:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 if ("/api/auth/login".equals(request.getRequestURI()) || request.getRequestURI().startsWith("/static/")) { return true; } // 校验 token,前端 axios 拦截器会把 token 放在 Authorization 头 String token = request.getHeader("Authorization"); if (token == null || !tokenService.verify(token)) { response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录过期\"}"); return false; } // 角色校验,路径包含 /admin/ 的要求管理员 if (request.getRequestURI().startsWith("/api/admin/") && !tokenService.isAdmin(token)) { response.setStatus(403); return false; } return true; }这里有个特别容易忽略的点:前端发起的非简单请求会先触发浏览器 OPTIONS 预检请求,如果拦截器没放行 OPTIONS,前端会看到“请求成功但无法读取响应”或者 401,网络请求列表里却能看到接口已经返回。解决方案是在拦截器最前面加"OPTIONS".equals(request.getMethod())就直接放行。另一个常见问题是 token 的 header 名称,前端拦截器写的是Authorization,后端读的是authorization,大小写不一致也会导致登录后立刻被踢出。
4.3 挂号并发与事务:别让一个号被两个人挂走
急诊挂号的数据一致性,是整套系统里最值得写进简历的一个点。表面看就是往挂号表插一条记录,实际上要同时处理号源扣减和重复挂号两个问题。如果先查号源数量,再判断大于 0 就插入,两个请求同时进来就会都通过检查,产生超挂。常见做法是用数据库的原子更新来扣减号源。
@Service public class RegisterService { @Transactional(rollbackFor = Exception.class) public void register(RegisterRequest req) { // 1. 原子扣减号源,update 语句自带行锁 int updated = registerMapper.deductStock(req.getDoctorId(), req.getDate()); if (updated == 0) { throw new BusinessException("该号源已被挂完,请选择其他时段"); } // 2. 扣减成功才插入挂号记录 registerMapper.insertRegister(req); } }对应的 SQL 大致是UPDATE doctor_schedule SET remain = remain - 1 WHERE doctor_id = #{doctorId} AND work_date = #{date} AND remain > 0。这条语句的remain > 0是并发控制的关键,数据库的行锁保证同一时刻只有一个事务能执行成功,另一个事务更新行数为 0,从而让业务层判断出号源已满。
@Transactional(rollbackFor = Exception.class)这里的参数值得解释。Spring 的事务默认只在遇到 RuntimeException 时回滚,而很多业务代码抛的是自定义检查型异常(继承 Exception),如果不配置rollbackFor,事务会提交而不是回滚,结果就是号源扣了但挂号记录没插进去。这是我见过的真实翻车现场,也是面试里经常追问“事务失效场景”时的标准回答。另外注意,事务方法被同类内部方法调用会失效,因为走的是 this 调用而非代理对象,所以这种带事务的 service 方法一定要从 controller 层直接调用。
5. 部署与运行避坑:版本、SSL、依赖、XML 四个高频翻车点
5.1 前端白屏:控制台一片红但后端明明在跑
现象:npm run dev后浏览器打开是空白页,控制台报Failed to resolve component或Cannot read properties of ...,后端却一切正常。
原因:这套项目最常见的是 Vue 和组件库版本错配。Element UI 是 Vue 2 专属,Element Plus 是 Vue 3 专属,混用会直接挂。还有 Node 版本过高导致 webpack 报ERR_OSSL_EVP_UNSUPPORTED,这是老项目在 Node 17 以上最常见的兼容问题。
解决:先看package.json,确认vue和element-ui/element-plus版本属于同一代。然后用nvm装项目要求的 Node 版本(老 Vue 2 项目我一般用 Node 14 或 16)。最好保留项目的package-lock.json,用npm ci代替npm install,能严格按锁定版本安装,减少玄学问题。
5.2 MySQL 连接失败:SSL 和时区两个背锅侠
现象:后端启动直接报Communications link failure,或者提示The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。中文乱码则表现为数据库里数据正常,查询出来全是?。
原因:MySQL 8.x 的驱动默认要求启用 SSL 连接,本地开发环境没有证书;时区问题是因为驱动读取了系统的中文时区描述,无法解析。乱码则是数据库连接串或表结构没设置为 utf8mb4。
解决:连接串里加useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。如果用的是 MySQL 5.7,驱动类名是com.mysql.jdbc.Driver,8.x 是com.mysql.cj.jdbc.Driver,别混。乱码问题三层一起改:建库时用utf8mb4,连接串带characterEncoding=utf8,.sql文件本身是 UTF-8 编码。只要这三层对齐,基本不会再出乱码。
5.3 Maven 下载慢与打包失败:镜像与 JDK 版本
现象:mvn clean package停在下载进度条半天不动,或者打包时提示Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin,再细看是invalid target release: 17之类的错误。
原因:第一次构建要下载几百 MB 依赖,默认中央仓库速度不稳定是主因。打包失败通常是 JDK 版本和 Maven 编译插件要求不一致,比如pom.xml里写了java.version=1.8,但当前环境是 JDK 17,或者反过来。
解决:在~/.m2/settings.xml里加阿里云镜像镜像配置:
<mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>aliyun public mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>JDK 版本问题先看项目要求:SpringBoot 2.x 对应 JDK 8/11,SpringBoot 3.x 必须 JDK 17+。用一个 JDK 版本把几套项目全跑通是不现实的,建议本机装两个 JDK,用 IDEA 每个项目的 SDK 单独指定。
5.4 MyBatis 报 Invalid bound statement:XML 没进 jar
现象:启动成功,但一调用某个 mapper 方法就报Invalid bound statement (not found): com.xxx.mapper.TriageMapper.listTriage。
原因:Mapper 接口的完全限定名和 XML 文件中 namespace 不一致,或者映射文件里的<select>标签 id 与接口方法名不一致。还有一种隐蔽情况:XML 文件放在src/main/java目录树下,Maven 默认只打包resources目录,导致 XML 根本没进 jar。
解决:先确认application.yml里mybatis.mapper-locations: classpath:mapper/*.xml。然后把 XML 统一放在src/main/resources/mapper下。如果项目结构特殊,必须在pom.xml里显式声明资源目录。改动后执行mvn clean package,用解压工具打开 jar 确认/BOOT-INF/classes/mapper/下能看到 XML 文件,这一步能排除 90% 的绑定问题。
5.5 登录后路由 404:history 模式与打包路径
现象:开发环境登录后从/跳到/register正常,但npm run build后部署到后端,刷新/register页面直接 404。
原因:前端用了 Vue Router 的 history 模式,路由是前端模拟的,没有真实文件;部署到 SpringBoot 后,请求/register时后端没有对应 Controller,返回 404。开发模式下的 404 被 devServer 兜底了,所以看不见。
解决:最简单的验证阶段做法是改回 hash 模式,路由变成/#/register,刷新不 404。如果要保留 history 模式,后端用 forward 把非接口请求转发到index.html,或者部署时用 Nginx 的try_files $uri $uri/ /index.html。开发归开发、部署归部署,别在联调阶段就混在一起排查。
6. 把课设做成作品:定时任务、验收清单与交付前检查
6.1 给系统加一个自动取消超时挂号的定时任务
急诊系统有一个真实业务场景:患者挂号后长时间未就诊,会占用号源影响后续患者。很多开源版本只做了挂号功能,没有超时释放,这正好可以作为自己动手加的亮点。常见做法是用 SpringBoot 的@Scheduled定时任务实现,在启动类加@EnableScheduling,然后写一个任务类:
@Component public class RegisterTimeoutTask { // cron 表达式:每 5 分钟执行一次,扫描超时 30 分钟的挂号记录 @Scheduled(cron = "0 0/5 * * * ?") public void cancelTimeoutRegisters() { // update register_info // set status = '已取消' // where status = '待就诊' // and create_time < date_sub(now(), interval 30 minute) registerMapper.cancelTimeout(30); } }这个功能的重点是 SQL 里的date_sub(now(), interval 30 minute),让数据库判断超时而不是 Java 端算好时间传进去,避免各服务器时钟不一致。定时任务在课设里够用,真正上线要考虑分布式锁防止多实例重复执行,但这是后话,能让演示时“挂一个号等 30 分钟自动释放”这个效果跑出来,已经比大多数没有这个功能的版本更进一步。演示时可以把interval 30 minute临时改成interval 1 minute,现场展示完再改回来。
6.2 答辩/交工前的验证方法:测试账号、并发冒烟与自检表
我做这类项目交付前,最怕的不是功能缺失,而是环境换一台机器就起不来。所以一定要列一张自检表,逐项打勾,而不是“刚才还能跑”就交付。下面这张表基本覆盖了从拿到源码到部署完成的全部关键节点:
| 检查项 | 操作 | 通过标准 |
|---|---|---|
| 数据库脚本 | 执行建库 SQL 和初始化数据 | 管理员和测试账号能登录 |
| 后端打包 | mvn clean package -DskipTests | jar 启动后/api/auth/login可访问 |
| 前端联调 | npm run dev或npm run build | 页面按角色渲染,无白屏,接口无跨域报错 |
| 部署整合 | build 产物放入 static 或使用 Nginx | 刷新子路由不 404,图片能加载 |
| 并发冒烟 | 两个账号同时挂同一个号源 | 只成功一个,号源数量正确 |
验证时我习惯用浏览器无痕窗口开两个会话,分别用护士账号和医生账号走一遍分诊到叫号的流程,这招比接口测试工具更直观。并发挂号用两个浏览器窗口或一个 Postman 快速点两次,看数据库最终数据。如果是答辩前夜才拿到项目,优先保“能启动、能登录、一条主流程走通”,别在权限细节上死磕。这类系统我做过三次之后最大的体会是:环境一致性的坑远多于业务代码的坑,与其猜,不如把上述清单完整走一遍。希望帮到你。
本文还有配套的精品资源,点击获取