ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的社区医疗预约挂号平台设计与实现

基于SpringBoot+Vue的社区医疗预约挂号平台设计与实现 你这套“javavue基于springboot框架的社区医疗预约挂号平台”一看就是最近两年毕业设计和我那个小工作室接私单时的高频选题。我前前后后带人做过五六套类似的系统自己也完整从零搭过一套给社区卫生服务中心做演示用对这个题目的坑和关键节点算是相当熟。这篇就把我的真实搭建思路、技术选型逻辑、数据库设计、前后端核心实现以及最容易翻车的几个环节全部摊开写清楚。不管你是准备拿它做毕设还是想往简历里塞一个完整项目照着这个思路走能省很多“踩坑重来”的时间。先说结论这个题目看起来是“管理系统”那一路实际上一旦加入“预约”和“排班”它就变成了一个带有并发写操作、状态流转和定时任务的中等复杂度业务系统。如果你只会CRUD能跑通页面但经不起追问如果能把号源扣减、订单超时释放、医生排班规则讲明白面试官或答辩老师基本就会点头了。1. 项目定位与整体设计思路1.1 这个平台到底解决什么问题社区医疗预约挂号核心痛点有三个。第一是“信息不透明”患者不知道社区医院今天有哪些科室开诊、哪个医生在班、号源还剩多少到了现场才发现挂不上或白跑一趟。第二是“排队低效”传统窗口挂号在早高峰经常排长队一个感冒发烧也要耗半个小时。第三是“管理粗放”社区医院本身人手紧科室排班、号源统计、爽约记录如果用Excel维护基本没法追溯。所以这个平台要做的不是把线下窗口搬到线上那么简单而是把“科室—医生—排班—号源—预约订单—就诊记录”整条链路数字化。患者端能查科室、看排班、抢号源、查订单医生端能看自己某天的接诊列表、标记就诊状态管理员端维护科室、医生、排班规则并处理预约异常。这个划分决定了后端表结构和接口设计的大方向。1.2 功能模块怎么切才合理我见过很多第一次做这种系统的人上来就把菜单列了一大堆什么公告管理、资讯管理、问卷管理全塞进去最后光是权限控制就把自己绕晕。真正核心的模块就四块基础数据管理科室、医生、排班规则。这属于“源头数据”没有它们预约无从谈起。预约挂号核心链路患者选科室→选医生→选日期时段→确认挂号→生成订单或支付→就诊时核销。用户体系患者注册登录、个人信息维护医生登录、查看排班和就诊列表。后台统计与异常处理号源使用情况统计、爽约记录、订单超时释放、退号处理。其余像健康资讯、留言反馈属于锦上添花建议放到第二期再做除非你论文里确实需要凑一个“系统扩展性”的章节。核心链路跑通、能演示、能讲清楚设计原因比功能堆砌重要得多。1.3 为什么用 Java SpringBoot Vue“为什么选这套技术栈”是答辩和面试必问题。合理的回答不是“因为大家都在用”而是从场景匹配度讲。社区医疗预约挂号是典型的“中后台管理系统 移动端适配的H5页面”混合体。后端需要处理复杂的业务规则排班冲突、号源扣减、状态流转和并发请求多人同时挂同一个号Java SpringBoot在这类场景里生态成熟MyBatis-Plus、Spring Validation、Spring Task这些组件拿来即用出问题网上一搜一大把解决方案。前端选Vue也很自然。它的组件化开发方式适合把科室卡片、排班表格、订单列表拆成独立组件响应式数据绑定让“选中日期→刷新可用号源→点击预约”这类交互写起来非常顺手。如果只想要后台管理Vue本身也能快速搭出可维护的单页应用。再加上市面上Vue的中文资料和组件库Element Plus、Vant非常全新手自己调样式也不至于卡死。这套组合的本质是“稳定后端 灵活前端”业务逻辑和界面呈现解耦这也是它能成为主流全栈组合的原因。2. 技术选型与关键工具解析2.1 SpringBoot 版本怎么选不踩坑这里必须先说一个热词检索里反复出现的问题SpringBoot版本太高导致项目起不来。我用IDEA创建项目时默认拉的是3.x结果不少教程里还在用2.x的写法配置类、javax命名空间对不上跑起来一堆ClassNotFoundException。我的建议很直接如果跟着教程走或者主要目标是稳定跑通毕设选 SpringBoot 2.7.x 系列。它是2.x的最终维护版本稳定、教程多、坑基本都被踩平了。如果一定要用3.x记得Java版本必须17以上并且所有依赖尤其MyBatis Plus、Druid、Lombok都要选适配3.x的版本否则compatibility问题能折腾你一整天。2.2 MySQL 与 ORM 框架怎么配数据库我用的 MySQL 5.7 / 8.0 都可以社区场景数据量不大不需要上复杂的分布式中间件。字符集一定要在建库时指定 utf8mb4否则患者姓名里有生僻字或者备注里有表情符号存进去直接乱码。ORM我推荐 MyBatis-Plus而不是纯 MyBatis 或 JPA。原因是这个项目里有大量“单表简单CRUD 少量多表联查”的场景MyBatis-Plus 的 BaseMapper 直接把单表操作封装好了代码量能砍一半。多表联查比如查某个医生的排班时连带科室名称自己写XML注解SQL就行完全够用。JPA在复杂动态条件查询时包装得有点绕对Java新手并不友好。2.3 前端工程从哪起步Vue这块如果你负责的是给患者用的H5/Web端建议直接用 Vue3 Vite Element PlusPC后台或 Vant移动端。Vite启动速度快热更新体验比Webpack时代的Vue CLI舒服太多。如果是第一次搭环境卡住了记住几个关键点Node.js 版本建议 16 以上Vite 5 以上需要 Node 18。国内网络环境用 npm 装依赖可以把 registry 切到淘宝镜像避免卡在 node-sass 这类二进制包下载上。这是网上教程里反复出现的痛点我建议开局就配好npm config set registry https://registry.npmmirror.comVue 的目录结构我习惯这样组织src/ api/ // 按模块封装的请求方法 assets/ // 静态资源 components/ // 通用组件 router/ // 路由配置 stores/ // 状态管理Pinia views/ // 页面组件 utils/ // 工具函数axios封装、日期处理等2.4 其他值得提前引入的组件身份认证用 Sa-Token 或 JJWT 生成 token。毕设阶段没必要引入 Spring Security OAuth2那套东西光过滤器链就能劝退一半人。Sa-Token 的中文文档简洁登录、鉴权、退出几行代码搞定。定时任务Spring 自带的 Scheduled 注解就够用用来做“超时未支付自动取消订单并释放号源”这类操作不要为了这个引入 Quartz。接口文档引入 knife4jSwagger增强版写完接口自动生成文档答辩时演示“我每个接口都有文档”会非常加分。参数校验Spring Validation 的 Validated NotNull 这类注解必须用上避免后端入口被脏数据打穿。3. 数据库设计与核心表结构3.1 核心表有哪些这套系统的表结构我拆成六张核心表和两张辅助表。user 用户表患者和医生可以放一起用 role 字段区分也可以拆成 patient 和 doctor 两张表。我建议合并为一张 sys_user简化登录逻辑。department 科室表科室名称、位置、简介、状态。doctor 医生表所属科室ID、姓名、职称、简介、头像。如果医生也登录系统就冗余一个 user_id 关联到用户表。schedule 排班表医生ID、排班日期、午别上午/下午、开始时间、结束时间、总号源数、剩余号源数、排班状态。appointment 预约订单表患者ID、排班ID、预约日期、时段、状态已预约/已取消/已完成/已爽约、创建时间、取消时间、就诊序号。department_rule 排班规则表可选存“每个医生每周几固定出诊、出诊是上午还是下午、放多少号”这类配置用于自动生成排班。辅助表包括 operation_log 操作日志表和 announcement 公告表根据你的扩展需求决定动不动。3.2 表关系与关键字段说明关系其实很好理一个科室下有多个医生department 1 - N doctor一个医生有多个排班doctor 1 - N schedule一个排班对应多个预约订单schedule 1 - N appointment最关键的字段设计在 appointment 表里唯一索引UNIQUE KEYuk_schedule_patient(schedule_id,user_id)确保同一个患者不能重复预约同一个排班。这个索引在并发场景下的意义我在后面章节展开讲。状态字段用 tinyint 表示我给这套系统的约定是 0待就诊 1已完成 2已取消 3爽约。比字符串好用省存储且查询快。就诊序号这个字段容易被忽略。用户预约成功后需要知道自己排在第几个。它不应该是自增主键而应该取“当前排班下已成功预约数1”。这个计算要在扣除号源的事务里同步完成。3.3 排班设计要提前想清楚排班是预约系统最容易设计混乱的地方。简单做法是管理员在后台“新增排班”页面选择医生、日期、时段、放号数插入一条 schedule 记录。前端首页展示医生时根据日期去查他的 schedule有记录就显示可约没有就显示“未排班”。稍微进阶一点的做法是支持“每周规则排班”管理员配置一次规则系统按规则自动生成未来七天的排班数据。这个功能工作量稍大但写进论文里非常出彩能体现你对业务建模的理解。我当时给社区医院做演示版时用的是规则半自动化管理员配置规则后系统每天凌晨跑一个定时任务生成未来三天的排班。如果某天医生休假管理员手工删除当天排班即可。这样既有自动化又不至于把逻辑搞得过于复杂。4. 后端核心功能与接口实现4.1 统一返回体与异常处理先讲后端基础建设这部分是新手最容易忽略、却最能体现工程素养的地方。定义一个统一返回体Data public class ResultT { private Integer code; // 200成功500失败401未登录 private String message; private T data; }所有接口都返回 Result前端 axios 拦截器统一判断 code。配合 RestControllerAdvice 全局异常处理器把业务异常比如“号源不足”“请勿重复预约”通过自定义 BusinessException 抛出代码会干净很多。这么做的好处是你不需要在每个 controller 里写一堆 try-catch前端也不需要每个接口单独处理错误。这也是实际企业项目的基本要求。4.2 JWT 登录与权限控制的落地登录流程用户输入手机号密码或验证码→ 后端校验 → 生成 JWT token → 前端存储到 localStorage 或 Pinia。后续每个请求在请求头带 Authorization: Bearer token。Java 端的核心逻辑用 jjwt 库写大概是这个意思String token Jwts.builder() .setSubject(userId.toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();我一般会把“解析token、获取当前登录用户”的逻辑封装成拦截器或 Sa-Token 的拦截器在进入 controller 前就完成身份注入。所有需要登录的接口在 controller 里直接通过参数或 ThreadLocal 拿到当前用户ID不要再让前端把用户ID传过来——否则任何人改个参数就能操作别人的数据这是一个非常严重的越权漏洞答辩时被问到就尴尬了。4.3 预约挂号的并发控制是重头戏预约的核心动作用户选好排班点击“确认预约”后端做两件事——扣减剩余号源、生成预约订单。高并发场景下如果两个用户同时请求都读到剩余号源是1都判断“还能挂”然后都执行插入就会超卖。社区医院流量不算大但这个问题必须处理因为它是系统设计和并发控制能力的直接体现。我推荐用“乐观锁 数据库唯一索引”双保险乐观锁实现在 schedule 表加一个 version 字段或者直接利用剩余号源字段做条件更新int updated scheduleMapper.update( UPDATE schedule SET remaining remaining - 1, version version 1 WHERE id ? AND remaining 0 AND version ?, scheduleId, currentVersion); if (updated 0) { throw new BusinessException(号源已被抢完请选择其他时段); }这个 SQL 的含义是只有在你读取排班之后、执行更新之前排班没有被别人改过version 没变并且还有剩余号源时更新才成功。affected rows 为0就说明竞争失败了直接返回“手慢了”。唯一索引兜底在 appointment 表对 (schedule_id, user_id) 建唯一索引即使乐观锁判断出了意外比如事务隔离级别设置不当数据库层面也能拒绝重复预约。再加上把扣减剩余号源和生成订单放在同一个事务里Transactional(rollbackFor Exception.class) public Appointment createAppointment(Long scheduleId, Long userId) { // 1. 查排班校验时间是否还在可约范围内 // 2. 乐观锁扣号源 // 3. 生成订单就诊序号 当前已预约数 1 // 4. 返回订单 }这套方案足够应对社区医院的真实压力同一时刻并发量几十就很夸张了。面试官如果问“为什么不用 Redis 分布式锁”你可以回答在单机应用 MySQL 场景下乐观锁已经足够引入 Redis 反而增加部署复杂度。这种回答体现的是“能根据业务量级选择合适方案”的能力比盲目堆技术更受认可。4.4 超时未支付与号源释放如果挂号不收钱只是预约那“超时取消”可以不做。但稍微真实一点的场景是有挂号费用户下单后几分钟内需支付否则订单取消、号源释放回池子。用 Spring 的 Scheduled 就能实现定时扫描Component public class AppointmentTimeoutTask { Scheduled(fixedRate 60000) // 每分钟执行一次 public void cancelTimeoutOrders() { // 查出 status待支付 且 create_time now - 15min 的订单 // 对每个订单将订单状态改为已取消同时把对应 schedule 的 remaining 1 // 注意这两个操作也要放在事务里并且取消前再查一次订单状态避免重复释放 } }注意这里的“重复释放”问题定时任务可能没有执行完下一次触发又来了或者刚好用户同时点了取消。所以任务里必须加状态条件更新UPDATE appointment SET status2 WHERE id? AND status0affected rows 为0则说明订单已被处理跳过。5. 前端 Vue 核心实现与页面流程5.1 路由与页面结构前端页面按角色拆分患者端首页科室/医生展示、排班详情页、我的预约页、登录注册页。医生端今日就诊列表页、预约记录页。管理端用户管理、科室管理、医生管理、排班管理、预约管理、统计页。路由我用懒加载方式引入const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /doctor/:id, component: () import(/views/DoctorSchedule.vue) }, { path: /appointments, component: () import(/views/MyAppointments.vue), meta: { requiresAuth: true } } ]路由守卫里检查 token 和角色没登录就跳登录页。这是前端权限控制的基础。5.2 axios 封装与请求拦截axios 我建议封装成一个模块而不是每个页面直接调// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, 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 res response.data if (res.code ! 200) { ElMessage.error(res.message) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )统一处理的好处后端返回401时自动踢回登录页业务错误统一弹提示。新写一个页面时只需要引入封装好的 service 实例不用关心这些横切逻辑。5.3 核心交互选医生→看排班→确认预约这是患者端最重要的交互流程。我拆成三步第一步科室列表页。从后端拿科室数据展示成卡片。点击某个科室跳转到该科室的医生列表页。第二步医生列表页。每个医生卡片展示姓名、职称、简介并带一个“预约”按钮。点击后跳转到排班页。第三步排班页。这是最核心的页面。布局是顶部显示医生信息下面是一个按日期排列的表格比如“今天、明天、后天”每个日期下面再分上午/下午两个时段每个时段显示“剩余号源数”号源为0或已过当天的时段置灰并禁用按钮。这里实现一个关键交互点击一个时段的“预约”弹出确认框里面展示医生、日期时间、挂号费、就诊序号生成规则用户确认后调用 createAppointment 接口。成功后跳转到“我的预约”页面。前端代码不复杂但状态管理要注意用户切换日期时要重新请求该医生的排班数据预约成功后要把当前时段的剩余号数减1并更新按钮状态不要等用户手动刷新才看到变化。5.4 vite 开发时的跨域处理本地开发时前端跑在 5173 端口后端跑在 8080 端口直接请求会跨域。在 vite.config.js 里配置代理export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样前端代码里所有请求都写 /api 开头开发环境由 Vite 代理转发生产环境交给 Nginx 做同样的转发。前后端分离项目的跨域问题基本就是这么解决的。6. 关键接口设计示例6.1 获取医生排班列表接口GetMapping(/schedule/list) public ResultListScheduleVO getScheduleList( RequestParam Long doctorId, RequestParam String startDate, RequestParam String endDate) { // 查询某个医生在日期范围内的排班带出剩余号源和状态 return Result.success(scheduleService.getDoctorSchedules(doctorId, startDate, endDate)); }返回数据里我建议不止返回 schedule 原始字段还加工一个可预约标识日期当天已过时段 → canBook false, reason 已过号剩余号源 0 → canBook false, reason 约满时间合法且剩余号源 0 → canBook true这个加工放在后端做前端展示就非常简单稍微判断 canBook 字段就能决定按钮可不可点。这是“后端多算一点前端少错一点”的典型例子。6.2 提交预约接口PostMapping(/appointment) public ResultAppointmentVO create(RequestBody CreateAppointmentRequest request) { Long userId StpUtil.getLoginIdAsLong(); return Result.success(appointmentService.createAppointment(request.getScheduleId(), userId)); }注意用户ID从登录态取前端不需要传。这个设计不仅仅是为了安全也简化了前端逻辑。7. 常见问题与排查技巧实录7.1 SpringBoot 项目启动失败端口被占用这个问题出现的频率极高尤其是之前跑过其他项目的机器。报错信息一般是Web server failed to start. Port 8080 was already in use.排查方法Windows 用netstat -ano | findstr :8080Linux/macOS 用lsof -i:8080查出占用进程的PID结束掉就行。如果是 IDEA 里上次运行的后端进程没退出直接在 IDEA 的 Services 面板里停掉即可。7.2 MyBatis-Plus 和 SpringBoot 版本不兼容现象启动后提示Invalid bound statement或者 Bean 创建异常。原因MyBatis-Plus 有多个适配 SpringBoot2 和 SpringBoot3 的分支版本。用 SpringBoot 3.x 时必须引入mybatis-plus-spring-boot3-starter而不是老的mybatis-plus-boot-starter。这个坑真的能卡住新人半天我遇到过好几个人来问。建议如果你的 SpringBoot 是 2.x用dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependencySpringBoot 3.x 则改成mybatis-plus-spring-boot3-starter。7.3 前端依赖装不上node-sass 报错Vue2 老项目里 node-sass 是重灾区现在的 Vue3 Vite 项目基本用不到它了。如果你还是在网上复制了旧项目模板遇到 node-sass 编译失败最快的方案是换用sass(Dart Sass) 替代或者在项目里把 node-sass 相关代码清掉。另外npm 安装速度极慢或卡住多半是网络问题。直接配淘宝镜像或者用 cnpm、pnpm 都行。我个人习惯用 pnpm安装快且节省磁盘空间。7.4 跨域请求被拦截现象前端请求能发出去但浏览器 console 报CORS policy: No Access-Control-Allow-Origin header。原因开发环境没有配代理或后端没开跨域配置。前端方案用 5.4 节的 Vite proxy 转发这是最优解。后端方案仅用于临时测试加一个CrossOrigin注解或实现 WebMvcConfigurer 的 addCorsMappings 方法。生产环境通常用 Nginx 反代解决不建议每个后端接口都放开跨域。7.5 重复点击预约按钮生成了多条订单前端没有做防重复提交用户手一抖连点两下结果系统里出现两条预约订单。解决分三层前端点击后立即置灰按钮接口返回前禁止再次点击。后端利用数据库唯一索引同一个人对同一排班只能有一条有效订单插入第二条直接报 DuplicateKeyException然后捕获并转为业务异常“您已预约该时段”。业务在 service 层先查一遍“是否存在未取消的预约记录”提前拦截。这三层都做了基本可以保证数据不乱。这也是我在实操中强烈建议后端一定要有唯一索引的原因前端防不住手抖和恶意请求。7.6 排班日期展示出现时区问题你可能会遇到前端选择的日期传过来后端存进数据库后发现日期少了一天。这通常不是代码写错而是 Jackson 的日期序列化时区与 MySQL 连接时区不一致导致的。解决办法在 application.yml 里统一时区配置spring: jackson: time-zone: GMT8 date-format: yyyy-MM-dd HH:mm:ss datasource: url: jdbc:mysql://localhost:3306/community_hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai前后端统一用 Asia/Shanghai 时区日期字段只传年月日字符串尽量避免 Date 类型的时间戳混用能省很多麻烦。8. 部署上线与扩展建议8.1 本地演示的部署组合毕设答辩或本地演示一套最简单的部署方式后端打包成 jarmvn clean package -DskipTests然后java -jar xxx.jar运行。前端执行npm run build产物在 dist 目录。本地测试可以直接用 Vite preview 预览如果要模拟真实部署用 Nginx 把 dist 作为静态资源目录并把 /api 反向代理到后端 8080 端口。Nginx 的关键配置server { listen 80; server_name localhost; root /opt/community-hospital/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }那个try_files ... /index.html一定要加否则刷新 vue-router 的 history 模式路由时会 404。这是前后端分离部署最常见的坑之一。8.2 这个项目还能怎么扩展如果你做完核心功能还觉得不过瘾或者想和同组人拉开差距可以在这些方向上加分引入 Redis 做号源预热和高频数据的缓存减少数据库压力。增加微信小程序端保留用户习惯的微信授权登录方式。引入在线支付微信支付/支付宝沙箱把支付状态和订单状态串起来。给医生端增加简单的电子病历填写功能让“预约挂号”延伸为“预约-就诊-病历”闭环。用 ECharts 做管理员端的科室挂号量趋势图、医生工作量统计图这是多个热词里明显被反复搜索的内容也确实是展示“全栈能力”的加分项。我个人在实际操作中的体会是很多做这套题目的同学前期把大量时间花在了界面美化上反而没想清楚排班和号源这两个核心数据模型。界面丑一点没关系答辩时老师更在意的是你能不能解释清楚“预约流程里数据库到底发生了什么变化”。你把并发控制、状态流转、超时释放这几条讲明白了这个项目的技术含金量就已经超越大部分同类毕设了。最后再分享一个小技巧写论文或项目文档时把“系统运行流程图”“数据库ER图”“接口权限表”这三张图画好比你贴几十页代码有用得多。图一贴老师对你的整体设计能力就有数了。这套项目做完你简历上“独立开发社区医疗预约挂号平台涵盖前后端完整链路”这句话是经得起追问的。
返回列表