ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue智慧医疗挂号系统:从数据建模到号源防超卖的完整实现

SpringBoot+Vue智慧医疗挂号系统:从数据建模到号源防超卖的完整实现 简介基于SpringBootVue的扁鹊智慧医疗系统毕业论文面向计算机相关专业学生及毕业设计开发者针对医疗资源分配不均、就医效率低等问题提供了一套前后端分离的B/S架构智慧医疗平台设计方案。资源为1个doc文件大小2.59MB是完整毕业论文文档不含其他附件。目前已有79人学习/下载。文档包含项目背景与意义、系统架构与技术选型、主要功能模块用户管理、在线预约挂号、电子病历管理、健康数据分析、医生在线咨询等、系统实现与测试、未来展望与结论还附有中英文摘要和关键词。通过阅读该论文可系统了解SpringBootVueMySQL全栈项目从需求分析到部署测试的完整流程借鉴其模块划分、技术栈选择和论文写作结构适合作为同类智慧医疗课题的参考范本。1. 毕设答辩最常被追问的不是算法而是号源并发怎么防超卖这两年看到的 SpringBootVue 毕设里智慧医疗是出现频率很高的题材但真正决定系统拿高分的问题往往不是“你做了几个模块”而是“如果两个患者同时预约最后一个号源你的系统会不会卖超”。扁鹊智慧医疗系统这类项目核心流程其实收敛在挂号、排班、病历记录和患者管理四条线上技术选型也相对固定后端 SpringBoot 提供 RESTful API前端 Vue 做单页应用MySQL 存业务数据Redis 承担缓存和简单分布式锁。这篇博文不套用网上那些把 Controller、Service、Mapper 一层层抄下来的写法而是从数据建模、鉴权拦截、前端路由到部署验证把一条能跑通、能解释、能扛住答辩追问的实现路径讲清楚。适合正在做同类毕设的在校生也适合想快速接手智慧医疗项目骨架的初级开发。2. 先落表和边界智慧医疗的排班、号源与预约记录怎么建模2.1 模块划分一张用户表打天下还是按角色拆表医疗类管理系统的角色通常包含管理员、医生和患者。很多毕业设计会习惯性地拆成user、admin、doctor、patient四张表这种做法在展示上直观但会给权限控制和跨表查询带来很多重复代码。我一般建议保留一张sys_user作为登录认证主表用user_type字段区分角色再关联doctor_profile扩展医生特有的职称、科室、简介等属性。这样 JWT 里只需要放userId和userType后端拦截器就能根据角色决定接口是否放行。模块上智慧医疗系统最少要覆盖系统管理、科室管理、医生排班、在线预约、病历记录。科室与医生是典型的一对多排班与预约是核心的事务链路。把这三组关系先画清楚后端写接口时就不会反复改表。2.2 核心建表脚本科室、医生、排班、预约四张表的关系以下脚本是按 MySQL 8.0 的 InnoDB 引擎写的字符集统一utf8mb4四张核心表可以直接作为起步版本。注意关系字段的注释要写清楚答辩时老师会看表设计。CREATE TABLE dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL COMMENT 科室名称, parent_id BIGINT DEFAULT 0 COMMENT 父科室ID0表示一级科室, intro VARCHAR(500) DEFAULT NULL COMMENT 科室介绍, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除0未删除 1已删除, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 科室表; CREATE TABLE doctor_profile ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 关联sys_user.id, dept_id BIGINT NOT NULL COMMENT 关联dept.id, title VARCHAR(30) DEFAULT NULL COMMENT 职称如主任医师, specialty VARCHAR(200) DEFAULT NULL COMMENT 擅长领域, introduce TEXT COMMENT 个人简介 ) COMMENT 医生扩展信息表; CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 关联doctor_profile.id, dept_id BIGINT NOT NULL COMMENT 冗余科室ID便于按科室检索, schedule_date DATE NOT NULL COMMENT 出诊日期, week_day TINYINT NOT NULL COMMENT 周几1-7, period TINYINT NOT NULL COMMENT 时段1上午 2下午 3晚上, stock INT NOT NULL DEFAULT 0 COMMENT 号源剩余数, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status TINYINT DEFAULT 1 COMMENT 1正常 0停诊, UNIQUE KEY uk_doctor_date_period (doctor_id, schedule_date, period) ) COMMENT 医生排班表;预约表单独列出因为它涉及并发安全。表结构如下CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, appointment_no VARCHAR(32) NOT NULL COMMENT 业务单号YYYYMMDD随机串, schedule_id BIGINT NOT NULL COMMENT 排班ID, patient_id BIGINT NOT NULL COMMENT 患者账号ID, patient_name VARCHAR(30) NOT NULL COMMENT 冗余患者姓名减少连表, visit_date DATE NOT NULL COMMENT 就诊日期, period TINYINT NOT NULL COMMENT 时段与schedule.period对应, status TINYINT DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消, source_type TINYINT DEFAULT 1 COMMENT 1小程序 2窗口 3电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_schedule_patient (schedule_id, patient_id), KEY idx_patient_date (patient_id, visit_date) ) COMMENT 预约挂号记录表;字段里有两个细节值得说明一是patient_name是反规范化设计预约列表展示时不需要每次都连sys_user查姓名这在数据量不大时很划算二是appointment_no单独做成业务单号方便对接叫号、退号等场景不要用自增主键直接对外暴露。source_type字段用来区分预约来源答辩时可以讲“系统支持多渠道预约”这是智慧医疗项目的加分项。2.3 防超卖的三个手段唯一索引、乐观锁、库存条件更新先看一个最容易被问到的场景两个请求同时进入预约接口都读到schedule.stock 1都执行插入最后appointment表里产生两条记录号源就超卖了。仅靠uk_schedule_patient唯一索引能挡住“同一患者重复预约”但挡不住“两个不同患者同时抢最后一个号”。推荐的做法是三层配合。第一层在schedule表增加version字段更新时使用乐观锁UPDATE schedule SET stock stock - 1, version version 1 WHERE id ? AND stock 0 AND version ?;这条 SQL 的返回行数如果为 0说明库存已空或版本冲突业务层直接返回“号源已被抢完”。第二层插入预约记录前先用 Redis 的setIfAbsent做一次分布式锁防止同一排班在极端情况下并发扣减。第三层把“扣库存 插入预约记录”放在同一个事务方法里锁要包住整个事务边界而不是只锁一条更新语句。这样设计之后接口的并发能力不再依赖数据库行锁即使将来用 Nginx 横向扩展多个后端实例逻辑依然成立。3. SpringBoot自动装配、JWT 拦截器和跨域配置一次理清3.1 springboot 自动装配在毕设里的真实体现配置少但要知道去哪找SpringBoot 的自动装配原理在面试里被反复问落到智慧医疗项目里实际看到的效果就是引入mybatis-plus-spring-boot-starter后SqlSessionFactory、DataSource、事务管理器都已经自动配置好了不需要再写applicationContext.xml。但自动装配不等于不用配置至少有三处要手动确认。下面是常用的application.yml配置可直接用于开发环境server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bianque_medical?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root redis: host: localhost port: 6379 database: 0 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 jwt: secret: bianque-medical-secret-change-me expire: 7200 header: Authorizationcontext-path配成/api意味着所有接口都会带/api前缀前端代理只认这一个前缀即可。logic-delete-field: deleted是 MyBatis-Plus 的逻辑删除配置实体类里必须有deleted字段查询时它自动追加deleted 0就不用每个 SQL 手写条件了。log-impl配置会让每条 SQL 打印到控制台开发时方便排查生产环境要关掉。还有一点要注意如果 springboot 版本选得过高网上很多老教程里的配置类可能已经不适用尤其涉及安全框架时WebSecurityConfigurerAdapter在 Spring Boot 3.x 里被移除。做毕设优先选择你熟悉的稳定版本线例如 2.7.x不要盲目追新否则配置排错会占用大量时间。3.2 JWT 拦截器放在 WebMvcConfigurer 里统一处理不用每个接口判断后端接口中医生排班、预约创建、病历查看都需要登录后才能访问。常见的写法是在每个 Controller 方法里取HttpServletRequest的 Header 再解析这样代码重复率高也容易漏判。正确做法是写一个HandlerInterceptor把鉴权逻辑集中处理。Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String secret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求避免跨域OPTIONS请求被拦截 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } String token authHeader.substring(7); // 解析校验签名不对或过期会抛出异常 Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(userType, claims.get(userType)); return true; } }Authorization: Bearer token是前后端分离项目里最常见的认证头格式。拦截器解析成功后把用户 ID 和角色放入 request 属性后续 Controller 用RequestAttribute(userId) Long userId直接取业务代码就不需要再解析一次 Token。注册拦截器时要注意放行白名单Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/auth/login, /auth/register, /dept/list, /schedule/available); } }/dept/list和/schedule/available放行是因为挂号首页通常允许未登录用户浏览科室和余号真正提交预约的接口必须拦截。这里还要注意放行路径的写法是AntPathMatcher规则/dept/**表示匹配该路径下所有子路径不能和/**混为一谈。3.3 跨域 403 的常见原因allowedOriginPatterns 与 allowCredentials 的配合前端开发服务器默认跑在http://localhost:5173后端是http://localhost:8080端口不同就构成了跨域。如果只在 Controller 上加CrossOrigin拦截器又没放行 OPTIONS 请求前端会出现“CORS 错误”和“401”交替出现的现象。统一的 CORS 配置建议放在 WebMvcConfigurer 里Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:5173, http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }这里最容易踩的坑是allowCredentials(true)时allowedOrigins(*)是无效的浏览器会拒绝响应。必须使用allowedOriginPatterns明确列出允许的前端地址。maxAge(3600)表示预检请求的缓存时间单位是秒配好后前端不会每次请求都先发一次 OPTIONS。跨域问题时先看浏览器 Network 面板如果 Preflight 请求返回 403优先检查是不是allowedOriginPatterns没写对如果预检过了但业务请求还是失败则去看 JWT 拦截器有没有把 OPTIONS 请求拦掉。4. Vue 3 工程化路由守卫、axios 封装与 m3u8 影像回放4.1 vue 安装及环境配置npm create 之后先做三件事前端的工程初始化现在推荐直接用 Vite 创建 Vue 3 项目npm create vitelatest medical-web -- --template vue cd medical-web npm install npm install vue-router4 pinia axios element-plus hls.js装完依赖后先做三件事再写页面。一是配置环境变量文件新建.env.developmentVITE_API_BASE/prod-api这个变量名必须以VITE_开头Vite 才会暴露给源码使用。二是配置开发代理在vite.config.js里加一段export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /prod-api: { target: http://localhost:8080/api, changeOrigin: true, rewrite: (path) path.replace(/^\/prod-api/, ) } } } })请求路径/prod-api/dept/list会被转发到http://localhost:8080/api/dept/list生产环境则由 Nginx 做同样转发。三是引入 Element Plus 并注册路由这一步不做后面每个组件都会在main.js里重复 import。4.2 axios 封装把 token 注入和统一异常处理放进拦截器前端所有请求都会走同一个 axios 实例我一般会在src/utils/request.js里封装import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) // 请求拦截器自动附加登录令牌 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理业务码和登录失效 service.interceptors.response.use( response { const { code, data, message } response.data if (code 200) { return data } ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) }, error { if (error.response?.status 401) { localStorage.clear() router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default service后端统一返回{ code, data, message }结构所以响应拦截器直接解构出data业务组件里拿到的就是纯净数据不用每个页面再判断一次。401 处理要放在响应错误分支里因为有的后端会把未登录也包装成 JSON 返回这时业务码判断会先命中更稳妥的判断顺序是先看 HTTP 状态码再看业务码。封装完成后页面里调用request.get(/dept/list)即可代码量和重复劳动会明显减少。4.3 vue 路由参数动态路径与 query 的选择会影响刷新后的表现预约详情页通常需要接收排班 ID比如/schedule/123。路由定义时用动态参数{ path: /schedule/:id, name: ScheduleDetail, component: () import(/views/schedule/ScheduleDetail.vue), meta: { requiresAuth: true, title: 排班详情 } }params方式传参只在页面内跳转时有效刷新后会丢失而query方式会把参数拼在 URL 上刷新后仍保留。我的习惯是详情页主键用params列表页的筛选条件用query。例如从科室列表进入排班列表时把deptId、date放进 query用户刷新后筛选状态不会丢这个细节在答辩演示时很容易被注意到。路由守卫控制登录和角色router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.roles) { const userType localStorage.getItem(userType) if (!to.meta.roles.includes(userType)) { next(/403) return } } next() })meta.roles可以是一个数组比如[DOCTOR, ADMIN]这样医生和管理员共用的页面只需要在路由上声明一次不需要在组件里写大量的if/else判断权限。4.4 vue 播放 m3u8影像回放与健康科普视频智慧医疗系统里医生端经常要回放患者上传的影像资料或播放健康宣教视频。前端播放 m3u8 流最简单的方案是用 hls.jsimport Hls from hls.js export function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoElement) hls.on(Hls.Events.ERROR, (event, data) { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: hls.startLoad() break case Hls.ErrorTypes.MEDIA_ERROR: hls.recoverMediaError() break default: hls.destroy() break } } }) } }Safari 浏览器原生支持 m3u8Hls.isSupported()会返回 false此时直接把视频地址赋给video.src即可。视频文件不要放在前端public目录下应单独放到 Nginx 或云存储避免打包体积过大如果某个视频是加密的记得在后端接口里做好 URL 签名或鉴权拦截器同样要包住视频接口否则很容易被绕过。5. 联调与验收Nginx 部署、Knife4j 复盘和号源并发压测5.1 vue 打包后布局异常History 路由回退与资源路径前端开发时一切正常npm run build后部署到 Nginx访问首页能打开但刷新/schedule/123就 404这是单页应用路由模式导致的。页面内部跳转是通过 Vue Router 的 history API 完成的服务器上并不存在schedule/123这个物理文件刷新时 Nginx 找不到对应资源。解决方法是配置try_files让所有路径都回退到index.htmlserver { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /prod-api/ { proxy_pass http://localhost:8080/api/; 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这一行的含义是先找真实文件找不到就尝试目录再不行就返回index.html把路由交给前端处理。/prod-api反向代理到后端/api注意proxy_pass末尾的/它表示把前缀替换掉。如果打包后发现背景图或 JS 文件 404根因通常是base路径配置不对vite.config.js里设置base: ./可以规避二级路径部署的麻烦。5.2 用 Knife4j 复盘接口用压测脚本给答辩留证据后端 API 文档建议引入 Knife4j它能把 Swagger 页面整理得清爽很多而且支持在线调试。启动项目后访问http://localhost:8080/api/doc.html每个接口的参数、响应结构一目了然。答辩前把所有核心接口在文档里跑一遍截图存档比现场敲代码演示稳妥得多。号源并发压测可以借助 Apache Bench模拟 50 个并发用户同时抢号ab -n 500 -c 50 -p appointment.json -T application/json -H Authorization: Bearer 替换成有效token http://localhost:8080/api/appointmentappointment.json里放预约请求体例如{scheduleId: 1, patientId: 2, visitDate: 2025-06-10, period: 1}。压测后统计响应结果成功数应该与schedule.stock的扣减数一致响应失败或返回“号源不足”的数量等于超出的请求数。再查一次预约记录表的总数确认没有重复数据。这一步既是系统验证也是答辩时最有说服力的数据支撑。压测时建议把 Redis 的database从 0 切换到 3避免压测数据污染本地缓存。压测结束后看日志里的 SQL 执行情况如果出现多条UPDATE schedule ... WHERE stock 0返回 0 的日志而最终数据库库存没有变成负数说明防超卖逻辑生效了。把压测结果和日志保存成文件放进毕设附件演示时直接打开给评委看。本文还有配套的精品资源点击获取
返回列表