ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+Vue医院急诊系统开发实战

2026/9/26 4:36:46 拓冰建站 浏览量
SpringBoot+Vue医院急诊系统开发实战 “医院急诊系统”这个标题第一眼看起来很像课程设计或者毕业设计的选题但真正把 SpringBoot Vue 前后端分离这套东西落地到急诊业务上你会发现它比普通的管理系统要麻烦不少。急诊的核心是“快”和“准”挂号、分诊、留观、收费、取药这几个环节环环相扣做系统的人如果只盯着 CRUD 写最后交付的很可能就是一个能跑但不好用的东西。我前前后后带过几个类似的项目也帮人改过不少这样的毕设代码。这篇文章就围绕 SpringBoot Vue 的医院急诊系统把从业务拆解、后端设计、前端联调到部署运行和排坑的完整思路讲清楚。不管你是准备做课程设计、毕业设计还是真的想在小规模医院环境里跑一套急诊信息管理系统这里面的设计取舍和实操细节应该都能帮你少走不少弯路。1. 先想清楚急诊系统到底在解决什么问题做任何系统之前第一件事不是建表也不是配框架而是把业务捋清楚。急诊科的业务流和门诊科差异很大如果用做“进销存”或者“通用 OA”的思路去做急诊系统后期需求一变就是大改。1.1 急诊流程和门诊流程差异在哪普通门诊的流程相对线性挂号、排队、看诊、开药或检查、缴费、取药。患者可以慢慢等系统慢个几秒通常没太大影响。急诊则完全不同。患者到急诊科时可能是急危重症分诊护士要在很短的时间内评估病情严重程度决定是进入红色区、黄色区还是绿色区。有的患者需要马上抢救有的需要留观有的只是轻微外伤包扎。这个“分诊”动作在医院信息系统里往往对应一个优先级字段比如“危”、“急”、“普通”三个等级系统的 UI 和接口设计都要围绕这个字段做文章。另外一个差异是时间敏感度。急诊的挂号、转归、留观记录、抢救记录都和具体时间强绑定记录必须精确到分钟级。比如留观患者的首次病程记录、出区时间、转入转出信息这些在普通门诊系统里几乎用不到但在急诊系统里属于核心数据。所以设计数据表的时候“时间”字段不是可选项而是每个核心流程表都必须存在的关键字段。1.2 模块拆分与功能清单对于基于 SpringBoot Vue 的医院急诊系统我习惯把功能模块拆成以下几个部分患者与挂号模块急诊患者登记、急诊挂号、急诊号源管理等。这里要支持“先抢救后补挂”的场景也就是患者信息可以先简化录入后续再补全。分诊与候诊模块护士分诊、分诊优先级设置、候诊队列展示。这块是整个系统的交互重点急诊大厅的大屏或护士工作台页面主要看这个队列。医生工作站模块急诊医生接诊、诊断录入、开立医嘱药品、检查、检验、病历书写等。留观管理模块留观床位管理、留观患者记录、留观费用累计、留观转归。收费与结算模块急诊收费、费用清单、退费处理、收据打印。统计与查询模块急诊工作量统计、病种分布统计、患者来源统计、急诊滞留时间统计等。如果你只是做毕设不需要把医保结算、药房库存这种大系统级别的功能全部塞进去否则工作量会失控。比较合理的方式是聚焦急诊主链路把“挂号—分诊—接诊—留观—收费”做完整再做一点统计报表点缀整体项目的完整度和答辩素材就都有了。1.3 技术选型思路为什么是 SpringBoot Vue现在医院信息系统的主流架构其实还是前后端分离的 Java 系SpringBoot 几乎是事实标准。原因很简单生态成熟上手门槛相对低SpringCloud 微服务那套对单个急诊系统来说又过于重一个单体 SpringBoot 足够应付几千人规模医院的急诊数据量。前端选择 Vue 的优势就更直接了。Vue 对初中级开发者非常友好数据双向绑定让“患者列表点击后刷新右侧详情”这种交互写起来很直观组件化开发也能把分诊台、医生工作台、收费窗口拆成独立模块各写各的互不干扰。加上 Element UI 或 Element Plus 这类组件库表格、时间选择器、表单弹窗这些医院业务的高频组件都是现成的。选 Vue 2 还是 Vue 3 要根据自己的情况定。如果是课程设计、论文答辩建议 Vue 3 Element Plus技术栈更新导师接受度也高。刚接触的话Vue 2 Element UI 的网课资料和现成代码最多踩坑成本低一些。我自己更推荐 Vue 3因为这套系统做完之后如果你想继续往深走Vue 3 的组合式 API 和 TypeScript 生态能让你后续扩展更舒服。2. 后端这样设计才扛得住急诊场景SpringBoot 后端的核心不是把接口写出来而是把数据模型设计合理、把鉴权和异常处理做完整、把核心业务的接口边界控制好。下面这几个点是我觉得最容易出价值感的地方。2.1 目录结构和依赖怎么搭我用 Maven 做依赖管理项目结构一般长这样src/main/java ├── com.hospital.emergency │ ├── config 配置类跨域、拦截器、MyBatis-Plus分页插件 │ ├── controller 控制层 接口入口 │ ├── service 业务层 实现类 │ ├── mapper MyBatis-Plus数据访问层 │ ├── entity 数据库实体类 │ ├── dto 前端交互对象入参、出参 │ ├── vo 视图对象查询结果、统计结果、分页结果 │ ├── common 统一返回、状态码、全局异常 │ ├── utils JWT工具、日期工具、Excel导出工具 │ └── security 登录鉴权相关 src/main/resources ├── application.yml ├── mapper.xml如果不用注解SQL单独放XMLpom.xml 里最基础的依赖我列一下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyMyBatis-Plus 是我个人在这个项目里比较推荐的选择而不是原生 MyBatis。急诊系统里有大量简单的单表 CRUD比如“查患者登记信息”“插入留观记录”“更新费用状态”MyBatis-Plus 的 BaseMapper 直接自带这些方法能省掉大量重复的 XML 编写。分页插件也内置支持一个配置类就搞定后面我会专门讲分页插件的坑。application.yml 的配置核心是数据源和端口server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_emergency?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意serverTimezoneAsia/Shanghai和 Jackson 的time-zone: GMT8必须一起配否则时间会差 8 个小时。这个坑在下面的避坑部分还会细说。2.2 核心表结构设计急诊系统的核心表建议这样划分。第一类是主数据表包括患者表、科室表、医生表、收费项目表第二类是流程表包括急诊挂号表、分诊表、急诊病历表、留观记录表第三类是辅助表包括操作日志表、统计汇总表、字典表。我拿急诊挂号表和分诊表举例t_emergency_registration id 主键 patient_id 患者ID patient_name 患者姓名冗余方便列表展示 gender 性别 age 年龄 phone 联系电话 id_card 身份证号 emergency_no 急诊号如 JZ20250110001 triage_level 分诊等级0未分诊 1普通 2急 3危 status 状态1待诊 2接诊中 3留观 4离院 triage_nurse_id 分诊护士ID visit_time 到院时间 register_time 挂号时间 deleted 逻辑删除标记我的习惯是“患者基本信息 就诊信息”分开存储。患者基本信息放t_patient每次挂号时把患者姓名、性别、年龄冗余一份到挂号表。这样做的好处是统计报表直接查挂号表就能出数据不用每次都 JOIN 患者表。冗余虽然违背三范式但在实际项目里能显著提升查询效率尤其是在前端分页列表展示高峰期。分诊表我单独建一张因为一次就诊可能会有多次分诊评估t_triage id registration_id 挂号记录ID nurse_id 分诊护士ID triage_level 分诊等级 temperature 体温 pulse 脉搏 blood_pressure 血压 consciousness 意识状态 chief_complaint 主诉 triage_time 分诊时间这里要强调一个细节分诊结果不要只更新挂号表的triage_level字段而是同时在t_triage里插一条完整记录。因为急诊科需要追溯“谁在什么时间给了什么评估”如果只更新一个字段后续要出分诊质量分析报表时就没数据可用。2.3 登录鉴权JWT 方案落地医院系统的账号体系比较简单角色就是管理员、导诊/护士、急诊医生、收费员这几类。我建议用 JWT 拦截器的方式而不是 Session。前后端分离项目天然适合 Token 机制且移动端如果后续要接入小程序或 AppToken 方案更容易复用。JWT 的核心逻辑有三个部分生成 Token、拦截请求解析 Token、处理过期和非法 Token。生成 Token 的工具类我一般用 jjwt 实现public class JwtUtil { private static final String SECRET hospital-emergency-secret-key; private static final long EXPIRE 1000L * 60 * 60 * 24 * 3; // 3天 public static String createToken(Integer userId, String username, String role) { return Jwts.builder() .setId(String.valueOf(userId)) .setSubject(username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET) .parseClaimsJws(token).getBody(); } }拦截器里做的事情也很直白从请求头里拿Authorization去掉Bearer前缀解析成功就把用户信息放进 request 上下文解析失败直接返回 401。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.getId()); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }这里有个容易忽略的问题跨域请求在正式发 POST、PUT 之前浏览器会先发一个 OPTIONS 预检请求。如果你在拦截器里直接拦截所有请求又不放行 OPTIONS前端调接口就会一直报跨域错误搞半天还以为是 CORS 配置问题。所以OPTIONS请求必须在拦截器里直接返回 true。2.4 急诊核心接口实现要点接口设计上不要把所有业务都堆在 Controller 里。Controller 只做参数接收和调用 Service业务逻辑必须放到 Service 层。比如“分诊完成后更新候诊队列状态”这种操作涉及挂号表状态修改、分诊表插入、候诊队列刷新三个动作就应该写成一个带事务的 Service 方法而不是在前端调多个接口。我用Transactional标注这种多步骤操作public interface TriageService { Transactional(rollbackFor Exception.class) TriageResultVO triage(TriageDTO triageDTO); }分诊接口的返回结构很关键。前端分诊台页面的核心交互是护士看到候诊列表选中一个患者填写生命体征和主诉选择分诊等级点击提交。提交成功后候诊列表顺序要立刻变化危重患者要置顶。所以接口返回值不能只返回一个“成功”标记还要返回更新后的候诊队列最好是返回当前的候诊列表和该患者在队列中的位置。我的返回 DTO 长这样public class TriageResultVO { private Integer patientId; private String emergencyNo; private Integer triageLevel; private ListWaitingPatientVO waitingQueue; } public class WaitingPatientVO { private Integer registrationId; private String patientName; private String emergencyNo; private Integer triageLevel; private String TriageLevelName; private Date visitTime; }这样前端拿到返回值后直接就能用新队列渲染页面不用再额外请求一次列表接口。看着是多写了一个字段的事实际体验差异非常大急诊分诊台追求的是“快”少一次请求就少一次等待。医生工作站的核心接口里最容易忽略的是医嘱开立后的费用计算。开药和开检查单时系统要根据收费项目表里的单价自动累积费用如果患者走的是留观流程还要按天计算留观床位费。这块建议在 Service 层做一个独立的费用计算模块public BigDecimal calcPatientFee(Integer registrationId) { // 1. 查询该患者的医嘱列表 // 2. 根据医嘱关联收费项目累计药品费和检查费 // 3. 查询留观记录计算床位费 // 4. 返回总费用 }这套计算逻辑可以后续被收费模块复用不用收费处再写一遍。2.5 统一返回结构和全局异常处理接口返回格式不统一是前后端联调效率低下的第一大元凶。我见过很多项目有的接口返回{code:200, data:xxx}有的接口直接返回裸数据还有的错误信息是英文或后端异常堆栈前端解析起来非常痛苦。统一返回结构看起来是小事但收益极大。我的Result类长这样public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }全局异常处理用RestControllerAdvice处理把未知异常统一转成友好提示。急诊系统后期最容易出问题的地方就是对空值和特殊字符的校验。患者姓名为空时就提交挂号、电话号输错、身份证号超长、诊断描述带了 XSS 脚本这些都是真实会发生的请求。所以我强烈建议在 DTO 上直接加参数校验注解比如NotBlank、Length、Pattern把校验前置到 Controller 层。这样 Service 层处理业务时就不需要到处写if (xxx null)的判断。3. 前端 Vue 部分怎么跟后端配合前端不只是在写页面真正的难点在于理解后端接口返回的数据结构、处理好状态更新的时序。急诊系统的页面交互复杂度比普通表单管理系统高不少下面几个点是我觉得最关键的。3.1 前端工程初始化和目录规划如果你直接用 Vite 创建 Vue 3 项目一条命令就搞定了npm create vitelatest hospital-emergency-frontend -- --template vue cd hospital-emergency-frontend npm install npm install element-plus axios vue-router pinia目录规划方面我建议把业务模块按“页面 API 路由”绑定在一起而不是所有接口请求都堆在一个 api.js 里。急诊系统前端目录可以这样分src ├── api 按模块拆分的接口文件 │ ├── registration.js │ ├── triage.js │ ├── doctor.js │ ├── fee.js │ └── statistics.js ├── router 路由配置 ├── stores Pinia状态管理 ├── views 页面 │ ├── login │ ├── triageConsole │ ├── doctorWorkstation │ ├── feeWindow │ ├── emergencyWard │ └── statistics ├── components 通用组件 └── utils 请求封装、日期格式化、常量字典API 文件的一个好处是“后端一改前端有据可查”。联调阶段后端修改接口路径或者返回结构时前端只需要改动 api 目录下的对应文件页面代码不需要动。3.2 请求封装是关键Axios 请求封装是整个前端联调好不好的分水岭。医院系统里几乎所有接口都需要带 Token错误处理逻辑又很统一401 跳登录、500 弹出错误提示如果每个页面都写一遍拦截逻辑代码会非常啰嗦。我的 axios 封装基本长这样import axios from axios; import { ElMessage } from element-plus; import router from /router; const request axios.create({ baseURL: /api, timeout: 15000 }); request.interceptors.request.use( config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }, error Promise.reject(error) ); request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } else { ElMessage.error(网络异常请稍后再试); } return Promise.reject(error); } ); export default request;有一个细节baseURL 配成/api同时在后端跨域配置里也匹配/api前缀前端打包后 Nginx 可以把/api反向代理到后端 Java 服务。这样开发环境和线上环境可以共用同一套 axios 代码部署时只需要改 Nginx 配置。3.3 路由权限和页面守卫怎么处理医院急诊系统的用户角色不同能看到的页面也不同。管理员看到的是全部菜单和统计报表护士看到的是分诊台医生看到的是医生工作站收费员看到的是收费窗口。Vue 路由守卫里最简单的权限控制方式是把角色信息存到 Pinia 里然后提前把路由表按角色分组const allowedRoutes { admin: [/dashboard, /triage, /doctor, /fee, /ward, /statistics], nurse: [/triage], doctor: [/doctor], cashier: [/fee] };路由守卫里判断如果目标路径不在当前角色允许的列表内就重定向到登录页或工作台首页。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); return; } if (token to.path /login) { next(/); return; } // 角色权限判断 const userRole useUserStore().role; if (token !allowedRoutes[userRole]?.includes(to.path)) { next(/); return; } next(); });这里我想提醒一个比较容易踩的坑页面路由的名字和菜单路径一定要和后端返回的权限标识严格对齐不要前端写/doctor-workstation后端判断的是/doctorWorkstation两个串不一致权限就永远判断不对。尽量统一用 kebab-case 风格前后端各存一份。3.4 急诊大厅页面的具体实现急诊大厅前端页面核心是候诊队列的实时展示和患者详情联动。我的实现思路是页面加载时请求一次候诊列表然后每 30 秒轮询一次刷新队列顺序。这样处理的原因很简单WebSocket 可以实现真正的实时推送但开发成本和管理成本都高一些对毕业设计或中小型项目来说轮询是性价比最高的方案。30 秒的轮询对数据库压力很小前端体验虽然不能算“毫秒级实时”但护士看队列时也不会觉得卡顿。候诊队列的关键字段是分诊等级我直接用标签颜色来区分el-table :datawaitingQueue stripe el-table-column label急诊号 propemergencyNo width140 / el-table-column label姓名 proppatientName width100 / el-table-column label分诊等级 width100 template #default{ row } el-tag :typelevelType[row.triageLevel] {{ levelText[row.triageLevel] }} /el-tag /template /el-table-column el-table-column label到院时间 propvisitTime width180 / el-table-column label操作 width200 template #default{ row } el-button typeprimary sizesmall clickstartConsult(row) 开始接诊 /el-button /template /el-table-column /el-table患者详情的联动我做了主从表布局。左侧是候诊队列表格右侧是选中患者的完整信息卡片包括生命体征、主诉和分诊评估记录。点击队列里的患者右侧立即请求详情接口并重新渲染。这种交互模式在 Element Plus 里用current-change事件就能实现。还有一个细节值得说前端的时间展示格式需要和后端约定好。后端返回的时间如果是2025-01-10T14:23:00.00008:00这种字符串前端直接渲染会非常难看。我统一在后端 DTO 字段上用了JsonFormat(pattern yyyy-MM-dd HH:mm:ss)前端拿到手就是格式化好的字符串直接展示不用再自己处理。4. 本地启动到部署的完整过程这个部分我重点说环境准备和部署链路因为这是很多初学者最容易卡住的地方。原理其实不复杂但细节一不留神就出错。4.1 开发环境准备后端环境要求很简单JDK 8 以上、Maven 3.6 以上、MySQL 5.7 或 8.0、IDEA 或其他你能顺手用的 IDE。JDK 版本我建议直接上 JDK 8虽然老但兼容性问题最少。如果你用的是 SpringBoot 2.7 左右的版本JDK 8 完全够跑。前端环境要装 Node.js。这里容易出现的版本坑是Vue 3 Vite 5 对 Node 版本要求比较高通常需要 Node 18 以上。如果你本机是 Node 14 或者 16创建 Vite 项目时可能会报错。解决办法有两个要么用 nvm 切换 Node 版本要么把 Vite 版本降到 4 以下。我建议直接用 nvm 管理 Node 版本一个命令就能切换。nvm install 18 nvm use 18 node -v npm -v环境装好后后端先启动确认 8080 端口能访问接口文档或测试接口再启动前端。前后端分离开发时前端开发服务器默认在 5173 端口要通过 Vite 的代理配置把/api转发到后端 8080// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });有了这个代理前端开发时就不需要处理 CORS 跨域问题。但要注意后端仍然要配置 CORS 允许的来源因为部署到生产环境时总会出现直接请求后端域名的情况。4.2 数据库初始化方案数据库初始化我建议用 SQL 脚本 手动执行的方式而不是直接把建表逻辑写死在业务代码里。SQL 脚本按模块拆分成多个文件比如01_create_db.sql、02_create_tables.sql、03_init_dict.sql、04_init_admin.sql。数据字典是容易被忽略的部分但非常重要。分诊等级、患者状态、医嘱类型、收费项目这些字段最好都通过字典表维护而不是在代码里写死魔法数字。比如triage_level字段如果直接存 1、2、3代码里到处都是if(level1)后期改需求时非常痛苦。字典表的做法是t_dict_item id dict_code 字典编码如 TriageLevel item_value 字典值如 1 item_label 字典名称如 普通 sort_order 排序前端可以一次性请求所有字典封装成getDictLabel(dictCode, value)的工具方法全局复用。管理员账号初始化的方式我建议直接在 SQL 里插入一条固定账号密码用 MD5 或 BCrypt 加密后的值。前端登录页提供给用户输入后端登录接口验证账号密码后生成 Token 返回。4.3 前端打包与后端部署前端构建很简单npm run build构建完成后dist目录就是静态文件。部署方案有两种常见选择。第一种是 Nginx 一把梭。把dist里的文件放到 Nginx 的 html 目录下配置一个 server 块并做 API 反向代理server { listen 80; server_name your-hospital-domain.com; root /data/www/hospital-frontend; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里的try_files $uri $uri/ /index.html非常关键因为 Vue Router 在使用 history 模式时刷新某个子页面路径比如/triage时 Nginx 必须把请求回退到 index.html否则刷新就 404。如果你不想配这个也可以让前端路由改用 hash 模式路径里带#新手期用 hash 模式更省事。第二种是后端打包成 jar 之后直接运行静态资源不做前后端物理分离。这适合演示场景但不推荐生产环境。用前后端分离的项目就老老实实分开放后面要加权限、限流、HTTPS、CDN 都方便得多。5. 项目实战中踩过的坑和排坑实录这一部分是我个人觉得最有价值的内容。下面这些坑几乎每个做 SpringBoot Vue 医院系统的人都会碰到有些坑甚至能卡掉你半天时间。5.1 跨域配置与拦截器的叠加问题跨域看起来是 CORS 配置的事但前后端分离项目里经常出现“配置了跨域还是报错”的情况。报错的一个常见原因是我前面说的拦截器把 OPTIONS 预检请求拦截了。浏览器先发 OPTIONSSpringBoot 跨域配置如果匹配到了会正常返回但如果你自定义的拦截器直接拦截了 OPTIONS 并返回 401前端就永远发不出真正的业务请求。CORS 配置类本身比较简单Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }排查思路要记住跨域报错不一定是配置不存在也可能是被其他过滤器拦截。你先看请求头有没有 OPTIONS再检查是不是自定义过滤器或拦截器动了它。5.2 JSON 序列化把 LocalDateTime 转成了数组这个坑很经典。实体类里用LocalDateTime类型Java 默认的 JSON 序列化会把时间转成{year:2025, month:1, day:10...}这样一串奇怪的对象而不是yyyy-MM-dd HH:mm:ss。解决办法有两个方案同时做第一全局配置 Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二在实体的时间字段上加注解最保险JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime visitTime;如果前后端传输时间字符串后页面显示出来还是比实际时间少 8 小时那就是时区没配全。MySQL 连接的serverTimezone、Jackson 的时区、JVM 默认时区三个地方都要一致。5.3 分页插件“失效”的怪现象MyBatis-Plus 的分页插件需要注册配置如果没配你会发现分页接口返回的数据是全部记录page.getTotal()也不生效。正确配置是加一个 MybatisPlusInterceptor BeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }还有另一种情况分页插件配置了但你在 Service 层用LambdaQueryWrapper传了last(LIMIT 10)这样的手动 SQL或者写自定义 SQL 时没把 Page 参数传进去分页也会失效。自定义 SQL 要分页必须这样写IPageStatisticsVO selectStatisticsPage(Page? page, Param(dto) QueryDTO dto);Mapper 的 XML 里不要再手动加LIMITMyBatis-Plus 分页插件会自动拼接。5.4 Token 过期导致页面无限跳转前端 Token 过期后响应拦截器会清除 localStorage 里的 token同时跳转登录页。但如果后端把所有未带 Token 的请求都返回 401并且前端每个页面在路由守卫里发现没有 token 就跳登录这会形成一个循环登录页的初始化也会请求后端接口后端返回 401拦截器又跳转登录页页面一直刷新不停。解决办法是在响应拦截器里加一个条件只处理那些非登录接口的 401if (error.response.status 401 !error.config.url.includes(/login)) { // 清空token跳登录 }同理登录接口本身返回的 401 是用户名密码错误的信息不能直接跳登录页否则用户连错误提示都看不到就被刷回登录页了。5.5 接口字段命名不一致的联调灾难这个问题在前后端联调阶段最让人抓狂。后端返回的字段是patient_name前端取的是patientName后端返回id_card前端用idCard。结果页面数据全是 undefined还不知道错在哪。要彻底解决这个问题统一规则必须在开发之前定好。MyBatis-Plus 默认开启驼峰映射数据库patient_name会转成实体类的patientName再通过 Jackson 序列化变成 JSON 里的patientName。前端只要按实体字段名对接就行。如果你的后端或项目里有人为了省事直接返回了 Map、或者手写了查询结果字段名就很容易出现命名不统一。我的建议是所有返回数据都走 DTO/VO 对象并且强制前端从 IDE 的接口定义或者接口文档中复制字段名不要在页面上手写。这个习惯养成之后联调效率会提升非常明显。5.6 PDF 上传与报告预览部分急诊系统会有上传检查报告、病历附件等需求。如果你用 SpringBoot 的MultipartFile接收文件并直接转存注意限制上传文件大小否则大文件会被 SpringBoot 默认的 1MB 限制拦截。spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB前端预览 PDF 时直接请求后端返回的文件流接口然后利用URL.createObjectURL生成临时地址塞到 iframe 里展示。这里要记得在拿到 Blob 后设置响应类型后端返回时也要指定Content-Type: application/pdf否则浏览器可能会把 PDF 当作文本或二进制下载下来。我做急诊系统时还遇到过一种情况医院电脑上 IE 内核的浏览器版本比较旧URL.createObjectURL不好用。后来统一用新版 Chromium 内核的浏览器或者用 pdf.js 组件库渲染才彻底解决。如果你在做这种项目我强烈建议你预留出一个“演示数据初始化”的入口。前期的排课、模拟挂号数据一定要能一键生成不然你在演示急诊流程时每一步都要手点半天不仅紧张还容易出错。演示数据里要特别注意患者姓名、时间戳要看起来真实年龄不要全是 20 多岁的年轻人急诊科的数据是各个年龄段都有的。另外一个很实际的经验是统计模块不要拖到最后做。急诊工作量统计、分诊等级分布统计这类查询看着很简单但实际上会引入复杂的分组 SQL 和多表 JOIN放到最后赶工时再加会非常仓促。建议在后端模块开发时就把统计接口的骨架搭好至少能有一个按日期查询就诊量的基础版本后面再逐步加维度。最后再分享一个经常被忽略的小事。医院系统的前端页面在挂号、分诊这类操作成功之后一定要有明确、醒目的反馈比如“分诊完成患者已进入急诊医生队列”。医护人员操作节奏快如果看不到明确结果往往会重复点击导致数据重复提交。你可以用 loading 按钮禁用状态加全局提示消息双保险这一条做得好用户对你的系统评价会高出一大截。