简介:基于SpringBoot+Vue的扁鹊智慧医疗系统毕业论文,面向计算机相关专业学生及毕业设计开发者,针对医疗资源分配不均、就医效率低等问题,提供了一套前后端分离的B/S架构智慧医疗平台设计方案。资源为1个doc文件,大小2.59MB,是完整毕业论文文档,不含其他附件。目前已有79人学习/下载。文档包含项目背景与意义、系统架构与技术选型、主要功能模块(用户管理、在线预约挂号、电子病历管理、健康数据分析、医生在线咨询等)、系统实现与测试、未来展望与结论,还附有中英文摘要和关键词。通过阅读该论文,可系统了解SpringBoot+Vue+MySQL全栈项目从需求分析到部署测试的完整流程,借鉴其模块划分、技术栈选择和论文写作结构,适合作为同类智慧医疗课题的参考范本。
1. 毕设答辩最常被追问的不是算法,而是号源并发怎么防超卖
这两年看到的 SpringBoot+Vue 毕设里,智慧医疗是出现频率很高的题材,但真正决定系统拿高分的问题,往往不是“你做了几个模块”,而是“如果两个患者同时预约最后一个号源,你的系统会不会卖超”。扁鹊智慧医疗系统这类项目,核心流程其实收敛在挂号、排班、病历记录和患者管理四条线上,技术选型也相对固定:后端 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 '父科室ID,0表示一级科室', 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 vite@latest medical-web -- --template vue cd medical-web npm install npm install vue-router@4 pinia axios element-plus hls.js装完依赖后,先做三件事再写页面。一是配置环境变量文件,新建.env.development:
VITE_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.js:
import 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 浏览器原生支持 m3u8,Hls.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.html:
server { 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 的日志,而最终数据库库存没有变成负数,说明防超卖逻辑生效了。把压测结果和日志保存成文件放进毕设附件,演示时直接打开给评委看。
本文还有配套的精品资源,点击获取