简介:这是一份 Java 医院排班系统源码包,基于 Spring Boot、Vue 和 MySQL 技术栈开发,采用 B/S 架构,主要面向医院管理人员和医护工作者,解决排班管理信息化、规范化问题,同样适合 Java 学习者用于毕业设计或项目实战。压缩包大小约 16.57MB,内含完整项目源码、数据库脚本和配套论文,可直接支撑系统部署、功能扩展与二次开发。当前已有 267 人学习下载,具备一定参考热度。系统支持管理员与医护双角色操作:管理员可管理个人中心、医院信息、医护信息、医护类型、排班信息、排班类型、科室信息以及投诉信息,医护人员可维护个人信息、查看个人排班和收藏内容。界面清晰、操作简单,能够有效提升医院排班管理效率,并帮助读者掌握 Spring Boot 与 Vue 前后端分离开发,以及 MySQL 数据库设计的关键思路。
1. 医院排班系统:为什么手工排班最后都会走向代码化
先别急着搜“医院排班系统源码”去下载一个就跑,我见过太多人拿到一套 springboot 排班系统后,第一步就卡在“这表怎么建”“排班规则写在哪”。医院排班系统——不管是门诊排班、护士排班还是手术室排班——本质都是同一个问题:在“科室人力有限”和“每天每个班次都有人”这两个约束里,找一组不冲突的安排,并且让每个人感受到“规则透明,不偏不倚”。手工用 Excel 排班,最痛苦的不是排不出来,而是排完之后的“换班连锁反应”:一个人调班,整周全乱,护士长周末还要对着表格瞪眼。这套 Java 排班系统的价值,就是把排班规则、人员可用性、换班申请这三件事收拢到一套代码里,让数据库来决定谁哪天值什么班。
它适合谁?适合正在做毕业设计的计算机专业学生,适合医院信息科想用 springboot 快速搭一套内部工具的工程师,也适合接私活的外包开发者——排班这类需求在中小企业里出镜率极高。这套系统的落地路径不算复杂:Spring Boot 做后端服务,MySQL 存人员和排班数据,排班算法写在 Service 层,前端可以是 Vue 也可以是 Thymeleaf,论文则重点描述需求分析、E-R 图、算法设计和测试过程。后面几章,我按“数据库设计 → 排班算法 → Spring Boot 接口 → 避坑 → 验证技巧”的顺序把整个方案拆开,每一步都给可复现的代码,你照着能跑通,也能在答辩或交付时说得清楚。
2. 排班系统的领域建模:数据库表怎么设计才能让规则不散落
排班系统最忌讳的是一上来就写“排班表”。如果你只建了一张 schedule 表,把“医生A周二上午出门诊”写成一行,那后面所有需求——请假、换班、按权限看排班——都会逼着你不停改表结构。正确的做法是先做领域建模,把“人员”“班次模板”“排班规则”“实际排班”拆成四个独立概念,再落成表。
2.1 从需求到表结构:人员、科室、班次模板的拆分逻辑
第一张表是科室和人员的基础信息。科室表 department 只需要 id、name、description 三个字段,但人员表 staff 必须多设计几个和排班强相关的字段:工种类型(医生/护士/技师)、是否参与夜班、每周最多班次数、职称。这些字段直接参与算法逻辑,不能放在备注里手写。第二张表是班次模板 shift_template,它定义“医院有哪些班型”:早班 07:30-15:30,晚班 15:30-23:30,夜班 23:30-次日 07:30,还要一个字段标记这是白班还是夜班,因为夜班和休息的算法权重完全不同。第三张表是排班规则表,字段包括生效周、硬性约束和软性偏好。最后才是排班结果表,它只存“某人在某天的某个班次”,一行的 id 由 staff_id + shift_date + shift_template_id 唯一确定。
这里有个容易被忽略的设计决策:排班规则为什么要单独建表,而不是写死在代码里?因为医院排班规则每半年可能变一次——比如夜班从“每人每月 6 次”改成“每人每月 4 次”,如果规则写在常量类里,每次改动都要重新部署;规则入库,运营人员改一条记录,算法下次运行时自动读新值。这个设计在答辩时可以着重讲,评委会觉得你有工程思维。实际建表时我一般会给每张表都加上 create_time 和 update_time 两个审计字段,后续排错查数据时真的能救命,尤其是“上周排班怎么突然变了”这类问题,一查 update_time 就知道是谁在什么时候改过数据。
2.2 可复现的 MySQL DDL:主键、唯一索引和字段注释一个都不能少
直接给可以落库的 DDL,字段和注释我都按医院真实场景写过一遍。你复制到 MySQL 8.0 直接执行,注意把字符集统一成 utf8mb4,不然排班备注里一出现“崔”字或表情符号就可能乱码。
CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '科室ID', name VARCHAR(64) NOT NULL COMMENT '科室名称', description VARCHAR(255) DEFAULT '' COMMENT '科室说明', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科室表'; CREATE TABLE staff ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID', dept_id BIGINT NOT NULL COMMENT '所属科室ID', name VARCHAR(32) NOT NULL COMMENT '姓名', job_type TINYINT NOT NULL COMMENT '1医生 2护士 3技师', title VARCHAR(32) DEFAULT '' COMMENT '职称', night_enabled TINYINT DEFAULT 1 COMMENT '是否参与夜班,1是 0否', max_weekly_shifts INT DEFAULT 6 COMMENT '每周最多班次数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员表'; CREATE TABLE shift_template ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '班次模板ID', shift_name VARCHAR(32) NOT NULL COMMENT '班次名,如早班/晚班/夜班', start_time TIME NOT NULL COMMENT '开始时间', end_time TIME NOT NULL, night_flag TINYINT DEFAULT 0 COMMENT '是否夜班,1是 0否', need_staff_count INT DEFAULT 1 COMMENT '该班次需要人数' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次模板表'; CREATE TABLE schedule_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL COMMENT '规则所属科室', rule_key VARCHAR(32) NOT NULL COMMENT '如night_count_per_month', rule_value VARCHAR(128) NOT NULL COMMENT '规则值,如4', effective_week VARCHAR(16) COMMENT '生效周,格式2025-W01', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='排班规则表'; CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, staff_id BIGINT NOT NULL COMMENT '人员ID', dept_id BIGINT NOT NULL COMMENT '科室ID,冗余加速查询', shift_date DATE NOT NULL COMMENT '排班日期', shift_template_id BIGINT NOT NULL COMMENT '班次模板ID', status TINYINT DEFAULT 1 COMMENT '1正常 2换班待审 3已换班', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_staff_date_template (staff_id, shift_date, shift_template_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='排班结果表';这套 DDL 里最关键的三个设计是:schedule 表加了唯一索引 uk_staff_date_template,防止同一人同一天被排两个相同班次——这是数据层给算法兜底;night_flag 字段单独抽取,因为算法判断“这人今晚值没值夜班”时不能只看班次名,如果科室自定义了“大夜”“小夜”这种名字,代码就写死了;schedule_rule 表用 key-value 结构存规则,虽然查询时要转一下,但换来的是规则“可插拔”。如果你用的是 MyBatis-Plus,想根据实体类自动生成创建表的 SQL 语句,记得在实体类上把 @TableName、@TableId 注解写全,否则生成的 SQL 里主键和表名会对不上,这是我们项目里吃过亏的地方。
2.3 换班申请和排班状态:为什么需要状态机而不是删除重排
医院排班最核心的业务动作是“换班”,而换班不是一个简单的 update 操作。A 和 B 换班,中间经历了“A 发起申请 → B 确认 → 护士长审批”三步,每一步失败都不能让排班数据处于半更新状态。所以 schedule 表的 status 字段专门设计了三个值:1 正常、2 换班待审、3 已换班。换班操作的正确实现方式是:把 A 和 B 的两条 schedule 记录都改成待审状态,事务提交后,审批通过再统一翻转成正常状态。
这里最容易踩的坑是:有人用“删掉 A 的记录,插入 B 的记录”来实现换班。这么做的代价是失去了审计轨迹——一个月后想查“这个班到底怎么变成小张上的”,发现那条记录已经没了。排班系统本质上是一个“排班 + 审批”的双层结构,不能当普通的数据增删改查来做。如果你只想要一个简单能跑的源码,数据库至少要把 schedule 和 schedule_rule 这两张表建好,换班状态可以先不做,但表的字段要预留 status。后面接排班算法时,你会发现这个预留的字段能省很多事。
3. 排班算法的核心逻辑:从常规约束满足到可落地的贪心实现
数据库建好后,整个项目的灵魂就是排班算法。很多毕业设计或者小项目里,排班就是随机分——把人员列表 shuffle 一下,按顺序往班次里填。这只能应付演示,真正能用的排班算法至少要满足三类约束:硬性约束(每天每个班必须有足够人数、同一个人同一天不能排两个班)、政策约束(夜班次数上限、白班和夜班之间必须有足够休息间隔)、软性偏好(尽量保证每人每周休息天数均衡、尽量不连续值夜班)。
3.1 约束条件拆解:硬约束、政策约束、软性偏好如何用代码表达
先把约束写清楚,再写算法,这是排班系统区别于普通 CRUD 工程的关键。硬约束其实有三条:覆盖约束——每个班次的实际人数必须等于该班次 need_staff_count;唯一性约束——同一员工同一时刻只能出现在一个班次;连续性约束——按医院标准,夜班结束后至少休息 24 小时,也就是昨天 23:30 值了夜班的人今天不能安排任何班次。政策约束通常是可配置的,比如“医生每月夜班不超过 4 次”“连续白班不超过 5 天”。软性偏好不强制,比如“尽量让每个人夜班数差值不超过 2”。
代码里表达约束,不能用一堆 if 堆在循环里,那样排班函数超过一百行就没人看得懂了。把每个约束写成一个校验函数,返回 boolean 和一个失败原因字符串,算法主体调用这些函数来决策。这样做的好处是:换一条政策时,你只需要改对应函数,不需要动排班主流程;写论文时把每个函数对应到一张约束表,逻辑特别清晰,答辩老师问什么都能答上。
3.2 一种可落地的排班实现:先按优先级填夜班,再用回溯补位
我一般用的排班算法思路是“按优先级填坑”:夜班要先排,因为它人数需求少而且参与人数有限;然后排周末白班;最后的工作日白班是最灵活的,可以用简单贪心补齐。夜班排法是从夜班人员池里按“本月已排夜班数从小到大”排序,取最少的那个填入空缺班次——这保证公平性,也避免某个人被反复排夜班。如果一个班次没人可填,就触发回溯,判断上一个被填的人能不能和这个位置的人交换。
下面这个代码是我在 springboot 项目里常用的核心排班方法,用 Java 写的,直接放在 Service 层。它接收排班周期和科室 ID,生成该周期内所有排班记录并批量插入 schedule 表。
public List<Schedule> generateSchedule(Long deptId, LocalDate startDate, int days) { // 1. 加载该科室参与排班的人员,夜班启用和非夜班人员分开 List<Staff> staffList = staffMapper.selectByDeptAndNight(deptId, true); List<ShiftTemplate> templates = shiftTemplateMapper.selectAll(); Map<Long, List<Schedule>> result = new HashMap<>(); List<ShiftTemplate> nightShifts = templates.stream() .filter(ShiftTemplate::getNightFlag).toList(); // 2. 先排夜班:遍历每一天,每个夜班缺人时选择本月夜班次数最少的人 for (int i = 0; i < days; i++) { LocalDate date = startDate.plusDays(i); for (ShiftTemplate night : nightShifts) { int need = night.getNeedStaffCount(); List<Schedule> exist = scheduleMapper.findByDateAndTemplate(date, night.getId()); if (exist.size() >= need) continue; List<Staff> candidates = staffList.stream() .filter(s -> canAssign(s, date, night.getId())) .sorted(Comparator.comparingInt(this::getNightCountInMonth)) .toList(); for (int j = 0; j < need - exist.size() && j < candidates.size(); j++) { Schedule s = new Schedule(); s.setStaffId(candidates.get(j).getId()); s.setDeptId(deptId); s.setShiftDate(date); s.setShiftTemplateId(night.getId()); result.computeIfAbsent(date.toEpochDay(), k -> new ArrayList<>()).add(s); } } // 3. 本日夜班排完后,如果人数不足,记录缺口并触发告警 if (needMissing > 0) log.warn("科室 {} 日期 {} 夜班缺口 {}", deptId, date, needMissing); } // 4. 批量保存 scheduleMapper.batchInsert(allSchedules); return allSchedules; }这段代码的核心参数有两个:canAssign校验函数负责硬约束和政策约束,它内部会查这人那天有没有已排班、如果前一天的班次是夜班是否已满 24 小时休息、本月夜班数是否已达上限;getNightCountInMonth是公平性排序函数,保证夜班次数少的人优先被选中。代码逻辑按夜班优先、贪心补位、缺口告警三层设计,批量插入放在最后一步,避免一条条插入造成大量数据库往返。实际跑起来一个 20 人的科室排 28 天班,生成速度可以压到 1 秒以内。如果你已经有现成的排班源码,核心要审查的就是这两个函数有没有写全——很多劣质源码会直接跳过 canAssign 的休息间隔校验,导致夜班连轴转,真实医院里根本不可能接受。
3.3 参数怎么调:夜班上限、休息间隔、每周班次数三组数值的经验值
排班算法里可调参数不是越多越好,真正影响排班质量的是三组。第一组是夜班月上限,一般医生设 4、护士设 6,值越小公平性越好,但可能排不满夜班空缺;如果发现某个班次老是缺人,先把上限调大而不是增加招聘。第二组是夜班后休息小时数,至少设 24,这属于安全线,不能随便调低。第三组是每周最大班次数,建议 6,有些源码默认值是 7,等于默认一周上满——这在真实场景里是违规的。这三个参数我都放在 schedule_rule 表中,而不是写死,方便运营调整。调参时有一个判断技巧:如果生成结果里夜班缺口超过总班次的 5%,优先检查人员池 night_enabled 的人数占比,而不是调算法——很可能 20 人里只有 8 人开了夜班权限,排不出来是数据问题,不是算法问题。
4. Spring Boot 工程搭建与核心接口:分层结构、配置和可复现的增删改查
拿到一套排班系统源码,第一件事不是看算法,而是看工程结构和配置是否完整。很多源码在本地跑不起来,十有八九是配置文件缺东西、包名和 Mapper 扫描路径不一致、或者 Lombok 版本和 JDK 打架。这一章我按一套标准的 springboot + MyBatis-Plus 结构把整个工程骨架拆开,每个关键位置给出代码和说明。
4.1 工程包结构与启动类:按 controller、service、mapper、entity 四层划分
一个合格的 Spring Boot 排班系统,包名结构至少要能看到业务边界。我常用的分包是controller(接收 HTTP 请求)、service(排班算法和业务事务)、mapper(数据库访问)、entity(数据库实体映射)、config(安全、跨域、MyBatis-Plus 配置)、common(统一返回体和异常处理)。排班算法会单独放一个core包,因为它是整个系统最独立的模块,以后可能抽出来给别的科室用,或者换算法重写,不能和业务 Service 纠缠在一起。
启动类上要做两件事:加@SpringBootApplication无非多谈,关键是@MapperScan("com.hospital.schedule.mapper")的路径必须和你的 mapper 包实际路径一字不差,否则 Spring 容器里根本找不到 Mapper Bean,启动直接报错。MyBatis-Plus 的高版本对包扫描路径非常敏感,包名多了个后缀都能让你排查半小时。启动类里我一般还会加一个CommandLineRunner做启动自检——检查数据库连接、检查 shift_template 是否有数据、检查 schedule_rule 是否配置了必要参数,任何一个不满足就在日志里醒目地打出警告。这个小动作在答辩演示时特别好用:评审老师问“如果规则没配怎么办”,你可以当场演示启动告警,比嘴上说“有校验”强得多。
4.2 application.yml 配置清单:数据库、MyBatis-Plus、时区三处最容易出错
配置是整个项目拿来能不能跑的关键,直接贴一份我在实际项目里反复验证过的配置,重点地方都写了注释。Spring Boot 2.7 和 3.x 的配置项略有差异,但下面这份两代版本通用性都很高。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_schedule?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置里最坑的是时区,serverTimezone=Asia/Shanghai必须加,否则你的排班日期会出现 8 小时偏差——今天生成的排班记录,数据库日期显示昨天。这个问题在真实医院环境暴露过:护士看着周五的排班表,数据库里存的是周四,原因就是数据库连接时区默认为 UTC。MyBatis-Plus 的逻辑删除配置也会引发一个隐蔽的坑:如果 schedule 表没有 deleted 字段而配置了逻辑删除,那么所有查询都会自动带上WHERE deleted = 0,导致查出来的排班记录永远为空,而且不报错。如果你的源码里配置了这一项,务必确认每张表都有对应的 deleted 字段。日志配了StdOutImpl后,每个 SQL 都会在控制台输出,排错阶段先开着,上线后记得去掉,否则生产环境日志会迅速膨胀。
4.3 核心接口实现:排班生成、查询人员和换班审批的请求链路
排班系统的接口不在多,而在三条链路是否通:生成排班、查询排班、换班审批。生成排班接口的请求体一般是{ deptId: 1, startDate: "2025-06-01", days: 28 },Service 层调用上一章的generateSchedule,然后把返回的排班记录逐条写入 schedule 表。查询排班接口要支持按科室和日期范围查询,方便前端按周视图展示。换班审批链路最复杂,需要两个接口:一个是提交换班申请,一个是审批通过,下面给出换班申请的典型实现。
@Transactional public boolean submitSwap(Long scheduleAId, Long scheduleBId) { Schedule a = scheduleMapper.selectById(scheduleAId); Schedule b = scheduleMapper.selectById(scheduleBId); // 校验两张排班记录属于同一天且班别不同,否则换班无意义 if (!a.getShiftDate().equals(b.getShiftDate())) { throw new BusinessException("换班必须发生在同一天"); } if (a.getStatus() != 1 || b.getStatus() != 1) { throw new BusinessException("只有正常状态的班次可以申请换班"); } // 查出两个员工的工号和姓名,写入日志留痕 Staff sa = staffMapper.selectById(a.getStaffId()); Staff sb = staffMapper.selectById(b.getStaffId()); log.info("换班申请:{} 与 {} 于 {} 的班次互换", sa.getName(), sb.getName(), a.getShiftDate()); // 用乐观锁更新状态,防止并发环境下两人同时提交导致状态覆盖 int countA = scheduleMapper.compareAndSetStatus(scheduleAId, 1, 2, a.getVersion()); int countB = scheduleMapper.compareAndSetStatus(scheduleBId, 1, 2, b.getVersion()); if (countA != 1 || countB != 1) { throw new BusinessException("排班状态已变化,请刷新后重试"); } return true; }这段代码最重要的设计是把“校验”和“更新”分成两步,并且更新时用带 version 的乐观锁语句,而不是直接 update。换班场景是典型的并发冲突场景——两个护士同时看到同一个班次,都想和对方换,如果不用乐观锁,后提交的人会覆盖先提交的人的状态。@Transactional保证两次更新要么都成功,要么都失败,不会出现只有一条记录变成“待审”的中间态。你拿到任何排班源码,重点看换班接口有没有这三样:事务注解、乐观锁、状态校验。缺任何一个,演示时可能没问题,真实用起来一定出乱子。审批接口的写法类似,只是把状态从 2 改成 3,同时要把两个人的 staff_id 对调写入班次记录,这一步同样要在一个事务里完成。
5. 排班系统避坑指南:5 个让排班系统“当场翻车”的隐蔽问题
排班系统看起来简单,实际跑起来有一堆黑匣子。这一章我把我做过和见过的问题按“现象 → 原因 → 解决”的方式列出来,每条都是花了时间排查换来的血泪经验。如果你在部署或二次开发这套源码时碰到类似症状,先对照这里排查。
5.1 排班日期无故偏移一天:时区配置不一致,数据库存的日期和页面显示对不上
现象:生成的排班表在页面上显示周五,但打开数据库看 shift_date 却是周四。原因:数据库连接串没带 serverTimezone,MySQL 驱动用了默认的 UTC 时区,而 JVM 用的是 Asia/Shanghai,两者有 8 小时差。排班当天 0 点到 8 点之间生成的记录就会落到“昨天”。解决:在 JDBC 连接串显式加上serverTimezone=Asia/Shanghai,同时把服务器的spring.jackson.time-zone也设成同一时区,双保险。如果改完还偏移,检查是不是用了 Docker 而容器时区不是 Asia/Shanghai,进入容器执行date看一眼,不对就在启动命令里加-e TZ=Asia/Shanghai。
5.2 单机运行正常,多人同时排班时数据错乱:缺少事务和乐观锁
现象:两个护士长同时打开排班页面,A 提交了换班,B 也提交了同一条记录的换班,最后数据库里状态变成了 B 的申请结果,A 的操作凭空消失。原因:代码里直接用了update ... set status = ? where id = ?,后提交的人覆盖了先提交的人。解决:给 schedule 表加 version 字段,更新时带上where version = ?,更新成功则 version + 1;MyBatis-Plus 里可以用@Version注解配合乐观锁插件实现,也可以像上一章的compareAndSetStatus一样手写 SQL。如果不想加字段,至少要在 update 语句里带上原状态条件,where status = 1,这样只有状态仍为正常的记录才能被更新。
5.3 夜班人员总是只有那几个人:排班算法按顺序选人,没有做公平性处理
现象:生成的排班表里,张三每个星期值两次夜班,其他人一次都没有,护士长质疑“这系统不对”。原因:排班代码里选人用的是order by id或者随机 shuffle,没按本月夜班次数排序——排班算法缺少公平性约束。解决:选人 SQL 或 Java 排序逻辑按night_count_in_month升序排,夜班次数最少的人优先被选中;同时加一个“如果某人本月夜班次数已经等于规则表里的上限,直接过滤掉”。代码改起来很小,但效果立竿见影。这个坑在几乎所有自己写的排班源码里都存在,评审时也容易被老师揪出来,建议拿到源码后先检查选人逻辑里有没有类似ORDER BY RAND()的写法,有就要改掉。
5.4 Spring Boot 版本太高,老配置全部失效:3.x 和 2.x 的兼容性差异
现象:源码用的是旧版 springboot,你本地 JDK 是 17,直接把 springboot 升级到 3.x,启动报一堆错。原因:Spring Boot 3.x 基于 Jakarta EE,javax包全部换成jakarta;同时很多自动配置类的路径变了,比如spring.datasource部分配置不再生效,需要用spring.datasource.druid前缀或引入专门的 starter。解决:如果只是跑这套排班源码,优先保持原源码的 Spring Boot 版本,别为了“用新版”而升级;如果必须升 3.x,就同时换掉所有javax.servlet、javax.validation等 import 语句,还要确认 MyBatis-Plus 用了支持 Spring Boot 3 的mybatis-plus-spring-boot3-starter,版本号以官方支持的为准。顺带说一个容易忽略的问题:Spring Boot 版本太高时,spring.banner.image在线生成的自定义 banner 也不一定兼容,虽然不影响业务,但启动输出乱码会让人误以为项目有问题。
5.5 排班结束后想要“后悔药”:数据被覆盖而没有快照,导致没法回滚
现象:某次排班调参后重新生成,之前排好的班次全被覆盖了,想恢复成上周的版本,数据库里却没有备份。原因:生成排班的代码先delete from schedule where dept_id = ? and shift_date between ? and ?,再插入新数据,而且删除是在事务里提交的。解决:生成新排班前,先执行一个“归档动作”,把旧排班复制进schedule_history表,保留生成时间和当时使用的规则参数;这样即使新排班排砸了,也能从历史表恢复。这个表的结构和 schedule 一样,多三个字段:archive_time、create_by、rule_snapshot(存当时规则表的 JSON 快照)。我建议把归档写成独立的 Service 方法,并在生成排班接口里强制先调用,没有归档就拒绝生成。这一步虽然简单,但在真实医院场景里是“保命”的设计,护士长最怕的就是“昨天的排班表又不见了”。
6. 排班结果交付的最后一步:用测试代码自动验证硬约束,别用眼睛盯
排班算法写完、接口调通,不等于系统就合格了。真实医院对排班结果的审查非常严格——万一某天夜班缺人,责任很大。我的习惯是交付前写一套自动化约束验证,把“人眼检查”变成“机器断言”。这一章给你一个可以抄的测试思路和代码,跑通后你就知道自己这套源码到底能不能交付。
核心思路是:排班结果生成后,写一个验证器,遍历结果集,逐条检查第 3 章定义的硬约束。验证器不需要引入测试框架,直接在 Service 里加一个validateSchedule(List<Schedule> schedules)方法,返回违规记录列表。但如果你的源码是用 Maven 管理的,我还是建议写成 JUnit 测试,因为每次排班参数调整后都能一键回归。下面是一个简化版验证器,我通常把它放在core包的validator子包里,逻辑直接可复制。
public List<String> validate(List<Schedule> schedules) { List<String> violations = new ArrayList<>(); Map<Long, List<Schedule>> byDate = schedules.stream() .collect(Collectors.groupingBy(s -> s.getShiftDate().toEpochDay())); // 校验1:同一人同一天不能有两个班次 Map<Long, List<Schedule>> byStaffAndDate = new HashMap<>(); for (Schedule s : schedules) { String key = s.getStaffId() + "_" + s.getShiftDate(); byStaffAndDate.computeIfAbsent(key, k -> new ArrayList<>()).add(s); } byStaffAndDate.forEach((key, list) -> { if (list.size() > 1) violations.add("人员 " + key + " 同日存在多个班次"); }); // 校验2:夜班后24小时休息检查 List<Schedule> sorted = schedules.stream() .sorted(Comparator.comparing(Schedule::getShiftDate)) .toList(); for (int i = 1; i < sorted.size(); i++) { Schedule prev = sorted.get(i - 1); Schedule cur = sorted.get(i); if (!prev.getStaffId().equals(cur.getStaffId())) continue; if (isNightShift(prev) && ChronoUnit.HOURS.between(prev.getEndTime(), cur.getStartTime()) < 24) { violations.add("人员 " + cur.getStaffId() + " 夜班后休息不足24小时"); } } // 校验3:所有夜班是否人数达标 Map<LocalDate, Long> nightCount = schedules.stream() .filter(s -> isNightShift(s)) .collect(Collectors.groupingBy(Schedule::getShiftDate, Collectors.counting())); nightCount.forEach((date, cnt) -> { if (cnt < nightShiftNeedCount) violations.add("日期 " + date + " 夜班人数不足"); }); return violations; }这段验证代码里我把最容易出问题的三条硬约束提取出来:同日多班、夜班休息不足、夜班缺人。isNightShift判断方法很简单,就是查该记录的 shift_template_id 对应的模板里 night_flag 是否为 1。实际项目里我会再补一条“夜班月次数超限”的校验,这次没有写出来是因为它依赖 schedule_rule 表的值,需要额外传参。你在跑通源码后,把验证器接在一个跑批入口上:每次生成排班,如果 violations 非空,就拒绝保存并列出违规明细。这是非常实用的一道安全闸门,能挡住大量肉眼看不出的数据问题。
最后说说我自己的习惯:交付这套排班源码时,我不光交付能跑的代码,还会顺手把这个验证器的输出格式做成一张简单的报表——按天列出夜班人员名单、休息人数、违规数量。这样护士长能看到“这个排班方案为什么合理”,而不是只看到一堆数据。排班系统真正的价值不在代码写得多漂亮,而在使用者能不能信任它。信任是拿数据说话建立的,不是“这是系统算出来的,不会错”这种话。希望这套方案和验证思路能帮到你,哪怕只让你躲开一两个我踩过的坑,也算值得。
本文还有配套的精品资源,点击获取