SpringBoot+Vue前后端分离医护人员排班系统设计与实现 📅 发布时间:2026/9/15 23:57:43 👁 浏览次数: 1. 项目概述与技术定位前后端分离的医护人员排班系统看到这个标题我就知道这类项目在市场上的需求量一直非常大。不管你是计算机相关专业的毕业生在准备毕业设计还是刚入行的Java后端开发想找个完整的全栈项目练手又或者是医疗信息化方向的技术人员想参考一下排班系统的业务设计这套SpringBoot Vue MyBatis MySQL的组合方案都能给你提供一套可以直接落地跑通的代码骨架。先说清楚这个系统到底能干什么管理员登录后台维护科室信息、医护人员账号、排班班次规则排班人员可以按周或按月维度查看和管理科室人员的排班情况支持自动排班推荐和手工调整医生护士登录后能看到自己的排班日历查询未来几周的班次安排系统还提供换班申请和审批流程遇到临时有事可以通过线上流程申请调整审批通过后自动更新排班状态。整个系统拆成前端和后端两个独立工程前端跑在Node环境通过接口请求访问后端服务数据全部落在MySQL里。这套技术栈的组合逻辑也非常清晰SpringBoot负责提供RESTful API并处理核心业务逻辑MyBatis作为持久层框架通过SQL映射与数据库交互Vue Element UI构建管理后台的界面和日历交互MySQL存储用户、科室、排班记录等关系型数据。前端用Axios发请求后端通过JWT做登录鉴权和接口权限控制跨域问题用配置类统一解决。整个链路从登录认证到数据查询都有完整的闭环非常贴近企业级项目的真实开发模式。适用人群方面如果你是新手这套项目最大的价值在于能让你把SpringBoot、MyBatis、Vue这几个主流技术通过一个真实的业务场景串起来。你会在项目里看到接口怎么设计、数据库表怎么规划、前后端怎么对接联调、打包部署怎么操作这些都是在单体项目或者教程Demo里学不到的实战经验。如果你已经是有一两年经验的开发者排班业务本身的复杂度也足够你研究特别是自动排班算法、换班事务处理、日历数据聚合这块有很多细节值得推敲。2. 系统架构设计与技术选型思路2.1 前后端分离架构的优势传统开发模式里前端页面由后端框架直接渲染典型的Java技术栈会用到Thymeleaf或者JSP页面和后端逻辑耦合在同一个工程里。排班系统这类管理应用如果采用单体模板渲染代码结构会随着功能增多迅速变得混乱尤其是日历交互、排班表格这种需要大量前端动态操作的页面每次修改界面布局都要动后端的Controller层开发和调试效率非常低。换成前后端分离之后前后端团队哪怕只有一个人从代码组织维度也是分离的可以完全独立开发。前端专注于页面渲染和用户交互后端专注于业务逻辑和数据处理。本地开发时前端通过Vite的代理转发请求到后端8080端口生产部署时前端打包成静态文件交给Nginx托管Nginx再把/api前缀的请求反向代理到后端服务。这条链路既解决了跨域问题也实现了静态资源和动态接口的分离部署。我个人的体会是前后端分离最大的收益不在技术本身而在工程组织。排班系统的页面类型非常丰富有数据表格页、日历视图页、表单弹窗页、审批流程页如果用模板引擎这些页面的交互逻辑既要写JS又要和后端模板标签嵌套时间一长维护成本非常高。分离之后前端是一个纯粹的Vue SPA所有页面组件化交互逻辑全部用JS控制后端只需要专注提供稳定规范的JSON接口职责非常清晰。2.2 技术栈对比与选型理由后端选SpringBoot几乎是这个场景下的默认答案。排班系统涉及用户认证、角色权限、定时任务、事务管理、数据库操作等能力SpringBoot的生态对这些都有非常成熟的整合方案。Spring Security JWT做认证Transactional注解管事务Scheduled做定时排班建议MyBatis管SQL映射。相比Node.js的Express框架和Python的DjangoSpringBoot虽然启动略重一点但类型安全和工程化能力更强特别是处理排班这种复杂的业务状态流转时Java的强类型和Spring的声明式事务管理能减少非常多隐蔽Bug。持久层选MyBatis而不是MyBatis-Plus或JPA核心考虑是排班查询的SQL复杂度。排班系统的核心查询是多条件组合科室筛选、日期范围筛选、值班类型筛选、人员状态筛选还要和换班申请、休假记录关联判断。这种场景下MyBatis的动态SQL可以精准控制每一段查询条件的拼接逻辑比JPA的自动生成SQL更可控。虽然MyBatis-Plus提供了很多便捷方法但动态排班SQL还是需要手写XML才能做到灵活高效。另外MyBatis的手写SQL方式对SQL优化也更友好复杂查询可以直接用Explain分析执行计划再针对性优化。前端选Vue Element UI理由也很直白Vue的学习曲线平缓模板语法和响应式机制很好上手Element UI的表格、表单、日历组件都能直接复用特别是el-calendar组件本身就实现了按月切换的日历框架用来做排班视图可以节省大量开发时间。相比React Ant DesignVue的中文资料更多遇到问题更容易查找解决方案对中小型管理项目来说效率优势明显。2.3 功能模块全景拆解排班系统的功能模块可以拆成六个核心域。第一个是系统管理域负责用户登录、角色权限、菜单管理这是所有管理系统的基座我们用的方案是JWT登录后按角色控制接口访问。第二个是基础数据域维护科室列表和医护账号信息医护人员按科室归属每个用户关联角色和职称信息。第三是班次规则域定义早班、中班、夜班、休假等班次类型每个班次包含开始时间、结束时间、是否需要跨天等参数。第四个是排班管理域整个系统的核心支持按月排班视图、自动排班生成、手工调班、批量排班导入。第五个是换班审批域医护人员发起换班申请科室负责人审批审批通过后两张排班记录做交换更新。第六个是统计报表域按人统计每月班次数量、按科室统计人力覆盖情况为排班决策提供依据。从标题提到的“完整源码”来看读者更关注的是能跑通的主链路登录 → 用户管理 → 科室维护 → 排班规则设置 → 排班日历展示和编辑 → 换班审批 → 部署上线。功能模块划分清楚后数据库设计和接口设计就有据可依了。3. 数据库设计与核心表结构3.1 数据表总体规划与关系设计排班系统的表结构设计是整个项目的地基。我经历过的很多类似项目前期表设计没做好后面写SQL的时候各种别扭排班查询动不动就关联五六张表性能和可读性都很差。这套项目里我把表拆成八张核心表sys_user表存储医护人员的登录账号和基本信息字段包括user_id、username、password、real_name、dept_id、title、phone、status。密码字段存的是BCrypt加密后的结果绝对不允许明文存储。sys_role表和sys_user_role关联表负责角色权限虽然医护排班系统的角色不多无非是系统管理员、科室排班员、普通医护但用标准的RBAC模型做以后扩展权限点会非常方便。sys_dept表存储科室信息字段很简单dept_id、dept_name、leader_id科室负责人leader_id关联sys_user表这个字段在换班审批环节会用到审批节点需要判断当前登录人是否是该科室的负责人。bs_shift表是班次规则表字段包括shift_id、shift_name、start_time、end_time、is_overnight是否跨天、color_tag前端日历显示的颜色标记。is_overnight这个字段容易被忽略但它在排班校验里非常关键跨天班次比如夜班20:00到次日08:00如果按自然日简单判断排班冲突就会出现逻辑漏洞。bs_schedule表是排班记录表核心字段包括schedule_id、user_id、dept_id、work_date、shift_id、status。status字段用来标记排班记录的状态正常值班、已换班待审批、已作废。每条排班记录上冗余存了dept_id虽然科室可以通过user_id关联查询得到但排班查看场景里按科室筛选非常高频冗余字段能省掉一次关联在这个数据量级下性能差距不明显但查询SQL的简洁度提升是实打实的。bs_shift_change表是换班申请表核心字段包括change_id、applicant_id发起人、target_date、source_shift_id、target_user_id被换班人、reason、approve_status、approve_remark、create_time。审批状态用整数存0待审批、1通过、2驳回比用字符串存储状态值更省空间配合MyBatis的ResultMap映射也更方便。3.2 建表SQL与MyBatis映射关键点以下是我在实际项目里沉淀的精简版建表SQL这套结构可以直接运行CREATE TABLE sys_user ( user_id bigint NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(64) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT BCrypt加密密码, real_name varchar(64) NOT NULL COMMENT 姓名, dept_id bigint DEFAULT NULL COMMENT 所属科室ID, title varchar(32) DEFAULT NULL COMMENT 职称, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint DEFAULT 1 COMMENT 状态1正常 0停用, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医护用户表; CREATE TABLE bs_schedule ( schedule_id bigint NOT NULL AUTO_INCREMENT COMMENT 排班ID, user_id bigint NOT NULL COMMENT 人员ID, dept_id bigint NOT NULL COMMENT 科室ID, work_date date NOT NULL COMMENT 值班日期, shift_id bigint NOT NULL COMMENT 班次ID, status tinyint DEFAULT 1 COMMENT 状态1正常 2待换班 3已作废, PRIMARY KEY (schedule_id), KEY idx_user_date (user_id, work_date), KEY idx_dept_date (dept_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT排班记录表;索引设计这里给新手提个醒排班记录表最常见的查询是按人查日期、按科室查月份所以联合索引就围绕这两个维度建。idx_user_date和idx_dept_date两个联合索引基本覆盖了所有高频查询场景。我见过很多项目把所有查询字段都加到索引里结果索引膨胀、写入变慢完全没有必要覆盖90%场景的索引才是好索引。3.3 数据库初始化与MySQL版本兼容问题MySQL的安装和初始化是很多新手卡壳的第一道关。这里重点说两个坑第一个是MySQL 8.x和5.x在连接方式上的差异MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver而5.x是com.mysql.jdbc.Driver。SpringBoot 2.x的版本如果不显式指定driver-class-name默认也会自动识别但如果你的JDBC URL、用户名、密码配置不对会报Communications link failure的错。第二个是时区问题JDBC连接串必须加上serverTimezoneAsia/Shanghai否则MySQL 8.x会报CLIENT_PLUGIN_AUTH错误或时区错误。建库时建议直接执行CREATE DATABASE IF NOT EXISTS hospital_shift DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4字符集必须强调因为排班备注、换班申请理由这些字段都可能包含emoji表情如果用了utf8字符集插入emoji会导致Incorrect string value报错。项目中所有表都用utf8mb4是我自己踩坑后的标配选择。4. SpringBoot后端核心实现4.1 项目初始化与目录结构SpringBoot项目的初始化主流两种方式访问Spring Initializr官网生成基础骨架或者在IDEA里直接新建Spring Initializr项目。搜索引擎里很多人在搜“springboot版本太高”怎么办这个问题的核心是SpringBoot 3.x和2.x在依赖坐标上的差异。SpringBoot 3.x基于Jakarta EEjavax.包全换成了jakarta.很多老版本的三方库直接不兼容。如果业务系统依赖的中间件没有及时适配老老实实用2.7.x是更稳妥的选择。我推荐的项目工程结构按业务域分包而不是按技术层分包com.hospital.shift ├── controller # 接口层 │ ├── AuthController.java │ ├── ScheduleController.java │ └── ShiftChangeController.java ├── service # 业务逻辑层 │ ├── ScheduleService.java │ └── ScheduleServiceImpl.java ├── mapper # MyBatis Mapper接口 │ ├── ScheduleMapper.java │ └── ScheduleMapper.xml ├── entity # 数据库实体 ├── dto # 前后端交互数据对象 ├── config # 配置类CORS、JWT拦截器 ├── common # 通用返回结果封装 └── utils # 工具类JWT工具、日期工具controller只做参数校验和结果封装不写业务逻辑service层负责具体业务处理比如排班生成的逻辑、换班审批后更新排班记录的事务操作mapper层只做SQL映射。排班业务中日期处理逻辑非常多我专门抽了一个DateUtils工具类处理周起始日、月天数、跨天班次校验等操作。分层清晰的好处是后期维护和排查问题很快能定位代码位置这也是企业级项目的基本要求。4.2 JWT认证与接口权限控制前后端分离系统最核心的认证方案就是JWT。用户登录成功后后端生成一个包含用户ID、用户名、角色信息的Token返回给前端前端存到localStorage里每次请求在请求头带上Authorization: Bearer {token}后端通过Spring拦截器统一校验。JWT生成的核心逻辑public String generateToken(LoginUser loginUser) { MapString, Object claims new HashMap(); claims.put(userId, loginUser.getUserId()); claims.put(username, loginUser.getUsername()); claims.put(role, loginUser.getRole()); return Jwts.builder() .setClaims(claims) .setSubject(loginUser.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 12)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); }拦截器里的校验逻辑其实不复杂从请求头取Token解析成功就放行解析失败返回401状态码。前端Axios响应拦截器收到401后就跳转登录页。权限控制的粒度我的做法是用一个自定义注解RequireRole(admin)或RequireRole(dept_leader)标记在Controller方法上拦截器里取当前Token携带的角色和注解要求做比较。这样普通医护调用管理员接口时会直接返回权限不足的提示不用在业务代码里反复判断角色。4.3 排班查询的动态SQL实现排班查询是整个系统复用率最高的接口前端日历组件每次切换月份都会请求一次。SQL的复杂度在于筛选条件不固定有的场景按科室查看整个月的排班有的场景按单个人员查看跨月时间段的班次还有的场景要同时筛选班次类型和日期范围。MyBatis的动态SQL在这种场景下非常好用select idselectScheduleList resultTypecom.hospital.shift.dto.ScheduleVO SELECT s.schedule_id, s.user_id, u.real_name, s.dept_id, d.dept_name, s.work_date, s.shift_id, sh.shift_name, sh.start_time, sh.end_time, sh.color_tag, s.status FROM bs_schedule s LEFT JOIN sys_user u ON s.user_id u.user_id LEFT JOIN sys_dept d ON s.dept_id d.dept_id LEFT JOIN bs_shift sh ON s.shift_id sh.shift_id where if testdeptId ! null AND s.dept_id #{deptId} /if if testuserId ! null AND s.user_id #{userId} /if if teststartDate ! null AND s.work_date gt; #{startDate} /if if testendDate ! null AND s.work_date lt; #{endDate} /if if testshiftId ! null AND s.shift_id #{shiftId} /if if teststatus ! null AND s.status #{status} /if /where ORDER BY s.work_date ASC, s.user_id ASC /select这个SQL写法里有个细节值得注意动态SQL用 标签后每个条件前的AND会被MyBatis自动处理第一个条件不会残留多余的AND。XML中的小于号必须转义成直接用会报XML解析错误这个坑我见很多人踩过。另外SELECT字段里把关联查询需要的展示字段都查出来前端日历渲染需要的personName、shiftName、colorTag直接就有了省得前端再做二次匹配。4.4 换班审批流程的事务设计换班审批是排班系统里业务逻辑最复杂、最考察事务能力的场景。流程是医护A发起换班申请选择自己和医护B在某天的班次进行交换提交到科室负责人审批。这里有两个核心问题需要仔细设计审批通过后如何更新排班记录以及并发场景下如何防止同一班次被多次调换。审批通过后的数据更新逻辑是这样的在bs_shift_change表中把approve_status更新为1同时把两个用户对应日期的排班记录做交换。实际操作是更新排班表将schedule_id为X的用户ID从A改为B将schedule_id为Y的用户ID从B改为A。这两个更新操作必须在同一个事务里否则可能出现只改了一个人排班的情况造成数据不一致。事务的Java实现Transactional(rollbackFor Exception.class) public void approveShiftChange(Long changeId, Long approverId) { ShiftChange change shiftChangeMapper.selectById(changeId); if (change null || change.getApproveStatus() ! 0) { throw new BusinessException(申请记录不存在或已处理); } // 权限校验审批人必须是申请人或目标人所在科室负责人 if (!isDeptLeader(approverId, change.getApplicantDeptId())) { throw new BusinessException(无审批权限); } // 校验目标日期排班是否仍有效 Schedule applicantSchedule scheduleMapper.selectByUserAndDate( change.getApplicantId(), change.getTargetDate()); Schedule targetSchedule scheduleMapper.selectByUserAndDate( change.getTargetUserId(), change.getTargetDate()); if (applicantSchedule null || targetSchedule null || applicantSchedule.getStatus() ! 1 || targetSchedule.getStatus() ! 1) { throw new BusinessException(排班记录状态异常无法完成换班); } // 交换两个用户排班记录中的user_id Long tempUserId applicantSchedule.getUserId(); scheduleMapper.updateUserId(applicantSchedule.getScheduleId(), targetSchedule.getUserId()); scheduleMapper.updateUserId(targetSchedule.getScheduleId(), tempUserId); // 更新换班申请状态 shiftChangeMapper.updateStatus(changeId, 1, 审批通过, new Date()); }这个函数里每一步都在做数据校验。特别是排班状态检查这一步如果排班记录已经被标记为待换班或者作废继续执行交换就会产生脏数据。并发控制方面我使用的是乐观锁思路在bs_schedule表加了一个version字段更新SQL里带上WHERE version #{oldVersion}更新失败说明有人在并发操作直接抛出异常提示前端重新加载数据。4.5 自动排班算法与定时任务自动排班推荐功能是这个系统比较出彩的点。核心逻辑是读取科室下所有在岗人员列表读取这个月需要覆盖的班次数量按照人员上个月班次数量从少到多排序优先给班次少的人分配值班。分配班次时还要检查约束条件连续值班天数不超3天、同一天只分配一个班次、跨天班次第二天不安排早班。算法用伪代码描述就是先按用户ID和月份查每个人已有班次数然后遍历本月每一天按班次类型逐一分配分配时遍历用户列表第一个满足所有约束条件的人被分配。由于值班人员的规模通常在几十人以内这种带约束的贪心分配算法性能完全没有问题。自动排班只生成待确认的草稿状态不直接覆盖已有人工排班记录。管理员可以一键清空草稿排班、重新生成也可以先运行自动排班再做手工调整这既保证了效率也保留了人工决策空间。我在实现时用Scheduled注解做了一个每天凌晨两点自动检查下月排班是否已生成的定时任务如果未生成就发送通知给排班员这个功能上线后收到了不少正反馈。5. Vue前端核心功能实现5.1 前端项目创建与Element UI集成前端工程用Vite创建Vue3项目命令是npm create vitelatest hospital-shift-frontend。Vite比Webpack的启动速度快很多本地开发体验很好而且Vue3 Vite的组合是当前社区的主流选型遇到问题资料很多。创建完项目后安装核心依赖npm install vue-router4 npm install pinia npm install axios npm install element-plus npm install element-plus/icons-vue npm install dayjsElement Plus的完整引入方式在main.js里import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/es/locale/lang/zh-cn import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(router) app.use(createPinia()) app.use(ElementPlus, { locale: zhCn }) app.mount(#app)注意Element Plus默认是英文语言管理系统中文字段比较多不设置locale会看到分页器的“Total x items”这种英文看起来很不专业。设置zhCn之后组件内置文案全部变成中文。另外Element Plus的图标不像旧版Element UI那样全局注册需要单独安装element-plus/icons-vue然后按需注册使用这是很多Vue2转Vue3的人容易踩的坑。5.2 排班日历组件的封装排班系统的核心交互是日历视图。Element Plus的el-calendar组件提供了按月切换的框架但排班数据的展示模式比较特别需要按天聚合展示多个人的班次信息。我的方案是封装一个ScheduleCalendar组件内部维护展示月份通过watch监听月份变化并触发接口查询。日历单元格的模板核心逻辑el-calendar v-modelcurrentDate template #date-cell{ data } div classday-cell span classday-number{{ data.day.split(-)[2] }}/span div v-foritem in getDaySchedules(data.day) :keyitem.scheduleId classshift-tag :style{ backgroundColor: item.colorTag } {{ item.realName }} - {{ item.shiftName }} /div /div /template /el-calendargetDaySchedules方法不需要遍历当天所有排班前端从接口拿到整个月的排班数组后用一个Map按日期分组key是yyyy-MM-dd格式的字符串value是该日期的排班数组。日历渲染时直接查Map即可时间复杂度O(1)即使一个月有几千条排班记录也不会造成渲染卡顿。5.3 Axios请求封装与token处理前端工程里所有接口请求都走封装好的Axios实例而不是每个页面单独写请求。核心封装逻辑在utils/request.jsimport axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器统一添加token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理业务码和异常状态 request.interceptors.response.use(response { const res response.data if (res.code 200) { return res } else if (res.code 401) { ElMessage.error(登录状态已过期请重新登录) localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(unauthorized)) } else { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { if (error.response error.response.status 401) { ElMessage.error(登录状态已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) })这个封装方案解决了vue前后端分离项目中最普遍的三个问题每个请求手动拼baseURL、每个页面判断登录状态、错误处理逻辑散落各处。把token的注入和401统一跳转收敛到拦截器之后后续新增页面只需要调用request.get(/schedule/list, { params })即可安全性和体验都有了。5.4 路由与页面权限控制前端路由设计分为公共路由和受保护路由两层。登录页是公共页面排班日历、排班管理、换班审批、用户管理都需要登录后才能访问。Vue Router的全局前置守卫里判断token是否存在不存在就重定向到登录页存在但访问的页面有管理员权限要求时再校验用户角色。路由的核心配置const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /schedule, children: [ { path: schedule, component: ScheduleCalendar, meta: { title: 排班日历 } }, { path: shift-change, component: ShiftChange, meta: { title: 换班申请 } }, { path: shift-approve, component: ShiftApprove, meta: { title: 换班审批, role: dept_leader } }, { path: user-manage, component: UserManage, meta: { title: 用户管理, role: admin } } ] } ]meta.role字段配合路由守卫实现页面级权限控制后端接口还有一层RequireRole注解做二次校验。前端的页面隐藏和后端的接口校验结合才能真正保证数据安全只靠前端隐藏菜单是挡不住接口直接调用的。6. 本地环境准备与完整部署流程6.1 基础环境安装与配置核对部署之前先把环境准备妥当。JDK选1.8或者11都可以SpringBoot 2.7.x对这两个版本兼容性都很好。Maven用于后端依赖管理和打包。Node.js版本建议16以上Vite 4要求Node 14.18版本太低会直接报错。MySQL用5.7或8.0都可以版本不同需要调整驱动配置这个前面已经说过了。后端application.yml核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_shift?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.shift.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: your-secret-key-change-in-production expire-hours: 12几个关键配置解释一下。map-underscore-to-camel-case设置为true数据库表的user_id字段就能自动映射到实体的userId属性省去写大量resultMap的麻烦。log-impl配置成StdOutImplMyBatis会把所有执行的SQL打印到控制台本地开发调试SQL非常有用生产环境记得关掉。allowPublicKeyRetrievaltrue是MySQL 8.x连接时经常遇到的坑不设置这个参数会报Public Key Retrieval is not allowed的错这个是MySQL 8.0.3之后驱动默认禁用RSA公钥检索导致的开发环境直接放开即可。6.2 后端打包与启动后端打包命令是mvn clean package -DskipTests执行完成之后target目录下会生成hospital-shift-0.0.1-SNAPSHOT.jar文件。这个jar包就是一个完整的可运行应用里面内置了Tomcat容器不需要再单独装Tomcat。启动命令java -jar hospital-shift-0.0.1-SNAPSHOT.jar生产环境推荐加JVM参数限制内存nohup java -Xms512m -Xmx512m -jar hospital-shift-0.0.1-SNAPSHOT.jar app.log 21 用nohup和让应用在后台运行日志输出到app.log文件方便排查。如果部署在云服务器上记得在安全组里放行8080端口否则外部访问不到。6.3 前端打包与Nginx部署前端构建命令是npm run build构建产物输出到dist目录。这个目录里是纯静态文件直接部署到Nginx的html目录下。生产环境的跨域方案不需要在后端代码里开CORS正确做法是用Nginx反向代理。Nginx配置的关键部分server { listen 80; server_name your-server-ip; # 前端静态资源 root /usr/share/nginx/html/hospital-shift; index index.html; # 页面刷新时路由自动指向index.html防止Vue Router history模式404 location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个非常重要的细节。第一个是try_files配置Vue Router如果使用的是history模式刷新页面时Nginx会拿着浏览器地址去磁盘找对应文件找不到就会404加了try_files之后会把所有路径都回退到index.html由前端路由自行匹配。第二个是location /api/的proxy_pass后面带了尾部斜杠这样前端请求/api/schedule/list会被转发到http://127.0.0.1:8080/schedule/list即/api前缀在后端路由中被剥掉。有人会问为什么前端本地开发时已经配了baseURL为/api后端接口路径却没有/api前缀不会404吗本地开发时Vite的proxy配置会把/api前缀转发到后端时去掉。生产环境Nginx的proxy_pass尾部斜杠也实现了同样的效果。这样一来后端接口可以保持干净的路径设计前端统一加前缀两边互不干扰。7. 高频问题排查与避坑经验7.1 跨域请求被拦截前后端分离项目最常见的错误。现象是浏览器控制台出现Access to XMLHttpRequest at http://localhost:8080/api/xxx from origin http://localhost:5173 has been blocked by CORS policy这样的报错。原因和解决方案要分清场景。本地开发时前端跑在5173端口后端跑在8080端口浏览器基于同源策略会拦截跨域请求。如果使用Vite代理前端请求写/api路径Vite把请求转发到8080浏览器看到的是同源请求不需要后端配置CORS。但如果你不走代理直接在前端请求http://localhost:8080/api/xxx那后端必须开启CORS。在SpringBoot中开启CORS的标准做法是重写WebMvcConfigurer的addCorsMappings方法允许指定来源和请求方法。这个方案适合开发期使用生产环境统一交给Nginx反向代理就能彻底规避跨域问题这也是我建议把CORS配置理解为开发便利配置而不是生产必要配置的原因。7.2 MyBatis数字比较与空值判断搜索引擎热词里有“mybatis 单个数字字符比较”这个问题我排查过很多次。典型场景是在MyBatis的XML里写类似AND status 1这样的条件如果status字段在数据库中是CHAR类型需要判断等于某个数字时会遇到类型比较问题。更隐蔽的情况是动态SQL里if teststatus ! null and status 1这个写法在MyBatis的OGNL表达式中会怎么执行当status是Integer类型时status 1可能不会按预期工作因为OGNL表达式对Integer和Integer的比较有时会出现类型解析问题。稳妥的写法是status 1L或者在Java代码里先把status转成Integer再传入。排查思路很简单SQL日志打印出来看实际执行的条件是什么就知道问题在哪。MyBatis配置了StdOutImpl日志后每次执行都会打印完整的SQL和参数值新人排查问题一定要学会看这一层信息不要瞎猜。7.3 Vue打包后白屏或资源404Vue项目本地运行正常npm run build后部署到Nginx发现白屏大概率是静态资源路径问题。默认Vite构建的index.html里资源引用路径是绝对路径/assets/xxx.js如果你把前端部署在Nginx的根路径下没问题但如果你用的是http://ip/hospital-shift/这种子路径访问就找不到资源。解决方案是在vite.config.js里设置baseexport default defineConfig({ base: ./, plugins: [vue()] })base设置为相对路径后构建产物中引用的资源变成相对路径./assets/xxx.js无论部署在哪个子路径下都能正确加载。这个配置改动简单但能省掉很多部署环境下的困惑。7.4 数据库时间字段显示8小时偏差排班系统的核心数据是日期和时间如果时间展示有偏差整个系统的排班数据看起来就是错乱的。问题的根源在JDBC连接串的serverTimezone和JVM默认时区的配合。开发机器和服务器不在同一时区或者MySQL的time_zone参数设置不对查询出来的时间就会和实际时间有偏差。标准解法是在JDBC URL里显式指定serverTimezoneAsia/Shanghai同时Jackson序列化的时区配置也用GMT8。如果后端是SpringBoot微服务部署在Docker容器里容器默认时区是UTC需要额外设置环境变量TZAsia/Shanghai否则应用日志和接口返回的时间都会偏8小时。这些属于基础配置问题但出现概率极高排查代价也很大列在这里帮大家直接跳过。7.5 换班审批后日历不刷新业务场景问题审批通过一个换班请求后前端排班日历没有立即显示更新后的排班情况。原因是前端日历组件只在切换月份时触发接口请求审批操作发生在另一个页面日历页面保留的是旧数据。解决方案有两种。方案一是用事件总线或者Pinia的共享状态管理审批通过后触发一个refresh事件日历页面监听这个事件后重新请求数据。方案二是切换回日历页时在onActivated钩子里强制刷新数据因为用了keep-alive缓存页面onMounted只执行一次后续每次切换到该页面都会触发onActivated。我的实际选择是方案二理由是最简单可靠。排班数据依赖的查询条件就是当前展示的月份每次日历页可见时重新拉取数据既能解决换班审批后的数据更新问题也保证了多人同时操作时数据的相对新鲜度。前端的“数据兜底”策略在业务系统中很实用与其设计复杂的消息通知机制不如用简单的页面生命周期钩子解决90%的刷新需求。这套排班系统从数据库建模到前端日历展示再到生产部署每个环节踩过的坑基本都覆盖到了。如果照着源码和教程走一遍前后端分离项目的整个开发链路会有一个完整的认知。最后再分享一个调试经验开发排班系统时一定要充分利用MyBatis的SQL日志打印每次操作排班、换班都去核对日志里SQL执行条件和结果刚开始会觉得麻烦习惯之后排查问题的速度会快很多。