基于SpringBoot+Vue3的医护排班系统开发实践

发布时间:2026/9/10 20:31:16
基于SpringBoot+Vue3的医护排班系统开发实践 排班系统这个项目我前后搭过两套。第一套还是用Freemarker写页面、后端一把梭的传统单体第二套才完全切到SpringBootVue3MyBatis前后端分离。说实话医护人员排班这块业务比普通的企业排班复杂得多——有夜班轮转、有职称约束、有工时统计还有换班审批这些细碎流程。用这套技术栈重新做一遍之后最大的感受就是系统的边界清晰了排班规则的扩展性也出来了。这篇就把整条链路拆开讲。从为什么选SpringBootVue3MyBatisMySQL这套组合到数据库怎么设计、自动排班算法怎么落地、Vue3前端日历组件怎么和接口对接最后是实际开发中踩过的坑和排查思路。适合正在做毕设、接外包或者想在企业内部快速搭建排班系统的人参考。项目源码是前后端分离结构后端负责排班引擎和业务接口前端负责交互展示数据库用MySQL存人员、科室、排班记录这些核心数据。1. 项目整体设计与思路拆解1.1 排班系统到底在解决什么问题医护排班和普通公司排班的区别很大。公司排班最多是“谁在哪个时间段值班”但医护场景里排班要同时满足几个硬性条件每个科室每天每个班次必须有对应职称的人值守比如夜班至少要有一个主治以上同一个护士不能连续两晚夜班之后又立刻排白班每个月总工时不能超过规定上限还要考虑年假、调休、产假这些请假类型对班次的影响。最早用手工Excel表排班的时候护士长每周五下午都要花三四个小时拿着一张纸质表对着人员名单来回画。人少还好科室超过15个人排列组合一下子就复杂了经常出现“排完班发现某天夜班没人”或者“某个人被排了连续三个夜班”的情况。所以做这个系统的核心目标很明确把排班规则固化到代码里用自动排班算法生成初稿再让护士长在系统里做微调最后发布排班表。1.2 系统的角色划分与功能边界系统的角色我分成了四类系统管理员、科室护士长、医生护士、科室主任。不同角色看到的功能入口差异很大这也是前后端分离模式真正发挥价值的地方——前端按角色动态渲染菜单后端按角色做接口权限控制。系统管理员维护科室、职称、账号、系统参数。护士长排班规则配置、自动排班、手工调班、发布确认。医生护士查看个人班表、提交换班申请、发起请假申请。科室主任查看全科排班总览、统计工时、审批特殊请假。这个权限模型决定了接口的设计方式。后端用SpringBoot拦截器加用户角色判断前端路由守卫控制页面访问。实际项目中我建议用Sa-Token或者Spring Security做权限但如果你只是想快速跑通用拦截器加一个RequireRole自定义注解就够了逻辑清晰还不用引入太重的依赖。1.3 为什么是SpringBootVue3MyBatis而不是其他组合选这套技术栈的原因其实很务实。SpringBoot是目前Java后端里开发效率最高的框架之一内置Tomcat、自动配置、starter机制少写大量配置代码尤其是版本用的是2.7.x稳定而且资料多。Vue3配合Vite的开发体验比Vue2Webpack强很多热更新速度快Composition API写复杂交互组件的时候代码组织更清晰。MyBatis则是为了SQL可控——排班查询往往涉及多表关联、动态条件、复杂统计MyBatis的XML里写SQL比JPA自动生成的SQL更直观、更好调优。MySQL作为存储层是最稳妥的选择。排班数据量虽然不大但是查询模式多样MySQL的索引机制完全能覆盖而且运维成本低不管你是部署在云服务器还是本地都不用操太多心。整体组合是“中间业务用Java扛、前端交互用Vue拖、SQL自己掌控”这套组合对于中小型业务系统来说性价比确实很高。2. 数据库设计排班系统的核心模型2.1 从业务需求推导数据表结构数据库设计是排班系统里最需要花时间琢磨的环节。我第一版设计的时候偷懒只建了一张schedule表把排班信息全塞进去结果后面加规则、加班次统计的时候表结构改得痛不欲生。第二版老老实实把表拆分成了这样user表用户基础信息包括姓名、工号、职称、所属科室、角色。department表科室信息。shift表班次字典表定义白班、小夜、大夜、休息、节假日班等。schedule_rule表排班规则配置表每个科室可以配置周期、每班人数、约束条件。schedule_record表排班结果表记录某人在某天属于哪个班次。shift_change表换班申请表。leave_request表请假申请表。以schedule_record表为例核心字段是id、user_id、department_id、work_date、shift_id、create_time。唯一索引建在(user_id, work_date)上防止同一天对同一个人排两次班。这是排班系统的底线约束必须在数据库层面锁死不能只靠前端校验。CREATE TABLE schedule_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 排班人员ID, department_id BIGINT NOT NULL COMMENT 科室ID, work_date DATE NOT NULL COMMENT 排班日期, shift_id BIGINT NOT NULL COMMENT 班次ID, status TINYINT DEFAULT 0 COMMENT 0草稿 1已发布, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 班次字典与规则配置表的设计思路班次字典表最好不要写死。不同医院的班次叫法不一样有的医院有“白班、小夜、大夜、休息”有的还有“责任班、辅助班、预约班、门诊班”。如果你把班次写死在枚举里每次调整都要改代码重新部署。所以我把班次做成字典表由管理员在系统里维护。schedule_rule表是排班系统里最灵活的配置点。它设计成规则模板的结构每个科室一行规则记录了该科室的排班周期、周期内各天需要的班次及人数。举个例子某科室规则是这样的周期为7天周一到周五每天需要2个白班、1个小夜、1个大夜周末需要1个白班、1个小夜、1个大夜。这个规则在表里用JSON字段或者子表方式存储都可以。我建议用规则明细子表因为后续做算法读取的时候SQL查询比解析JSON更直接。CREATE TABLE schedule_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, department_id BIGINT NOT NULL, rule_type VARCHAR(20) DEFAULT weekly COMMENT 周排班/月排班, cycle_days INT DEFAULT 7 COMMENT 排班周期天数, effective_date DATE COMMENT 生效日期, status TINYINT DEFAULT 1, remark VARCHAR(255) );2.3 索引设计与事务边界要注意的点排班系统查询最多就是“按科室日期范围查排班表”和“按人员查当月班表”。这两个查询要单独建索引。(department_id, work_date)和(user_id, work_date)是两个最核心的联合索引。实际开发中我发现如果某个科室排班记录超过10万条之后不加索引的慢查询会直接拖垮接口尤其是日历组件一次要拉整月的排班数据。事务边界主要在换班审批和请假流程。换班涉及两条schedule_record记录的修改必须放在同一个事务里避免出现一个人被换到两个班次的脏数据。排班自动生成的时候建议事务粒度控制在“每次只生成一个科室的一轮班表”如果整个科室月初批量生成50个人的整月班表一个事务锁太多记录并发操作时会出问题。3. 自动排班算法从理论到Java实现3.1 规则建模把护士长的经验翻译成代码排班算法是整个系统的灵魂。要实现自动排班第一步就是把你脑子里的排班规则翻译成计算机能理解的形式。这里我用的是约束满足的思路把排班需求拆成硬约束和软约束。硬约束是必须满足的违反就不生成每天每个班次的在岗人数必须达标。同一个人同一天只能排一个班次。一个人连续排班天数不能超过6天。大夜班之后必须安排至少48小时休息。软约束是尽量满足的用于优化排班质量同一个人尽量不连续上两个大夜班。每个护士一个月内的夜班次数尽量均等。周末和法定节假日的班次尽量轮换分配。这种建模方式在代码里不需要复杂的人工智能算法用轮转加上回溯就够了。先把人员列表按工号排序维护一个“上次夜班日期”数组遍历每一天的时候优先选出满足硬约束的人再在候选集合里按照“夜班距离天数最长优先”的规则选人这样就能自动实现夜班次数均等的效果。3.2 Java实现基于轮转的排班引擎实际写排班引擎的时候我采用的是“按天逐日生成周轮转偏移”的方式。核心代码如下public ListScheduleRecord generate(ListUser users, Department dept, ListShift shifts, LocalDate startDate, int days) { ListScheduleRecord result new ArrayList(); MapLong, LocalDate lastNightMap new HashMap(); for (int i 0; i days; i) { LocalDate date startDate.plusDays(i); MapString, Integer demand getDemand(dept.getId(), date); for (Shift shift : shifts) { int needCount demand.getOrDefault(shift.getCode(), 0); if (needCount 0) continue; ListUser candidates users.stream() .filter(u - !u.isLeave(date)) .filter(u - !isOnDuty(u, date, result)) .filter(u - !violatesNightRest(u, date, lastNightMap, shift)) .collect(Collectors.toList()); candidates.sort((a, b) - compareByLastNight(b, a, lastNightMap)); ListUser selected candidates.subList(0, Math.min(needCount, candidates.size())); for (User user : selected) { ScheduleRecord record new ScheduleRecord(); record.setUserId(user.getId()); record.setWorkDate(date); record.setShiftId(shift.getId()); record.setDeptId(dept.getId()); result.add(record); if (shift.isNight()) lastNightMap.put(user.getId(), date); } } } return result; }这段代码的核心逻辑就是三层过滤加一层排序。先过滤掉请假的人、当天已经排了班的人、夜班后休息不够的人然后对剩余候选人按“离上次夜班间隔时间最长优先”排序选前N个。这样既满足硬约束又不至于出现某个人被连续安排多个夜班的情况。3.3 冲突检测与手工调整实现自动排班只能生成初稿实际场景中护士长几乎总要手工微调。所以系统必须提供一个“冲突检测”功能在护士长调整完班次后马上提示哪些调整违反了硬约束。我在后端实现了一个validate接口接收调整后的批量排班数据逐条检查硬约束返回冲突列表。前端收到冲突列表后直接在日历上把冲突格子标红鼠标悬停还能看到具体冲突原因。这个交互在Vue3里实现很顺手用computed属性监听排班数据变化动态计算每个格子的样式类就行。冲突检测器本质上是把排班引擎里的约束逻辑抽出来形成一个独立的校验类。这里要特别注意手工调整后的校验和自动生成时的约束判断必须复用同一套代码不要写两遍。不然会出现“自动排班能通过手工调完却校验不过”或者反过来的一致性问题。4. 后端核心实现SpringBoot接口与MyBatis落地4.1 项目分层与核心接口设计后端项目我用的是经典四层结构Controller、Service、Mapper、Entity。Controller只做参数接收和结果封装不写任何业务逻辑。Service层放排班算法、审批流这些业务逻辑。Mapper层只负责和数据库交互。接口设计遵循REST风格核心接口有以下几个接口方法说明/api/department/{id}/schedule?startendGET查询某科室的排班表/api/schedule/generatePOST触发自动排班/api/schedule/adjustPUT手工调整班次带校验/api/schedule/publishPUT发布排班表/api/shift-change/applyPOST提交换班申请/api/shift-change/approvePUT审核换班申请/api/stats/workload?userIdmonthGET工时统计统一返回体我用的是Result对象包含code、message、data三个字段。这样做的好处是前端axios拦截器可以统一处理错误码比如code为401时自动跳转登录页code为500时弹出错误提示。4.2 MyBatis动态SQL实战条件查询与批量插入排班查询接口是最典型的“多条件动态查询”参数可能有科室、日期范围、班次类型、人员姓名、角色。用MyBatis动态SQL处理这种场景非常合适。select idselectScheduleRecords resultTypecom.example.entity.ScheduleRecord SELECT sr.*, u.name AS userName, u.rank AS userRank, s.name AS shiftName, s.start_time AS shiftStartTime FROM schedule_record sr LEFT JOIN user u ON sr.user_id u.id LEFT JOIN shift s ON sr.shift_id s.id where if testdepartmentId ! null AND sr.department_id #{departmentId} /if if testuserId ! null AND sr.user_id #{userId} /if if teststartDate ! null AND sr.work_date gt; #{startDate} /if if testendDate ! null AND sr.work_date lt; #{endDate} /if if testshiftId ! null AND sr.shift_id #{shiftId} /if /where ORDER BY sr.work_date, sr.shift_id /select批量插入排班记录的时候MyBatis的batch要小心一点。我建议用executeBatch的方式而不是foreach标签一条条insert因为后者拼接出来的SQL特别长MySQL默认的max_allowed_packet可能不够用。实际代码层面在SpringBoot里注入SqlSessionTemplate强制走batch executorAutowired private SqlSessionTemplate sqlSessionTemplate; public void batchInsert(ListScheduleRecord records) { SqlSession session sqlSessionTemplate.getSqlSessionFactory() .openSession(ExecutorType.BATCH, false); try { ScheduleRecordMapper mapper session.getMapper(ScheduleRecordMapper.class); for (ScheduleRecord record : records) { mapper.insert(record); } session.commit(); } finally { session.close(); } }4.3 权限拦截与日志记录的实现细节权限拦截我用的是SpringBoot的HandlerInterceptor。定义一个注解RequireRole标注在Controller方法上拦截器里从请求头中取出token解析用户信息判断角色是否有权限访问对应接口。日志方面我用的AOP切面统一记录操作日志切点定义在Controller层记录请求参数、返回结果、耗时。特别是排班发布和换班审批这两个敏感操作一定要记录操作人和操作时间。不然出了事没法追溯。一个容易忽略的细节是排班数据的查询接口要支持“发布前草稿”和“发布后正式”数据的分开查询。草稿状态的数据只对护士长和管理员可见普通护士看不到发布后所有人都能看到。这个状态区分在查询SQL里要加条件判断不要把所有排班数据一股脑返回给前端让前端自己过滤。5. 前端Vue3实现排班日历与交互细节5.1 Vite初始化与项目结构组织前端我用Vite初始化Vue3项目相比vue-cliVite的依赖预构建和热更新速度快很多。项目结构上views按页面划分components放可复用的排班日历、人员选择器等组件stores放Pinia状态管理api目录集中管理所有后端接口请求。npm create vitelatest nurse-schedule-fe -- --template vue cd nurse-schedule-fe npm install npm install pinia vue-router axios element-plusElement Plus是这套系统的UI基石。排班页面里用到的日历组件、下拉选择器、弹窗、表单Element Plus都有现成的稍微改改样式就能用。日历这一块我用的是FullCalendar的Vue3适配因为Element Plus本身没有完整的日历年视图组件而排班表的周视图、月视图切换FullCalendar自带这个能力。5.2 排班日历组件数据绑定与视图渲染排班日历组件的核心是把后端返回的排班记录数组映射成日历上的单元格。我设计的数据结构是Map键是日期字符串值是该日期所有排班条目列表。const scheduleMap computed(() { const map new Map(); props.scheduleList.forEach(item { const key item.workDate; if (!map.has(key)) map.set(key, []); map.get(key).push(item); }); return map; });模板里渲染单个格子的时候把当前格子的日期作为key去取map中的值遍历显示班次名称和人员姓名。每个班次用不同颜色的标签区分白班绿色、小夜橙色、大夜深蓝色、休息灰色。颜色的配置放在班次字典数据里由后端返回这样护士长在前端调整班次颜色就不需要改代码。排班调整操作在日历上的交互是点击某个格子弹出一个对话框显示当前已排班人员和可以替换的人员列表替换时前端先把候选人的日期占用情况查出来如果目标人在当天已经有班就提示冲突。这个“先查再用”的前置校验能减少很多无效请求。5.3 前后端联调的接口设计与状态管理联调的时候最容易出问题的是日期格式。后端返回的LocalDate序列化之后默认是2024-12-01这种字符串前端组件接收没问题。但如果前端传日期参数给后端最好统一用YYYY-MM-DD格式不要用时间戳。我在axios封装里做了请求拦截器如果params里有Date类型就自动格式化成字符串避免格式不一致。Pinia在这里主要管理两类状态当前登录用户信息、当前选中的科室和日期范围。登录信息在用户刷新页面后需要从本地存储恢复通过一个initialize方法在App.vue的onMounted里调用。科室和日期范围则是一进入排班页就更新后续组件共享这些响应式状态。前端还有一个很重要的细节发布排班表时要先拉取最新的排班数据对比前端本地数据是否有改动。如果护士长打开页面后另一个人已经发布了新班表这时候再发布就会覆盖掉别人的操作。我在发布接口里加了一个version字段做乐观锁前端把最初拉取数据时返回的version回传后端对比version不一致就拒绝发布提示“页面数据已过期请刷新后重试”。6. 常见问题排查与避坑实录6.1 SpringBoot与MyBatis的版本兼容坑SpringBoot版本高的时候MyBatis的starter也要跟着升级。我见过最多的问题是SpringBoot 3.x要求JDK 17以上如果本地装的还是JDK 8启动就会直接报错。所以如果你用的是JDK 8环境SpringBoot尽量选2.7.xMyBatis Spring Boot Starter选2.x版本。SpringBoot 3.x MyBatis starter 3.x虽然也能跑但是需要额外引入mybatis-spring的配置调整新手容易卡住。另一个高频问题是MyBatis接口和XML映射文件绑定不上。报错信息一般是Invalid bound statement (not found)。排查思路固定检查Mapper接口和XML文件在同一个包路径下检查application.yml里mybatis.mapper-locations配置的是不是classpath:mapper/*.xml检查target/classes目录里有没有编译进去XML文件。最后一条很关键IDEA默认不会把src/main/java下的XML文件拷贝到target目录如果你把XML放到了java包里必须加一个build配置build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build6.2 数据库连接池与SQL性能问题开发环境经常遇到数据库连接不够用的情况。SpringBoot默认的HikariCP连接池默认最大连接数是10如果排班页面同时打开多个日历视图每个视图发多个请求连接池很容易被打满。出现这个问题的典型报错是Connection is not available, request timed out。解决办法是把最大连接数调到50左右注意设max-lifetime防止数据库侧回收了连接但连接池侧还不知道。SQL性能上最容易踩的坑是排班总览页面的N1查询。一次拉取整个科室一个月的数据如果先查排班记录再逐条查用户名、班次名性能会很差。正确做法是用一条JOIN SQL把排班记录、用户姓名、班次信息一次性查出来。用上面MyBatis里那套LEFT JOIN方案就能解决。6.3 Vue3中容易踩的坑响应式丢失与组件刷新Vue3里一个常见的坑是用reactive包裹数组然后直接用下标赋值结果界面不刷新。这是因为Vue3的reactive对数组下标的更新是支持响应式的但如果你用解构的方式把数组元素赋值给了普通变量再修改这个普通变量的属性响应式就断了。排班调整逻辑里我遇到过修改某个scheduleItem对象的shiftId页面不更新的情况原因就是对象是从Map中取出来的修改的只是局部引用。解决办法是修改后把整个Map重新赋值或者用ref包一层通过.value方式整体替换。我在排班日历组件里所有修改排班数据的操作最后都会调一个refresh方法重新从后端拉数据保证视图一致性。这种做法虽然多一次请求但是避免了响应式复杂调试对排班这种低频操作来说体验更好。6.4 排班系统的并发问题重复排班和发布覆盖并发问题其实在演示环境很少出现生产环境一上就暴露。我遇到最典型的现象是两个护士长同时打开排班页面A排了周一的班B排了周二的班结果B先保存A后保存B的排班被覆盖了。这个问题就是前面提到的失发布用version乐观锁解决。到数据库层再补充一个唯一索引(user_id, work_date)即使代码里有漏洞数据库这层兜底也不会出现同一个人同一天被排两个班的情况。排班系统的数据量虽然不大但并发问题仍然值得注意因为你面对的不是“大量用户”而是“少量用户做高频操作”。两个护士长可能同时在调整班表操作节奏又完全一致所以乐观锁这里是必须实现的不能怕麻烦而省略掉。7. 从源码到部署环境搭建与运行配置7.1 本地环境准备JDK、Node、MySQL版本选择想把这个项目跑起来本地环境需要JDK 8或11对应SpringBoot 2.7.x、Node 16.20对应Vite 4、MySQL 5.7或8.0。MySQL 8.0需要注意时区问题连接串里必须加serverTimezoneAsia/Shanghai否则会报Server returns invalid timezone错误。前端启动前要改api配置文件里的后端地址默认是http://localhost:8080/api。如果后端接口前缀不同调整axios的baseURL就好。我用.env.development文件管理环境变量开发环境和生产环境分开配置。# .env.development VITE_API_BASE_URLhttp://localhost:8080/api后端启动前要确保数据库库表已经建好我提供了建表SQL脚本直接用Navicat或者命令行执行都行。注意字符集要指定utf8mb4不然存不了emoji或者特殊符号。数据库连接信息在application.yml里配置默认账号密码是root/root如果不一致要改掉。7.2 生产部署前后端分离项目的打包方式生产部署其实不算复杂但有几个细节要处理好。前端用npm run build生成dist目录里面是纯静态文件用Nginx托管。后端用Maven打包成jar包java -jar命令启动。Nginx配置的关键是反向代理后端接口。前端静态资源直接访问Nginx端口而/api路径要代理到后端服务server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }最后一行try_files配置特别重要Vue3路由用history模式时刷新非首页路径会出现404这行配置的作用是把所有前端路由都回退到index.html由前端路由接管。如果你用的是hash模式就不会有这个问题但history模式更干净所以我推荐保留这行配置。7.3 排班系统后续扩展方向这个项目的架构留了不少扩展空间。比如自动排班算法目前是轮转加约束后续可以接入更智能的优化算法像遗传算法或者模拟退火用来寻找更均衡的排班方案。再比如数据报表可以做深从单纯的工时统计扩展到人员负荷分析、夜班频次预警这些都是护士长非常关心的点。我自己的体会是排班系统真正难的不是技术而是把业务规则完整梳理清楚。技术栈只是一层壳核心是对排班业务的理解程度。你如果只是想要一个能跑起来的Demo那按照这套架构搭起来很快但如果你想真正在科室里用起来一定要花时间把护士长的排班思路沟通透把所有规则落到代码里然后再开始写逻辑。这个顺序千万不能反。最后再分享一个踩过的坑开发排班条目的拖拽调整功能时我一直纠结要不要做拖拽后来发现护士长的操作习惯是点选加确认而不是拖拽。做完第一版拖拽功能后在演示时护士长普遍反馈不习惯最后还是改回了点选模式。所以做这类行业系统先搞清楚目标用户的真实使用习惯比追求交互炫酷重要得多。