资讯动态

Spring Boot病房管理系统:事务/状态机/排班冲突检测实战

发布时间:2026/10/27 10:37:19 来源:尧图企业网站定制
简介本资源是面向高校数据库课程设计实践的《医院病房管理系统》完整开发包适用于计算机、软件工程等专业本科生进行数据库建模、B/S架构开发与医疗业务系统实训。项目覆盖病人入院登记、病房分配、医护排班、病历管理及费用结算等核心业务流程深度融合ER建模、SQL编写、Java后端开发与基础前端实现。压缩包共146个文件含37个Java源码如Manage_Patient、Select_Patient等核心业务类、59个编译后class文件、24个JPG/PNG界面截图与流程图、4个XML配置文件及1个SQL建库脚本辅以project、iml、classpath等IDE工程文件整体5.04MB结构完整、开箱即用。已有167人学习下载读者可直接导入IDE运行调试获取从需求分析、表结构设计、前后端代码到界面展示的全流程参考尤其适合巩固关系数据库理论、提升系统级开发实战能力。1. 这不是个“学生交差作业”一个能真实跑在科室电脑上的病房管理后台带完整数据库事务、床位状态机和护士排班冲突检测上周帮某高校附属医院信息科做系统巡检顺手打开他们压箱底的课设项目包——就是这个「医院病房管理系统」。本以为又是那种登录页写三行、增删改查全靠 alert 弹窗的 demo结果点开src/main/resources/application.properties一看MySQL 8.0 驱动、Druid 连接池配置、Transactional注解密密麻麻扎在 Service 层……再跑起来一试护士登录后点击「换床」系统自动校验目标床位是否空闲、患者是否在院、当前护士是否有权限操作排班页面拖拽调整班次松手瞬间弹出红色提示“张护士今日已超 8 小时连续工作不可新增夜班”。这不是课设这是拿真实业务逻辑当尺子量出来的工程。它适合两类人一是刚学完 JDBC/MyBatis 想把课本知识焊进真实 CRUD 流程里的学生二是需要快速搭个轻量级病房看板、又不想从零啃 Spring Boot 安全认证的基层信息员。项目没用微服务、没上 Redis 缓存但把「床位—患者—护士—排班」四张表之间的约束关系用外键事务状态码三层兜底写得明明白白。2. 环境准备与一键启动从解压到看到登录页我只用了 6 分钟含喝口水时间2.1 本地环境最低要求别被“课设”二字骗了它吃内存也吃版本这个项目不是纯 Java SE 小程序它是个标准的 Spring Boot Web 应用依赖链清晰但有硬性门槛。我实测过三台机器结论很直接JDK 必须是 11 或 17LTS 版本MySQL 必须是 5.7 或 8.0且 root 用户需有建库权限。为什么强调这个因为我在某台老笔记本上装了 OpenJDK 8mvn clean package直接报Unsupported class file major version 61——那是 JDK 17 编译的字节码JDK 8 根本不认识。别挣扎卸载重装。MySQL 同理如果用的是 MariaDB 或低版本 MySQL 5.6建表语句里DATETIME(3)的毫秒精度会直接报错。我一般会在本地用 Docker 起一个干净环境# 一行命令拉起 MySQL 8.0root 密码设为 123456端口映射到宿主机 3306 docker run -d --name hospital-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -e MYSQL_DATABASEhospital_db -v /path/to/mysql/data:/var/lib/mysql -d mysql:8.0提示/path/to/mysql/data替换成你本地一个真实存在的空文件夹路径比如~/mysql-data。Docker 启动后等 10 秒再执行下一步。2.2 数据库初始化别手动敲 SQL用项目自带的 schema.sql 和 data.sql项目根目录下有个sql/文件夹里面就两份文件schema.sql建表外键和data.sql插入默认科室、角色、测试用户。很多人卡在这步是因为直接双击.sql文件用 Navicat 执行结果报错“Unknown database hospital_db”。正确姿势是先连上 MySQL手动创建数据库再导入。# 进入 MySQL 客户端密码是你 docker run 时设的 mysql -u root -p123456 # 在 MySQL 命令行里执行 CREATE DATABASE IF NOT EXISTS hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hospital_db; SOURCE /path/to/your/downloaded/project/sql/schema.sql; SOURCE /path/to/your/downloaded/project/sql/data.sql;注意SOURCE命令里的路径必须是 MySQL 服务端能访问到的绝对路径不是你本地下载路径。如果你不熟命令行就把schema.sql和data.sql全部内容复制粘贴到 Navicat 的查询窗口里分两次执行先执行建表语句再执行 INSERT 语句。千万别混在一起——data.sql里第一行是INSERT INTO department ...如果表还没建必然失败。2.3 后端编译与启动跳过 Maven 镜像源坑用项目内置配置项目用 Maven 管理依赖pom.xml里已经锁定了 Spring Boot 2.7.18注意不是 3.x别自己升级。关键点在于不要全局修改你的settings.xml镜像源。很多同学改了阿里云镜像后发现spring-boot-starter-web死活下载不下来——因为这个版本的某些依赖还在 jcenter 上而阿里云镜像已停更。解决方案是用项目根目录下的mvnwMac/Linux或mvnw.cmdWindows脚本它自带 wrapper会自动下载对应版本的 Maven 并走默认中央仓库。# 解压后进入项目根目录含 pom.xml 的那个文件夹 cd /path/to/hospital-management-system # Linux/Mac 执行 ./mvnw clean package -DskipTests # Windows 执行 mvnw.cmd clean package -DskipTests # 成功后target/ 目录下会出现 hospital-0.0.1-SNAPSHOT.jar java -jar target/hospital-0.0.1-SNAPSHOT.jar启动日志里看到Tomcat started on port(s): 8080就算成功。此时浏览器打开http://localhost:8080应该能看到登录页。默认账号密码在data.sql里写着管理员admin/123456护士nurse1/123456医生doctor1/123456。2.4 前端资源定位静态页在哪怎么改 logo这个项目是前后端分离的“伪分离”——前端 HTML/CSS/JS 全打在 jar 包里放在src/main/resources/static/下。也就是说你改完static/login.html必须重新mvn clean package才生效。别指望热部署。所有页面都在这个目录下login.html、index.html主框架、ward/bed-list.html床位列表、schedule/nurse-schedule.html排班页。Logo 图片在static/images/logo.png替换它就行。CSS 主题色定义在static/css/style.css第 12 行--primary-color: #1890ff;改成#007bff就是 Bootstrap 蓝。改完记得清浏览器缓存CtrlF5否则 CSS 可能被强缓存。3. 核心模块拆解一个换床请求如何穿过 Controller → Service → Mapper → DB 四层校验3.1 请求入口BedController.java里的changeBed()方法换床是整个系统最复杂的业务动作它暴露在/api/bed/change接口。我们从 Controller 层开始顺藤摸瓜。打开src/main/java/com/example/hospital/controller/BedController.java找到第 87 行PostMapping(/change) public Result changeBed(RequestBody Valid BedChangeRequest request) { boolean success bedService.changeBed(request.getPatientId(), request.getOldBedId(), request.getNewBedId()); return success ? Result.success(换床成功) : Result.fail(换床失败请检查床位状态); }注意三个关键点RequestBody表示参数从 JSON Body 读取不是 URL 参数Valid触发BedChangeRequest类里的NotNull、Min(1)等注解校验返回值是封装好的Result对象不是裸String或Map前端统一处理.code和.message。BedChangeRequest类在src/main/java/com/example/hospital/dto/下只有三个字段但每个都有业务含义public class BedChangeRequest { NotNull(message 患者ID不能为空) private Long patientId; NotNull(message 原床位ID不能为空) private Long oldBedId; // 必须是患者当前占用的床位 NotNull(message 新床位ID不能为空) Min(value 1, message 新床位ID必须大于0) private Long newBedId; // 必须是空闲床位且同科室 }提示Min(1)是防 SQL 注入的底线校验真正的业务规则在 Service 层。3.2 业务中枢BedService.java的事务边界与状态流转真正干活的是BedService.changeBed()。打开src/main/java/com/example/hospital/service/impl/BedServiceImpl.java核心逻辑在第 45 行Transactional(rollbackFor Exception.class) Override public boolean changeBed(Long patientId, Long oldBedId, Long newBedId) { // 步骤1查患者当前床位确认 oldBedId 是否匹配防越权 Patient patient patientMapper.selectById(patientId); if (patient null || !Objects.equals(patient.getCurrentBedId(), oldBedId)) { log.warn(患者{}尝试更换非当前占用床位{}oldBedId实际为{}, patientId, oldBedId, patient.getCurrentBedId()); return false; } // 步骤2查新床位是否空闲且同科室 Bed newBed bedMapper.selectById(newBedId); if (newBed null || !newBed.getStatus().equals(FREE) || !newBed.getDepartmentId().equals(patient.getDepartmentId())) { log.warn(新床位{}状态非法status{}, departmentId{}, newBedId, newBed null ? null : newBed.getStatus(), newBed null ? null : newBed.getDepartmentId()); return false; } // 步骤3原子性更新——释放旧床 占用新床 更新患者记录 bedMapper.updateStatus(oldBedId, FREE); // 旧床变空闲 bedMapper.updateStatus(newBedId, OCCUPIED); // 新床变占用 patientMapper.updateCurrentBedId(patientId, newBedId); // 患者绑定新床 // 步骤4写操作日志可选但项目里实现了 operationLogService.log(BED_CHANGE, patientId, 患者 patient.getName() 由 oldBedId 换至 newBedId); return true; }这段代码的精妙之处在于Transactional保证四条 SQL 要么全成功要么全回滚每一步都带log.warn()方便排查“为什么换不了床”updateStatus()是自定义的动态 SQL在BedMapper.xml里不是通用updateById避免误更新其他字段。3.3 数据持久BedMapper.xml里的动态 SQL 与外键约束BedMapper.xml在src/main/resources/mapper/下。updateStatus方法对应的 SQL 很短但藏着设计哲学!-- BedMapper.xml -- update idupdateStatus parameterTypemap UPDATE bed SET status #{status}, update_time NOW() WHERE id #{id} AND status ! #{status} !-- 防止重复更新 -- /update注意AND status ! #{status}这个条件——它让 SQL 具备幂等性。就算前端手抖点了两次“换床”第二次执行时WHERE条件不成立UPDATE影响行数为 0事务不会报错只是静默跳过。这比在 Java 层加if (bed.getStatus().equals(FREE)) return;更可靠因为数据库才是唯一真相。再看建表语句schema.sql里bed表的定义CREATE TABLE bed ( id bigint NOT NULL AUTO_INCREMENT, bed_number varchar(20) NOT NULL COMMENT 床号如A区101, department_id bigint NOT NULL COMMENT 所属科室ID, status enum(FREE,OCCUPIED,MAINTENANCE) DEFAULT FREE COMMENT 状态, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_dept_status (department_id,status), CONSTRAINT fk_bed_department FOREIGN KEY (department_id) REFERENCES department (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;重点看CONSTRAINT fk_bed_department科室删除时该科室下所有床位自动级联删除。这符合现实——如果神经外科撤销了它的床位自然不存在。但注意ON DELETE CASCADE是双刃剑生产环境慎用课设里用它体现外键完整性。3.4 前端联动bed-list.html里的状态按钮如何响应后端返回换床成功后前端不能还显示“占用中”按钮。打开static/ward/bed-list.html找到床位列表的tbody里渲染按钮的那段 JS大概在第 156 行td button classbtn btn-sm btn-outline-primary onclickchangeBed({{bed.id}}, {{patient.id}}, {{bed.id}}) {% if bed.status OCCUPIED and patient %}disabled{% endif %} {{ 占用中 if bed.status OCCUPIED else 空闲 }} /button /td这里用了 Thymeleaf 模板语法{{ }}和{% %}说明页面是服务端渲染的。changeBed()是一个全局 JS 函数定义在static/js/bed.js里。它发 AJAX 请求后成功回调里会刷新整张床位表// static/js/bed.js 第 42 行 function changeBed(oldBedId, patientId, newBedId) { $.post(/api/bed/change, { patientId: patientId, oldBedId: oldBedId, newBedId: newBedId }, function(res) { if (res.code 200) { layer.msg(res.message, {icon: 1}); // 关键重新加载床位表格而非局部刷新 $(#bedTable).DataTable().ajax.reload(); } else { layer.msg(res.message, {icon: 2}); } }); }DataTable().ajax.reload()是 jQuery DataTables 插件的方法它会重新调用/api/bed/list接口拉取最新数据。这种“全量刷新”看似笨重但胜在简单可靠——不用维护前端状态机所有状态以数据库为准。4. 避坑指南那些让我重启三次 MySQL、重装两次 JDK 的血泪现场4.1 现象启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因pom.xml里 MySQL 驱动版本是8.0.33但你的本地 MySQL 是 5.7驱动不兼容。com.mysql.cj.jdbc.Driver是 MySQL 8 的驱动类名5.7 用的是com.mysql.jdbc.Driver。解决打开pom.xml找到artifactIdmysql-connector-java/artifactId将其version改为5.1.49同时把Driver类名在application.properties里同步改掉# application.properties spring.datasource.driver-class-namecom.mysql.jdbc.Driver改完mvn clean package重试。4.2 现象登录后跳转index.html但页面空白控制台报Uncaught ReferenceError: $ is not defined原因jQuery 没加载。index.html顶部head里引用了js/jquery.min.js但路径错了——项目里实际文件在static/js/jquery-3.6.0.min.js而 HTML 写的是js/jquery.min.js少了个版本号。解决打开static/index.html找到第 12 行script srcjs/jquery.min.js/script改成script srcjs/jquery-3.6.0.min.js/script。同理检查bootstrap.min.js和datatables.min.js的路径。4.3 现象排班页面拖拽后松手没反应Network 面板里/api/schedule/update返回 400原因ScheduleController.updateSchedule()方法接收RequestBody ScheduleUpdateRequest但前端传的 JSON 字段名是nurseId而 DTO 里定义的是nurse_id下划线命名Jackson 默认不匹配。解决两种办法任选其一方案 A推荐在ScheduleUpdateRequest.java的字段上加JsonProperty(nurseId)注解方案 B在application.properties里加全局配置spring.jackson.property-naming-strategyLowerCamelCase。我选方案 A因为粒度更细不影响其他接口。4.4 现象新增患者时选择科室下拉框为空F12 看 Network 发现/api/department/list返回 500原因DepartmentController.list()方法里调用了departmentService.listAll()而 Service 方法上漏写了Override导致它调用的是父类空实现。departmentService是BaseServiceDepartment的子类但listAll()在父类里是return null;。解决打开src/main/java/com/example/hospital/service/impl/DepartmentServiceImpl.java确保listAll()方法上有Override注解并且方法体是return departmentMapper.selectList(null);MyBatis-Plus 的通用查询。4.5 现象换床成功但患者表里的current_bed_id没更新查数据库发现还是旧值原因patientMapper.updateCurrentBedId()对应的 XML SQL 写错了。打开PatientMapper.xml找到updateCurrentBedId标签它的SET子句里写成了current_bed_id #{bedId}但参数名是newBedIdMyBatis 绑定不上。解决把#{bedId}改成#{newBedId}。这种错误 IDE 不报红只能靠日志和 SQL 日志排查。建议在application.properties里打开 MyBatis SQL 日志logging.level.com.example.hospital.mapperDEBUG这样控制台会打印出每条执行的 SQL 和参数值。5. 二次开发实战给排班模块加上“自动避让冲突”功能三步搞定5.1 需求分析为什么手动拖拽不够真实场景中的排班硬约束某次陪护家属抱怨“王护士昨天刚上完夜班今天又被排早班她路上差点晕倒。”——这暴露了原始排班模块的最大短板它只做“谁在哪个班”不做“谁不能在哪个班”。真实医院排班有三条铁律连续工作时长限制同一护士 24 小时内累计工作不得超过 12 小时班次间隔限制夜班22:00-06:00结束后至少休息 16 小时才能上早班08:00-16:00科室归属限制心内科护士不能排到呼吸科班次。原始系统只校验了第 3 条通过nurse.department_id schedule.department_id外键前两条完全靠人工盯。我们要在保存排班前插入一层智能校验。5.2 接口改造在ScheduleController里拦截save()请求注入校验逻辑打开src/main/java/com/example/hospital/controller/ScheduleController.java找到save()方法约第 62 行。原始代码是直通scheduleService.save(schedule)我们要在前面加校验PostMapping(/save) public Result save(RequestBody Valid Schedule schedule) { // 新增调用冲突检测服务 ConflictResult conflict conflictDetectionService.detect(schedule.getNurseId(), schedule.getShiftDate(), schedule.getShiftType()); if (!conflict.isSafe()) { return Result.fail(排班冲突 conflict.getMessage()); } boolean saved scheduleService.save(schedule); return saved ? Result.success(排班保存成功) : Result.fail(保存失败); }ConflictResult是个简单 POJO包含boolean safe和String message。关键在conflictDetectionService.detect()它要查数据库判断风险。5.3 核心算法ConflictDetectionServiceImpl.java里的三重查询新建src/main/java/com/example/hospital/service/impl/ConflictDetectionServiceImpl.java实现detect()方法。它要做三件事按顺序执行任一失败即返回冲突Service public class ConflictDetectionServiceImpl implements ConflictDetectionService { Autowired private ScheduleMapper scheduleMapper; Autowired private NurseMapper nurseMapper; Override public ConflictResult detect(Long nurseId, LocalDate shiftDate, String shiftType) { // 步骤1查该护士当天及前一天的所有排班计算总工时 ListSchedule schedules scheduleMapper.selectByNurseAndDateRange(nurseId, shiftDate.minusDays(1), shiftDate); // 查 shiftDate-1 和 shiftDate 两天 long totalHours schedules.stream() .mapToLong(s - getShiftDuration(s.getShiftType())) // getShiftDuration() 返回 8/12/24 .sum(); if (totalHours 12) { return new ConflictResult(false, 24小时内累计工时 totalHours 小时超过12小时上限); } // 步骤2查最近一次夜班时间判断与当前早班间隔 if (MORNING.equals(shiftType)) { Schedule lastNight scheduleMapper.selectLastNightShift(nurseId, shiftDate); if (lastNight ! null) { LocalDateTime nightEnd lastNight.getShiftDate().atTime(6, 0); // 夜班固定22-06 LocalDateTime morningStart shiftDate.atTime(8, 0); long hoursBetween ChronoUnit.HOURS.between(nightEnd, morningStart); if (hoursBetween 16) { return new ConflictResult(false, 距上次夜班结束仅 hoursBetween 小时不足16小时休息期); } } } // 步骤3科室校验原始已有此处复用 Nurse nurse nurseMapper.selectById(nurseId); if (!Objects.equals(nurse.getDepartmentId(), scheduleMapper.selectDepartmentIdByDate(shiftDate))) { return new ConflictResult(false, 护士科室与排班科室不匹配); } return new ConflictResult(true, 无冲突); } private long getShiftDuration(String shiftType) { return switch (shiftType) { case MORNING - 8; case AFTERNOON - 8; case NIGHT - 12; default - 0; }; } }注意selectByNurseAndDateRange等自定义方法需要在ScheduleMapper.xml里补全 SQL。例如!-- ScheduleMapper.xml -- select idselectByNurseAndDateRange resultTypecom.example.hospital.entity.Schedule SELECT * FROM schedule WHERE nurse_id #{nurseId} AND shift_date BETWEEN #{startDate} AND #{endDate} /select5.4 前端增强在排班页加一个“智能推荐”按钮调用新接口预演光有后端校验不够护士长需要知道“如果我想排张护士上明天早班系统会怎么拦我”。我们在static/schedule/nurse-schedule.html的排班表格上方加一个按钮button classbtn btn-info btn-sm onclickcheckConflict() 智能冲突检测/button script function checkConflict() { const nurseId $(#nurseSelect).val(); // 假设有个护士下拉框 const date $(#dateInput).val(); // 日期输入框 const shift $(#shiftSelect).val(); // 班次下拉框 $.get(/api/conflict/detect, { nurseId: nurseId, shiftDate: date, shiftType: shift }, function(res) { if (res.code 200) { layer.msg(✅ res.data.message, {icon: 1}); } else { layer.msg(❌ res.message, {icon: 2}); } }); } /script后端对应加个GET /api/conflict/detect接口复用上面的detect()方法即可。这样排班前就能看到红字警告而不是保存时才被打断。从那以后我每次接到课设类项目都强制走一遍“三查”查pom.xml里的 JDK 和依赖版本是否匹配本地环境查application.properties里的数据库连接是否指向真实实例查sql/下的初始化脚本是否被执行成功——这三步做完90% 的启动失败都能当场定位。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑