做医护排班系统之前,我先接了一个真实的吐槽:护理部的老师每个月用 Excel 排班,光是把几十号护士的班次排完就要两三天,结果表刚发下去,第二天就收到三四个人的换班申请。每次换班,她都得手动改表格、重新查重、再发一版。有次改到一半忘了同步,导致两个护士同一天被排进同一个班次,差点出了医疗事故级别的乌龙。
也就是从那一刻我意识到,医疗排班绝不是一个“填格子”的功能,而是一套不断在变动、需要强约束校验、必须留痕可追溯的生产系统。所以当我要做一套企业级医护人员排班系统时,我直接选了 SpringBoot + Vue + MyBatis + MySQL 这套组合。这篇文章我就把整个系统的业务建模、排班引擎、审批闭环、权限设计和落地踩坑完整拆一遍,给正在做同类系统、或者想从源码角度学习这套架构的开发者一个参考。
1. 医疗排班为什么不能靠 Excel 硬扛——从需求到系统的关键差异
1.1 Excel 排班的三大死穴
很多人不理解,排班这种事简单得很,一个星期七天、一天三个班次,表格画一画不就完了?真做进去就会发现问题完全不同。
Excel 排班最大的问题是冲突检测靠人眼。护士长排完一百多个格子,全靠肉眼扫有没有人一天出现了两次、有没有昨天夜班今天白班这种连轴转。短期二三十人还能勉强看,人一多、班型一杂,肉眼扫不出来的概率就会指数级上升。
第二个问题是变更管理完全失控。排班表不是发下去就结束了,今天有人请假、明天有人调班、后天有人临时补位。Excel 表一旦经过多人编辑,根本说不清哪个版本是当前生效版本,更说不清每一次改动是谁改的、为什么改。真出了纠纷,连审计记录都拿不出来。
第三个问题是统计报表太耗时。月末要算每个人的夜班次数、工时、补贴,护士长还得对着 Excel 表重新数一遍,还要手工汇总到另一张表上。每次月底都是地狱周。
1.2 医疗排班的业务复杂度在哪里
医院的排班制度比普通企业员工排班复杂得多。这套系统面向的主要场景是护理排班、医生排班,常见的班型包括白班、小夜、大夜、休息、总值班、急诊班、门诊班等。不同科室的班次时段还不一样,有的科室是三班倒,有的科室要覆盖 24 小时每隔八小时换班,还有的科室存在行政班和临床班两套体系。
业务规则也很多。比如:
- 科室每个班次必须有最低在岗人数,不能出现某个时段只有一个人顶着;
- 具备特定资质(如急救资质、ICU 资质)的医护才能排到对应的特殊班次;
- 连续夜班不能超过两个,夜班后必须有足够的休息时间;
- 休假期间不能被排班;
- 同一人在同一天不能重复排班;
- 工作量要在科室内部尽量均衡,不能让固定几个人总在顶夜班。
这些规则单独拎出来都不复杂,但是组合在一起再加上人的变数,整个问题就变成了一个带约束条件的调度问题。Excel 根本承载不了这种复杂度,最终只能落在带校验逻辑的信息系统里。
一个具体的场景是这样的:某科室有 28 个护士,每天需要 6 个白班、4 个小夜、2 个大夜,加上周末和节假日的排班规则差异,再叠加几个护士的产假、年假、进修计划。你要在满足所有这些条件的基础上,让每个人的夜班次数相对均衡,同时尽量少地打扰护士长的二次调整。这个问题的复杂度,手工排班和基础工具已经完全处理不了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型真相:SpringBoot + Vue + MyBatis + MySQL 不是最潮,但是最稳
2.1 前端、后端、数据库各选型的理由
先说明一点,我不是在无脑吹“SSM/Y”这套组合。做排班系统这类企业级管理软件,选技术栈首先要考虑的不是社区热度,而是能不能稳定落地、出了问题好不好排查、团队好不好招人。
后端选 SpringBoot 的理由很简单:生态成熟、约定大于配置、起步快。排班系统的核心服务无非是 REST API、事务管理、权限认证、定时任务、审批流程。这些能力 SpringBoot 都能用很标准的方式搞定。像我们的自动排班引擎,运行时间可能要好几分钟,就必须用异步任务或消息队列来做,Spring 的 @Async 和 Spring Task 直接就能接住。权限部分用 Spring Security + JWT 做无状态认证,也很顺手。
前端选 Vue 的原因在于排班界面是高交互、强联动的场景。排班页面通常是一个大表格,横轴是日期,纵轴是医护人员,每一格是一个下拉选择或弹窗组件。左手边是人员列表,右边是日历,顶部是周切换,还要支持拖拽、批量设置、颜色标记班次类型。Vue 的响应式系统对这种矩阵式页面非常友好,组件化也方便把排班日历、人员选择器、换班申请弹窗拆成独立模块。Vue Router 管理多页面、Vuex/Pinia 管理全局的登录态和科室上下文,这一套都很成熟。
MyBatis 在这个项目里是故意选的。排班系统的查询条件特别灵活:按月份查、按科室查、按人员查、按班次类型查、按变更记录查,各种条件组合在一起,SQL 动态变化非常频繁。MyBatis 的 XML Mapper 对复杂 SQL 和动态 SQL 的掌控力很强,比 JPA 那种自动生成 SQL 的方式更可控。排班报表里经常出现多表联查、行转列、子查询,MyBatis 写起来思路最直接。
MySQL 在这套系统里承担了所有核心业务数据存储。选它不是因为“免费”,而是因为排班系统的读写模型是典型的 OLTP 场景:单条记录小、事务性强、并发量有限、数据量主要在历史积累。这类场景 MySQL 配合 InnoDB 事务和行级锁已经绰绰有余,运维成本也比商业数据库低很多。
2.2 这套架构在扩展性和维护性上的真实边界
很多人会问,这套架构算不算“企业级”?我的理解是,企业级讲的不是技术有多新,而是分层清晰、可维护、可扩展。SpringBoot 提供了标准的 Controller-Service-Mapper 分层,Vue 项目按 components / views / api / store 拆分,整个代码结构是很容易让新成员上手的。
这套组合也有它的边界。排班系统的并发量通常不高,但历史数据会越积越多。如果医院规模大(比如几十个院区、上万名医护),单库单表的 MySQL 会出现性能瓶颈,调度任务的执行也会变慢。这时候不用急着把系统拆成分布式微服务,最省事的做法是:
- 把历史排班数据按月分表或归档到独立库;
- 把自动排班引擎做成独立的异步服务,通过消息队列解耦;
- 引入 Redis 缓存科室配置、班次定义等低频变动数据;
- 读写分离,报表查询走从库。
这套演进路径在 SpringBoot + MySQL 的架构里是天然的,不需要推翻重来。真正到几十万人的规模,再考虑引入更重的调度引擎和分布式任务框架也不迟。
3. 领域模型与表结构:把“排班”这件事拆成一组清晰的核心表
3.1 排班领域核心实体梳理
做排班系统前,我是先把业务对象一个个落到纸上的。整个排班领域的核心实体就这几类:机构科室、医护人员、班次定义、排班计划、排班明细、休假申请、换班申请、审批记录。
它们之间的关系是:科室下面有多名医护,医护具备多项资质;班次定义描述了班次的起止时间、工时、是否算夜班;排班计划是某科室某月份的一次排班任务,排班明细是计划里的每一个“人 + 日期 + 班次”网格;换班申请和休假申请是排班发布后的变更入口,最终都会回写排班明细。
这个模型最核心的设计决策是把排班计划(header)和排班明细(detail)拆成两张表。一开始我试着只做一张 schedule 表,每次调整就改那一行数据。后来发现根本不行:护士长需要整体发布一版月度排班,所有人看到的是同一个版本的完整结果,发布后如果小范围调整,也不能把别人看到的版本搞乱。拆成 plan + detail 之后,一个方案就是一个版本,回滚和版本对比都很好做。而且单独记录 schedule_plan 的 status(草稿、已发布、已归档)和 version,就可以支撑“每月多版本发布”的场景。
3.2 关键表结构和设计决策
我直接把几张核心表的字段设计和设计理由列出来。
科室表 department
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(64) | 科室名称 |
| parent_id | bigint | 上级科室,支持多级树 |
| type | tinyint | 科室类型:病区/门诊/行政 |
| status | tinyint | 是否启用 |
科室做树形结构是为了支持集团医院或大医院的多级管理。排班管理员可能负责整个内科部,而普通护士长只负责自己病区,所以数据权限过滤时需要根据这一棵树去扩散。
医护人员表 staff
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| work_no | varchar(32) | 工号,唯一 |
| name | varchar(32) | 姓名 |
| dept_id | bigint | 所属科室 |
| job_title | varchar(32) | 职称:主管护师/护师/护士 |
| hire_date | date | 入职日期 |
| status | tinyint | 在职/离职 |
这里要特别说明医护人员和用户(sys_user)是两个概念。staff 是业务对象,描述这个人的职称、科室、入离职状态;sys_user 是登录账号,关联角色和权限。两者通过 staff_id 关联。这样设计的好处是:系统账号可以换人登录,不改变业务数据;离职人员只需要禁用账号,排班历史记录仍然保留。
班次定义表 shift_definition
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| code | varchar(32) | 班次编码 |
| name | varchar(32) | 班次名称 |
| start_time | varchar(8) | 开始时间 |
| end_time | varchar(8) | 结束时间 |
| work_hours | decimal(4,1) | 标准工时 |
| night_flag | tinyint | 是否归属夜班 |
| color | varchar(16) | 前端显示颜色 |
| min_staff | int | 该班次最低在岗人数 |
班次定义不写死枚举,而是做成表配置,这是为了适配不同科室的差异。有的科室白班是 8:00-16:00,有的科室是 8:00-12:00 + 14:00-17:30 的行政班,如果枚举写死在代码里,换一个科室就要发版一次。参数化之后,科室管理员可以自己维护班次库,系统才有“通用化”的底气。
排班明细表 schedule_detail
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| plan_id | bigint | 排班计划 ID |
| staff_id | bigint | 医护人员 ID |
| work_date | date | 排班日期 |
| shift_id | bigint | 班次 ID |
| version | int | 乐观锁版本号 |
| create_by / create_time / update_by / update_time | - | 审计字段 |
schedule_detail 是整个系统的核心数据表,几乎所有的重逻辑都围绕它展开。work_date 和 shift_id 之间需要建立联合索引,因为最频繁的查询是“某科室某天有哪些人在岗”“某人在某月有哪些班次”。version 字段用于并发更新控制,后面讲换班审批时会重点说明。
换班申请表 swap_request
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| plan_id | bigint | 排班计划 ID |
| from_staff_id | bigint | 申请人 |
| from_date | date | 原班次日期 |
| from_shift_id | bigint | 原班次 |
| to_staff_id | bigint | 被申请人 |
| to_date | date | 目标班次日期 |
| to_shift_id | bigint | 目标班次 |
| status | tinyint | 待审批/通过/驳回 |
| audit_user / audit_time | - | 审批信息 |
换班申请比较关键的字段是同时保存了“原班次”和“目标班次”两个信息。审批通过后,只需要在一个事务里更新四条记录:把 from 的班次改成 to,把 to 的班次改成 from。如果只记录“申请人想换班”而不记录目标班次,那审批通过后系统根本不知道该把数据改成什么。这是一开始建模容易忽略的坑。
3.3 索引设计与数据量估算
表结构设计完之后,不能等数据量上来了再考虑索引。排班系统月粒度下,一个 50 人的科室排班明细大约是 50 人 × 30 天 = 1500 条,一个月生成一个 plan 也就是 1500 条 detail。如果医院有 50 个病区,一个月新增约 75000 条明细,一年接近 90 万条。这个量级 MySQL 完全扛得住,但必须建好索引。
我实际建过的主要索引有这么几个:
sql复制ALTER TABLE schedule_detail ADD INDEX idx_plan_staff (plan_id, staff_id);
ALTER TABLE schedule_detail ADD INDEX idx_plan_date (plan_id, work_date);
ALTER TABLE schedule_detail ADD INDEX idx_date_shift (work_date, shift_id);
ALTER TABLE swap_request ADD INDEX idx_from_staff (from_staff_id, status);
核心查询基本都被覆盖了。唯一要留意的坑是不要在排班明细这种高频变更的表上加太多联合索引,因为每次换班审核、排班调整都会触发多条索引更新,索引太多会拖慢写入速度。
4. 自动排班引擎:硬约束、软约束和公平性轮转的落地写法
4.1 排班规则不是 if-else,而是约束集合
我最早做自动排班时,代码里全是 if 判断:if 这一天已经排了班、if 这个人正在休假、if 这个班次不匹配资质……写了几百行以后发现完全维护不了,加一条规则就要改一大片逻辑。
后来我才改成约束驱动的思路。把所有排班规则统一抽象成两类:
硬约束:一旦违反就必须重排,没有商量余地。比如:
- 同一人同一天只能有一个班次;
- 排班人员必须具备该班次要求的资质;
- 休假申请通过后对应日期不能排班;
- 每个班次在岗人数不低于最低配置;
- 连续在岗时长不能超过安全上限。
软约束:允许违反,但会降低排班质量,应该在条件允许时尽量满足。比如:
- 每人每月夜班次数尽量均衡;
- 相邻两次班次之间休息时间尽量足够;
- 连续排班天数不要过长;
- 尽量平衡工作日和节假日的工作负担。
系统在自动排班时,硬约束是过滤器,软约束是排序器。先用硬约束筛掉不合法的排班选择,再用软约束对合法选择打分排序,选出当前最优的一个。这比写一大堆 if 要清晰得多,而且规则越加越多时,每一条规则都可以独立维护。
4.2 逐日生成的引擎主流程
自动排班引擎的主流程并不复杂,核心思路是按天推进、逐格填充。我用伪代码把它大致写一下:
java复制public void autoSchedule(SchedulePlan plan, List<ShiftDefinition> shifts, List<Staff> staffs) {
// 1. 装载所有硬约束和软约束
RuleEngine<HardRule> hardRules = loadHardRules();
ScoreCalculator<SoftRule> softRules = loadSoftRules();
// 2. 逐日处理
for (LocalDate date = plan.getStart(); !date.isAfter(plan.getEnd()); date = date.plusDays(1)) {
Map<Long, Integer> staffLoad = loadCurrentLoad(plan, date);
// 遍历当天所有班次配置
for (ShiftDefinition shift : shifts) {
int currentCount = getAssignedCount(plan.getId(), date, shift.getId());
int needCount = shift.getMinStaff() + getExtraNeed(date, shift);
while (currentCount < needCount) {
Staff picked = pickBestCandidate(staffs, plan, date, shift, staffLoad, hardRules, softRules);
if (picked == null) {
// 找不到候选,记录冲突,交给人工调整
conflictCollector.add(plan.getId(), date, shift);
break;
}
createScheduleDetail(plan.getId(), picked.getId(), date, shift.getId());
currentCount++;
}
}
}
}
这里最关键的 pickBestCandidate 方法是候选人的选择逻辑。第一步用硬约束过滤掉所有不可用的人,第二步对剩余的人用软约束打分。比如当前硬约束过滤掉正在休假的、当天已排班的、资质不匹配的、班次时段和前一个班次休息时间不足的,剩下的候选人里,选择夜班积分最少、连续工作时长最短的人。
4.3 公平性轮转:夜班积分怎么写
很多排班系统做出来被护士长嫌弃不好用,最大的问题不是约束没满足,而是分配不公平。如果只是机械地按顺序填格子,大概率会出现固定几个人一直被排夜班的现象。为了把公平性做进去,我给每个医护人员加了一个“夜班积分”的概念。
具体做法很简单:每个人有一个月度累计的夜班权重,每次被排进一个夜班,权重就增加。候选人的排序依据之一就是这个权重,权重越小越优先被排进夜班。这样排到月底,全科人员的夜班次数会趋向于均衡。
java复制private int getNightWeight(Staff staff, LocalDate month) {
return scheduleDetailService.countNightShifts(staff.getId(), month);
}
这只是最基础的做法。进阶一点,可以引入“期望值”的概念:根据科室总夜班任务量除以可排班人数,算出每个人的理论夜班数,然后比较当前实际值跟期望值的差距,差距越大越优先排班。像“月末数据核对”这个环节,从实现层面拉一张“预期夜班量 vs 实际夜班量”的报表,护士长一眼就能看出排班是否偏向谁,很多矛盾在源头就能避免。
5. 换班审批闭环:状态机、并发锁和消息通知怎么串起来
5.1 换班业务链路与状态机
排班发布之后,最活跃的功能其实是换班申请。一个几百人的病区,每个月换班申请少说也有几十条。换班流程的闭环如果做得不好,系统价值就直接减半。
我设计的换班业务链路是这样的:
- 申请人选择自己某一天的班次,再从排班表里选择想交换的同事和班次;
- 系统先做一次预校验,校验双方新班次是否可行;
- 预校验通过后生成待审批申请,推送给指定审批人(通常是护士长);
- 审批人同意后,事务里同时交换两个人的班次;
- 审批人驳回则流程直接结束,排班数据不受影响。
整条链的状态流转非常清晰:待审批 -> 通过/驳回。通过后如果想把修改回退,需要再走一次反向换班申请,不能直接改历史记录。这个设计是为了保证每一条排班变更都有审计留痕。
5.2 审批并发与数据一致性
换班审批最容易被忽略的问题是并发冲突。设想一个场景:护士 A 想和护士 B 换班,同时护士 B 又发起了另一条换班申请想和护士 C 换。如果两个审批几乎同时通过,数据库里可能产生互相覆盖的结果,导致 A、B、C 三人的排班数据乱掉。
处理这个问题的标准手段有两个。第一个是在查询有效班次时对相关记录加行锁:
java复制List<ScheduleDetail> details = scheduleDetailMapper.selectForUpdate(
planId, staffIds, workDate
);
第二个是为 schedule_detail 增加乐观锁版本号。执行更新时带上 version,如果更新行数为 0 说明数据已被其他人改过,此时提示“排班数据已变更,请刷新后重试”,整体回滚事务。这两者组合起来,基本能杜绝并发导致的数据错乱。
从业务上还有一个间接手段:同一个排班计划里,一个人同一时间只能存在一条“待审批”状态的换班申请。这个可以在数据库层面通过唯一索引或应用层幂等校验来实现,有效降低并发冲突概率。
5.3 审批通过后的通知机制
审批通过后,系统不能静默完成,必须通知申请人、被申请人和相关同事。通知渠道可以做成接口,同时支持站内信、企业微信、钉钉这类办公平台推送。我这里是做一个统一的 message provider 接口,不同渠道各自实现,审批事件完成后发一条领域事件出去。具体到代码层面,可以用 Spring 的 ApplicationEventPublisher 来实现:
java复制applicationEventPublisher.publishEvent(new SwapApprovedEvent(swapRequest));
这样做的好处是流程主逻辑不关心通知细节,后面接入短信、邮件都不用改主流程。
6. 排班合规校验:把“不能这么排”变成一套可配置的规则引擎
6.1 哪些校验规则必须做成硬约束
排班系统如果只能自动生成,不能处理人工微调,那效率再高也不会被接受。护士长总会因为临时情况去手动改某个格子。问题在于,手动改完之后,系统必须有能力立刻告诉他“这个改法不合法”。
我把校验规则做成了一个独立的校验模块,这个模块既给排班页面保存前调用,也给换班审批前调用,还提供给批量导入 Excel 后做校验。常用的硬校验项我整理成了下面这张表:
| 校验项 | 校验内容 | 违反后的处理 |
|---|---|---|
| 同日多班 | 同一人同一天不能被排两次班 | 阻止保存 |
| 资质匹配 | 人员是否具备当前班次需要的资质 | 阻止保存 |
| 休假冲突 | 休假期间不能存在排班记录 | 阻止保存 |
| 在岗人数 | 每班次实际人数不低于最低配置 | 警告但允许保存 |
| 连续夜班数 | 连续夜班超过配置上限 | 警告但允许保存 |
| 休息间隔 | 两班次之间休息时间不足 | 警告但允许保存 |
这里有一点需要分清:阻止保存的是硬冲突,警告的属于软违规。如果所有规则都直接阻止保存,护士长在月底临时补人时会特别痛苦,因为排班问题往往是“没有完美解,只有最不坏的解”。所以硬冲突直接拦,软违规给警告但保留人工决策权,这个度把握好了,系统才真正好用。
6.2 规则引擎如何“接住”新规则
规则引擎的设计不复杂,关键在于不要写死。我是这样组织的:定义一个统一的规则接口,每条规则是一个独立实现类;规则类的启用状态和参数可以通过配置表维护。
java复制public interface ScheduleRule {
/** 规则编码 */
String getRuleCode();
/** 校验,返回冲突描述 */
RuleValidateResult validate(ScheduleValidateContext context);
}
比如“连续夜班数不能超过 2”这条规则,它读取 context 里的人员近几天排班记录,统计连续夜班数,跟配置的最大值对比,超过就返回警告结果。配置表里存着 rule_code = NIGHT_LIMIT,min_value = 0,max_value = 2,level = WARN。以后医院说“我们这边规定连续夜班不能超过 3”,管理员在后台改一下配置就行,不用动代码。
这里我踩过一个坑:如果规则实现类太多,Spring 注入的时候顺序会是乱的,可能导致校验顺序不稳定。解决办法是给每条规则加一个 order 字段,注入后用 Comparator.comparing(ScheduleRule::getOrder) 排个序,保证校验结果输出稳定。这个小细节很实用,排错时会省很多时间。
7. 权限治理、统计报表与部署落地:企业级系统最后三块拼图
7.1 多级角色与数据权限隔离
排班系统面向的用户角色比普通管理系统要复杂。我实际划分出的角色至少有五层:
- 超级管理员:管理全院科室、用户、基础配置;
- 院级排班管理员:可以查看和调整所有科室的排班,但通常不直接改一线数据;
- 科室护士长/科主任:管理自己科室的排班,审批换班、休假;
- 普通医护:查看自己的排班、提交换班和休假申请;
- 审计员:只读权限,查看排班变更日志。
菜单权限用 RBAC 模型实现,前端路由根据角色动态生成。但真正容易出问题的是数据权限。比如内科部的管理员,能看到内科下所有病区的排班;而某个病区的护士长,只能看自己病区的排班。这个隔离如果只靠前端隐藏按钮是不够的,后端所有查询接口都必须带上科室范围过滤。
我的做法是后端维护一个当前登录人的 dept_scope 列表,查询时强制拼接 dept_id in (…) 条件。这个条件不能由前端传参决定,而是由后端根据当前用户的角色和数据权限实时计算,防止有人通过修改请求参数越权访问其他科室数据。
7.2 排班统计报表和考勤联动
排班数据的价值不只是“安排谁上班”,更重要的是为后续的考勤、薪酬、绩效提供数据基础。这套系统里我实现了几个核心报表:
- 月度工时统计:按人员汇总当月各类班次工时,支持导出 Excel;
- 夜班统计:按人员统计夜班次数,直接给补贴发放提供依据;
- 出勤与缺勤统计:结合休假和排班数据,算出实际出勤天数;
- 排班稳定性报表:统计一个排班计划发布后经过多少次调整、谁调整的,用于衡量排班质量。
报表模块的技术选型有个现实考量:如果用 POI 直接导出大批量 Excel,医院科室多的时候内存很容易撑爆。我实际的做法是导出任务改造成异步任务,数据量大的时候返回一个任务 ID,前端轮询任务状态,任务完成后生成下载链接。这个机制在十来个科室同时导月度报表时没有出过问题。
7.3 部署配置与最容易踩的四个坑
最后聊一下部署落地。我这套系统前端打包后丢 Nginx,后端打成 jar 跑在一台 4C8G 的云服务器上,MySQL 用的 8.0,连接池用的 HikariCP。正式环境配置跟开发区分开,用 Spring Boot 的多 profile 方式管理:
yaml复制# application-prod.yml
spring:
datasource:
url: jdbc:mysql://your-mysql-host:3306/nurse_schedule?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: prod_user
password: ${MYSQL_PASSWORD}
hikari:
maximum-pool-size: 20
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
部署过程本身不复杂,但有几个坑值得专门说。
第一个坑:SpringBoot 版本和 MyBatis 插件不兼容。 SpringBoot 3.x 之后要求 JDK 17,很多老的 MyBatis 生成器插件、代码生成插件在升级后直接不工作。我踩过之后就把版本锁在了 SpringBoot 2.7.x + JDK 8 的组合上,团队里所有人开发环境统一,省掉一堆“我这边跑得好好的啊”式的沟通成本。
第二个坑:MyBatis 的 #{} 和 ${} 用混。 排班报表里经常要根据前端传入的字段进行排序和分组,这地方最容易把 ${} 直接拼进 SQL。${} 是字符串替换,存在 SQL 注入风险;#{} 是预编译占位符,安全但不能用在表名、列名、排序字段上。我的做法是:所有排序字段走一个白名单映射,前端传 sort=work_date,后端映射成 work_date 再拼接,绝不直接信任前端参数。白名单之外的字段一律走默认排序。
第三个坑:MyBatis 一级缓存导致数据“不新鲜”。 默认情况下,MyBatis 的一级缓存是 SqlSession 级别的,同一个 SqlSession 内连续两次相同查询会命中缓存。但在有定时任务、消息通知回写、多线程处理排班明细的场景下,一级缓存有时候会让人困惑:代码明明刚改了一条数据,后面再查却还是旧值。开发环境我通常直接关掉一级缓存,避免踩坑:
yaml复制mybatis:
configuration:
local-cache-scope: statement
第四个坑:MySQL 深度分页慢。 排班明细表在按人员查历史记录时,如果用户翻页到几百页之后,LIMIT 100000, 20 这种写法会把前面十万条数据都扫描一遍,非常慢。我的处理是避免深度分页,改成“按时间范围游标”的方式下拉加载,或者用 WHERE id < ? 的方式往前翻。排班系统的历史数据本来就是按月看的,很少真的需要暴力翻几千页,这个优化简单又有效。
实际做排班系统的一些体会
这套企业级医护人员排班系统从零到落地,前后经历了需求梳理、数据库设计、自动排班引擎实现、审批流打通、权限管控和现场部署几个阶段。真正做下来以后,我最大的体会是:排班系统的技术难点从来不在某个单一技术点上,而在于业务规则的理解、数据模型的设计和系统对人工变动的接纳能力。
自动排班引擎写得再聪明,也不可能覆盖所有科室的真实情况。最终让护士长愿意每天打开这个系统、放弃 Excel 的,恰恰是那些看起来不起眼的功能:发布后一键通知、冲突时即时提醒、换班时自动避让、月底自动出统计报表。这些能力靠 SpringBoot、Vue、MyBatis、MySQL 完全能承载,关键是要舍得在这些业务细节上打磨。如果你也在做同类系统,我的建议是先别急着写代码,用两天时间把科室排班的真实规则和例外情况摸清楚,比什么都值钱。
