体检中心管理系统前后端分离实战:SpringBoot+Vue+状态机设计

发布时间:2026/9/14 9:07:06
体检中心管理系统前后端分离实战:SpringBoot+Vue+状态机设计 简介面向体检中心信息化建设场景这套基于SpringBoot、Vue与Element UI的前后端分离源码适合Java开发人员、医疗信息系统学习者及需要快速搭建体检管理模块的团队参考。系统围绕预约、结果录入与查询等典型业务设计采用Spring Security与JWT保障访问安全前后端分离架构便于并行开发与后续扩展。资源包共1656个文件约13.64MB以Vue组件296个、Java源码285个class与265个java为主配合JavaScript、CSS、SQL脚本及开发文档覆盖前端页面、后端接口与数据库初始化等层次目录结构清晰便于学习和二次开发。内容还包含批量启动脚本及运行配置能帮助读者快速启动项目并理解鉴权流程。目前已有108人学习下载适合希望结合真实项目掌握SpringBoot与Vue整合、RESTful接口设计及权限控制实践的开发者。1. 体检中心信息管理系统的前后端分离难点从来不在框架做过体检业务的人都知道最折磨人的不是登录、不是菜单而是“一个订单里挂了几十个检查项每个项由不同科室录结果最后总检医师还要把异常项汇总成报告”。如果继续沿用以前的单体 JSP 或 PHP 后台方式改数据前后端代码混在一个工程里体检高峰期光是一个“批量录入”页面就能把开发拖垮。所以这一类项目换成 SpringBoot 后端 Vue 前端 Element 组件库的「前后端分离」结构根本原因不是追新而是业务需要后端只出接口前端只关心交互分工清楚、排错简单、也方便以后接小程序或自助机。这篇内容面向正在做类似管理系统的 Java 开发讲清楚这类设计源码里最值得拷贝的三块状态建模、权限接口和科室录入的联动顺便把容易出错的地方都标出来。2. 把体检流程先变成状态机再谈库表设计体检中心信息管理系统接手时第一件事永远不是建表而是把业务流程图铺开预约、到检登记、分检、抽血、各科室录入、总检审核、报告打印。绝大多数系统的混乱都源于“这些环节之间的流转关系没定义清楚”。业务上每个环节都会产生状态变化而状态变化又决定了哪些人能看到哪些界面、能调哪些接口。所以把这层先立住后端接口和前端页面的工作量能省下至少三分之一。2.1 体检单的核心状态流从登记到报告一个体检单也就是 exam_order最稳定的状态模型通常是下面这张表状态值状态含义可流转方向操作角色0已登记待检1前台登记护士1检查中可跨科室0, 2各科室医生、护士2总检中待审核1, 3总检医师3已出报告0, 2总检医师、系统管理员这里最容易被忽略的是“退回”总检医师看到某个项目的描述与结论互相矛盾时必须能把状态重新打回 1而且退回时还要写清退回原因。否则这个单子就会卡死在天上变成“查不到、改不了、打不印”的僵尸单。所以在状态机设计时不要只考虑下行流倒退回退的路径必须和正向路径一样完整这直接决定后面代码里状态合法性校验的判断逻辑。2.2 库表落地登记单、项目明细、结果表分开存个人经验是体检中心信息管理系统的数据库表至少要拆三层第一层是体检档案 customer存姓名、性别、年龄、手机号、既往病史等基础资料 第二层是体检单 exam_order一次到检一条记录存体检日期、所属套餐、总金额、状态、总检结论等 第三层是体检单明细 exam_order_item一个订单对应多条项目每个项目关联科室、检查状态、结果内容。结果表不建议每个科室建一张因为检查项目结构差异太大耳鼻喉科的描述和检验科的数值完全不是一个形态。常规做法是在 exam_order_item 表里设计一个 result_json 字段把结果描述、参考范围、异常标记、小图片路径统一塞进 JSON等出报告时再按项目模板解析展示。这样后端接口的代码几乎不用跟着科室增加而改动。CREATE TABLE exam_order ( id BIGINT PRIMARY KEY COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 体检单号, customer_id BIGINT NOT NULL, package_id BIGINT COMMENT 套餐ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0登记 1检查中 2总检中 3已完成, reviewer_remark VARCHAR(500) COMMENT 总检退回原因, report_path VARCHAR(200) COMMENT 报告PDF路径, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_order_no(order_no) ); CREATE TABLE exam_order_item ( id BIGINT PRIMARY KEY, exam_order_id BIGINT NOT NULL, item_code VARCHAR(32) COMMENT 项目编码, item_name VARCHAR(64) COMMENT 项目名称, dept_code VARCHAR(32) COMMENT 科室编码, status TINYINT DEFAULT 0 COMMENT 0待检 1已采集/已检查 2已出结果, result_json JSON COMMENT 结果内容结构由模板控制, operator_name VARCHAR(32) COMMENT 录入人, check_time DATETIME COMMENT 检查时间, version INT DEFAULT 0, KEY idx_order_id(exam_order_id) );这两张表的关键设计点在于exam_order 的 status 控制整单流转exam_order_item 的 status 只控制项目粒度总检界面必须先把全部明细状态确认成 2才能把整单状态推向“已出报告”。写接口时这个校验必须放在数据库事务里做不能靠前端去判断状态再传参数。2.3 并发录入下的行锁与乐观锁一旦到了上午十点的体检高峰同一个订单的多个科室项目可能同时被不同医生提交。此时最典型的 bug 是总检医师看到的是旧数据或者两个医生同时把同一个项目的结果覆盖掉。预防手段有两个一是对 exam_order 的更新强制走 select ... for update让整个审核动作串行化二是对 exam_order_item 加 version 字段通过乐观锁控制同一条明细不能被后提交的脏数据覆盖int updated examOrderItemMapper.updateByVersion(item); if (updated 0) { throw new BizException(该项目结果已被其他科室更新请刷新后重试); }3. SpringBoot 后端接口分层、JWT 校验与总检事务看这类体检管理系统的源码第一步应该是先看包结构而不是去看 controller 里写了多少行。统一的分层结构能让人一眼定位业务逻辑在哪儿、校验在哪儿、数据库映射在哪儿这比代码注释管用得多。常见做法是把后端拆成 controller、service、mapper、domain、common 五个包common 里放统一返回体、异常、JWT 工具和配置类。3.1 接口分层与核心依赖一个可维护的 SpringBoot 后端工程目录结构大致长这样com.example.pec ├── common │ ├── Result.java │ ├── BizException.java │ └── GlobalExceptionHandler.java ├── config │ ├── SecurityConfig.java │ └── MybatisPlusConfig.java ├── controller │ ├── AuthController.java │ ├── ExamOrderController.java │ └── ReportController.java ├── service │ ├── ExamOrderService.java │ └── impl/ExamOrderServiceImpl.java ├── mapper │ └── ExamOrderMapper.java └── domain ├── entity/ExamOrder.java ├── dto/ReviewRequest.java └── vo/ExamOrderVO.javaSpringBoot 版本选择上建议卡在 2.7.x而不是无脑上 3.x。原因很现实很多企业级系统仍基于 JDK 8 和 javax 包体系SpringBoot 3 切换到 jakarta 命名空间后旧版 MyBatis 插件、代码生成器、运维脚本都可能要跟着改。体检中心这类管理系统的诉求是稳定顶住业务高峰不是做新语法试验田所以 2.7.x 配 MySQL 8.0 是踩坑成本最低的组合。依赖方面预留 Redis 可选项但核心链路并不强制依赖它dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency这套依赖里 spring-boot-starter-security 的主要用途不是做会话管理而是让 Spring Security 的过滤器链接管请求同时关闭默认登录页只保留 JWT 过校验。业务上真正用到 Security 的只有两处密码加密和接口方法级权限注解。3.2 前后端分离下的 JWT 与请求 Token 处理前后端分离之后后端不再维护 HttpSession识别用户身份的方式改为前端把 JWT 放在请求头 Authorization 里后端每次请求先解析并校验签名。这一步做不好最常见的现象就是用户登录后刷新页面又跳回登录页或者接口偶发 401。核心过滤器这样写Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Value(${jwt.secret}) private String secret; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (StringUtils.hasText(header) header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); String userId claims.get(userId, String.class); String roleCode claims.get(roleCode, String.class); // 写入当前请求上下文 UserContext.set(userId, roleCode); } catch (ExpiredJwtException e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录已过期\}); return; } } chain.doFilter(request, response); } }这段过滤器需要关注的参数有三个token 前缀约定为 Bearer、过期时间在生成 token 处统一配置为 8 小时、jwt.secret 必须从配置文件中读取而不能硬编码。企业环境里 secret 超过 32 字符才能保证 HS256 签名强度。另一个容易踩的坑是过滤器放行路径登录接口和静态资源要显式加白名单否则会出现“登录接口自己返回 401”的乌龙。3.3 总检审核的事务边界先锁单再改状态总检是整个系统里事务性最强的接口因为一步要同时写主单状态、更新多个明细状态、记录退回原因任何一步失败都要整体回滚。推荐用 Service 层事务加行锁的方式实现Transactional(rollbackFor Exception.class) public void review(ReviewRequest req) { ExamOrder order examOrderMapper.selectByIdForUpdate(req.getOrderId()); if (order null) { throw new BizException(体检单不存在); } if (order.getStatus() ! 2) { throw new BizException(当前状态不允许总检操作); } if (req.getPassed() examOrderItemMapper.countUnfinished(req.getOrderId()) 0) { throw new BizException(存在未完成的检查项目无法通过总检); } order.setStatus(req.getPassed() ? 3 : 1); order.setReviewerRemark(req.getRemark()); examOrderMapper.updateById(order); }方法上加 Transactional 能保证多表写在同一个事务里selectByIdForUpdate 会锁住 exam_order 表中对应记录防止同事工位的总检医师同时审核同一条单子造成状态互相覆盖。注意 review 接口不接收前端传来的目标状态只接收“通过/退回”目标状态由后端依据当前状态计算否则恶意请求能把一个报告单直接改成任意状态。3.4 统一返回体与错误码约定前端拿到接口响应时最怕“一会儿返回对象、一会儿返回字符串、一会儿直接 HTTP 500”。体检管理系统里最常见的约定是统一用 JSON 包一层{ code: 0, msg: success, data: {} }code 为 0 表示业务成功非 0 为业务失败HTTP 状态码只负责 transport 层语义。比如登录过期时返回 HTTP 401前端 axios 拦截器看到 401 就直接清 localStorage 并跳登录页。业务错误码用 4 位数字规划避免和 HTTP 码混淆。4. Vue 与 Element 前端菜单、Token 与科室录入联动体检中心的前端页面总量不大但交互密度高一个检查项目录入页可能要同时处理“单项保存”“批量标记完成”“异常项弹窗备注”三种动作。前端这部分用 Vue 附带 Element 组件库正好合适Element 的表格、表单校验、日期选择器都是现成的开发速度比纯手写快很多而且前后端分离后前端工程可以独立部署、独立打包不用再跟后端抢同一个 Tomcat。4.1 工程初始化与版本选型新的项目直接用 Vue 3 Vite Element Plus 起步特别是打包速度比 Webpack 快得多。如果项目代码是以前用 Vue 2 写的保留 Element UI 也完全可维护核心差异只在组件引入方式上。这里按 Vue 3 Element Plus 讲解初始化命令如下npm create vitelatest pec-front -- --template vue cd pec-front npm install element-plus axios vue-router pinia安装完 Element Plus 后推荐按需自动导入而不是全量引入否则打包产物会多出几百 KB部署到体检中心那种配置普通的服务器上首屏加载明显变慢。自动导入需要用 unplugin-auto-import 和 unplugin-vue-components配置在 vite.config.js 的 plugins 里。打包后如果发现样式错乱优先检查这两个插件版本与 Vite 版本是否匹配多数情况是版本落差导致的部分组件样式丢失。4.2 axios 封装与 Vue 请求 Token 处理前端每个接口请求都要带上 token不能每写一个接口就手动拼一次请求头。标准做法是把 axios 实例统一封装在拦截器里集中处理 token 注入和 401 跳转import axios from axios; import router from ../router; import { ElMessage } from element-plus; const service axios.create({ baseURL: /api, timeout: 15000 }); service.interceptors.request.use(config { const token localStorage.getItem(pec_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error)); service.interceptors.response.use(response { const res response.data; if (res.code ! 0) { ElMessage.error(res.msg || 操作失败); return Promise.reject(new Error(res.msg)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(pec_token); localStorage.removeItem(pec_user); router.push(/login); } return Promise.reject(error); }); export default service;这个封装里有两个关键点baseURL 设为 /api上线后由 Nginx 反向代理到后端服务避免在代码里写死 IP 和端口响应拦截器里直接返回 res.data业务代码里拿到的就已经是后端 data 字段的内容少一层嵌套。注意业务 code 为 401 时后端返回的 HTTP 状态码也要同步是 401否则拦截器只能靠业务码判断。4.3 路由守卫与按角色过滤菜单体检系统里有护士、医生、总检医师、管理员四种角色不能把所有菜单都渲染给所有人。常见方案是登录接口返回菜单列表 MeunTree前端拿到后动态添加路由router.beforeEach((to, from, next) { const token localStorage.getItem(pec_token); if (!token to.path ! /login) { next(/login); return; } if (token !router.hasRoute(Home)) { const menus JSON.parse(localStorage.getItem(pec_menus) || []); menus.forEach(item { router.addRoute({ path: item.path, name: item.name, component: () import(../views/${item.component}.vue) }); }); } next(); });动态加载组件路径时import 后面必须用模板字符串把完整相对路径拼出来否则 Vite 无法在构建时分析出依赖打包后会报“Failed to resolve component”。初次加载时可以进入 /login 再跳首页让 Vue Router 把动态路由注册完再渲染菜单避免出现首页空白。4.4 科室录入页Element 表格、表单校验与状态联动科室录入是整个系统最常被吐槽的页面因为医生录完一个项目后下一个项目可能还是同一个科室的也可能是别的科室的。页面设计上通常分三个区域左侧待检列表、中间项目表格、右侧异常说明面板。Element 的 el-table 加 selection 列配合 el-form 的 rules 做校验基本能覆盖绝大多数录入场景。表格多选有个非常隐蔽的坑翻页后默认清空勾选导致医生刚标记完第一页项目切到第二页再回来发现全没了。Element Plus 里解决办法是给表格加 row-key 属性并在 selection 列上打开 reserve-selectionel-table :dataitems row-keyid selection-changeonSelectionChange el-table-column typeselection reserve-selection :selectablerow row.status 0 width45 / el-table-column propitemName label项目名称 min-width140 / el-table-column propdeptName label所属科室 min-width100 / el-table-column propresultSummary label检查结果 min-width160 / /el-table这里 selectable 的作用是把已录完结果的项目置灰防止医生重复勾选。批量提交按钮触发时遍历选中行逐个调用保存接口哪个失败就把行号返回定位到表格里而不是整批失败。医生电脑上经常同时开着多个页面保存接口必须返回行 id前端才能精确提示到底是哪一项冲突了避免带着一个模糊错误到处找问题。5. 生产部署与前后端联调验证体检中心这类内网系统部署路径通常很固定后端打 jar 包跑在服务器上前端打包成静态文件丢给 Nginx前端请求 /api 时由 Nginx 转发到后端 8080 端口。开发环境下前后端分离的跨域问题是绕不开的一环Vite 配置代理是最省事的方式server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这个配置的含义是前端请求 /api/login 时Vite 开发服务器把它转发到 localhost:8080/login浏览器里看起来是同源请求不会触发 CORS。生产环境同理Nginx 上做一层 location /api 反代location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } location / { root /opt/pec-front/dist; index index.html; try_files $uri $uri/ /index.html; }两个 location 的配合要点location /api/ 把带 /api 前缀的请求转发到后端且去除前缀location / 里 try_files 归到 index.html否则 Vue Router 用 history 模式时刷新子页面会报 404。打包出来的前端文件如果出现白屏先开浏览器网络面板看 js 和 css 是否 404404 就看 vite.config.js 里的 base 配置设置为相对路径 / 或者服务器部署子路径。部署完成后建议按下面这个清单走一遍完整闭环任何一个环节失败都可以快速定位是后端接口问题还是前端交互问题验证项操作预期结果后端存活curl http://127.0.0.1:8080/api/auth/login返回 JSON 而非连接拒绝登录签发用护士账号调登录接口返回 token且角色为护士前端静态资源浏览器访问 Nginx 首页加载出登录页控制台无红色报错创建体检单前端登记一条新订单后端库出现 exam_order 数据状态为 0总检流转先录完明细再调总检通过接口状态由 2 变为 3报告路径被写入最后有一点值得强调体检管理系统上线后的日常维护量主要来自科室项目模板调整和报告格式修改与前端的页面代码关系不大。因此后端把项目模板配置做成数据库驱动前端将体检表单项做成动态渲染后续业务变化时就能只改配置不动代码。改完配置后记得用一条变更过套餐的测试体检单重新走一遍流转确认状态节点从登记顺畅推进到出报告这套源码才算真正可用。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询