SpringBoot+Vue医院预约挂号系统实战:从数据库设计到JWT接口开发

发布时间:2026/10/13 3:49:34
SpringBoot+Vue医院预约挂号系统实战:从数据库设计到JWT接口开发 1. 项目定位与核心业务拆解先把这个项目的底摸清楚。SpringBootVue的文理医院预约挂号系统本质上就是一个非常典型的Java Web全栈项目前台给患者用后台给医院管理员和医生用。市面上类似的“医院预约挂号系统”开源项目很多但大多数要么业务太浅只有简单的增删改查要么代码结构混乱没法拿来直接做毕设答辩。这套项目的价值在于把“患者在线挂号”这条完整业务链路走通了同时兼顾了管理端的数据维护需求。如果你正准备做Java Web方向的毕业设计或者想找一个能写进简历的完整项目练手这套源码值得花时间吃透。它覆盖了SpringBoot后端开发、Vue前端构建、MySQL数据库设计、接口文档编写、前后端联调这些核心环节几乎把Java Web开发的主流技术栈全部串起来了。更关键的是它不是一个“玩具项目”而是奔着能实际使用去设计的所以业务逻辑里有不少值得琢磨的地方。先说清楚这套系统的核心角色和业务主线。整个系统分两种身份患者前台用户和后台管理人员包括管理员和医生。患者这一端主要做四件事注册登录、浏览科室和医生信息、查看医生排班并预约挂号、在个人中心查看预约记录和取消预约。后台管理端则是管理员维护基础数据管理科室、管理医生信息、管理排班、查看所有预约记录医生端可以查看自己被预约的情况。听起来不复杂但真正动手做的时候你会发现业务逻辑的难点不在页面多不多而在数据之间的关系。医生属于某个科室排班需要关联医生预约记录需要关联排班排班需要处理剩余号源。这一串关联关系如果理不清楚代码写到最后就是一团乱麻。这套项目的表结构设计我已经帮你拆解过了逻辑上是很顺的后面会详细讲。2. 技术选型为什么是SpringBoot Vue而不是别的组合2.1 这个选型背后真正的原因先说一句实话对于毕设级别的项目来说SpringBootVue确实是当前最优解没有之一。后端选择SpringBoot是因为它把Spring家族繁重的配置工作全部省掉了。过去用SSM框架光是配置文件就要写好几百行各种XML配置、bean声明、事务配置一套下来没个半天搭不好环境。SpringBoot用自动配置和内嵌Tomcat把这些问题全解决了你只需要一个main方法就能把服务跑起来。对于做毕设的同学来说这意味着可以把更多精力放在业务代码上而不是浪费在环境搭建和框架整合上。前端选择Vue理由更直接它够轻、够快、够简单。Vue的核心思想是数据驱动视图你只管维护数据页面会自动更新。这对于做管理后台这种表单密集型项目来说太合适了。你改一个数据表格自动刷新不用像用原生JS那样手动操作DOM。而且Vue的生态非常成熟配合Element UI组件库一个看起来像模像样的管理后台前端一个熟练的人两天就能搭完。有同学问过我要不要用更重的方案比如Spring Cloud微服务或者前端用React。我的建议是毕设阶段千万不要为了炫技而过度设计。微服务是解决高并发、大规模场景的问题的你一个预约挂号系统单机部署完全够用强行拆成微服务不仅代码量爆炸答辩的时候还会被老师追问“为什么这么设计”。同样的道理React当然也能做但Vue的学习曲线更平缓中文资料更多遇到问题更容易找到解决方案。2.2 项目工程结构的落地实践这套项目的工程结构是我见过的比较规范的毕设级结构前后端完全分离目录职责清晰。后端是标准的Maven工程分包逻辑非常明确com.wenli.hospital ├── controller # 控制层接收前端请求 ├── service # 业务逻辑层处理核心业务 │ └── impl # 业务实现类 ├── mapper # 数据访问层操作MySQL数据库 ├── entity # 实体类对应数据库表结构 ├── config # 配置类如跨域配置、拦截器配置 ├── common # 公共工具类如统一返回结果、JWT工具 └── HospitalApplication.java # SpringBoot启动类前端Vue项目的结构也清晰明了vue-hospital ├── src │ ├── api # 接口请求封装按模块拆分文件 │ ├── assets # 静态资源 │ ├── components # 公共组件 │ ├── router # 路由配置 │ ├── store # Vuex状态管理 │ ├── views # 页面视图 │ │ ├── admin # 后台管理页面 │ │ └── user # 患者端页面 │ ├── App.vue # 根组件 │ └── main.js # 入口文件 ├── package.json └── vue.config.js # Vue CLI配置文件配置开发代理前端通过axios请求后端接口开发环境中通过Vue CLI的proxy配置实现跨域访问生产环境中则通过Nginx反向代理将前后端请求转发到同一个端口。这套架构是目前企业级开发的标准实践你把这个吃透了面试时能聊的东西就很多了。3. 数据库设计与SQL脚本的精心编排3.1 表结构设计是最见功力的地方很多人做毕设恨不得把所有字段塞进一张表这样做开发初期很爽后面全是坑。这套项目的表结构设计我认为是合理的核心表一共有六张用户表患者账号、科室表、医生表、排班表、预约记录表、管理员表。每张表的职责都相对独立表之间通过外键逻辑关联。科室表、医生表、排班表、预约记录表这几张表之间的关系是需要重点理解的。科室和医生是一对多的关系一个科室下有多个医生。医生和排班是一对多的关系一个医生在不同时间段有多条排班记录。排班和预约是一对多的关系一个排班时间段可以被多个患者预约但预约时需要注意号源总数不能超过排班的最大号数。以预约记录表为例它的核心字段设计如下CREATE TABLE appointment ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 预约ID, user_id int(11) NOT NULL COMMENT 患者用户ID, schedule_id int(11) NOT NULL COMMENT 排班ID, appointment_date date NOT NULL COMMENT 预约就诊日期, status tinyint(4) DEFAULT 0 COMMENT 预约状态0已预约1已完成2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_schedule_id (schedule_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT预约挂号记录表;这张表的设计有两个值得注意的点。第一status字段用数字表示状态而不是用字符串这样数据库查询效率更高代码里通过常量或枚举来定义状态含义即可。第二在user_id和schedule_id上建了索引因为这两个字段是查询预约记录的必经条件加索引后大数据量场景下查询速度有明显的提升。排班表的设计也很有讲究。排班不是简单地把日期和时间存进去就完事还要包含号源信息CREATE TABLE schedule ( id int(11) NOT NULL AUTO_INCREMENT, doctor_id int(11) NOT NULL COMMENT 医生ID, schedule_date date NOT NULL COMMENT 出诊日期, start_time varchar(10) DEFAULT NULL COMMENT 开始时间如08:00, end_time varchar(10) DEFAULT NULL COMMENT 结束时间如11:30, total_count int(11) DEFAULT 20 COMMENT 总号源数量, remain_count int(11) DEFAULT 20 COMMENT 剩余号源数量, fee decimal(10,2) DEFAULT 0.00 COMMENT 挂号费, PRIMARY KEY (id), KEY idx_doctor_date (doctor_id, schedule_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;total_count和remain_count这两个字段的设计是关键。预约的时候先检查remain_count是否大于0如果大于0就先扣减剩余号源再插入预约记录。这两个操作必须在同一个事务中完成否则会出现并发问题——多个患者同时抢最后一个号源结果大家都预约成功了号源变成了负数。这个并发问题我在后面“常见问题”章节里会详细讲。3.2 SQL脚本里藏着的初始化数据拿到SQL脚本后你会发现里面不只是建表语句还有大量的初始化数据。这些初始化数据非常关键因为前后端开发联调的时候如果没有数据接口测起来很痛苦。比如科室数据脚本里预置了内科、外科、妇产科、儿科、骨科、眼科、耳鼻喉科、口腔科等常见科室。每个科室下预置了两到三个医生医生姓名都是虚构的每个医生都关联了职称、简介和排班记录。我强烈建议你拿到脚本后先把数据导进本地MySQL跑一遍不要急着改代码。数据能跑起来你的开发环境就通了后面所有的调试工作才能正常进行。另外记得把数据库连接配置改成本地的账号密码SpringBoot的配置文件在application.yml里spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver这里特别提醒一下连接MySQL 8.x版本时驱动类名称必须是com.mysql.cj.jdbc.Driver而不是旧的com.mysql.jdbc.Driver而且数据库URL后面最好加上serverTimezoneAsia/Shanghai不然会因为时区问题报错。4. 后端核心接口与业务逻辑实现4.1 用户注册登录与JWT权限控制预约挂号系统的用户体系不算复杂但有一个核心问题需要解决用户登录后的身份鉴权。这套项目使用的是JWTJSON Web Token这是目前前后端分离项目最主流的方案。JWT的原理可以用一个生活场景来类比你去某个园区保安给你一张临时通行证通行证上写着你的身份信息和有效期之后你在园区里每次进出只要出示通行证保安不需要每次查你的身份证。JWT做的事情也是一样的用户登录成功后后端签发一个Token返回给前端前端把Token保存下来之后每次请求都在HTTP Header里带上这个Token后端通过拦截器校验Token的合法性来判断用户身份。Token的生成和校验在这套系统里封装得非常工整。核心工具类JwtUtil的代码逻辑大致如下public class JwtUtil { private static final String SECRET wenli-hospital-secret-key; private static final long EXPIRE_TIME 7 * 24 * 60 * 60 * 1000; // 7天有效期 // 生成Token public static String createToken(Integer userId, String role) { JwtBuilder builder Jwts.builder() .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET); return builder.compact(); } // 解析Token public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }登录接口的处理逻辑是前端传入用户名和密码后端通过用户名从数据库查出用户记录然后用MD5校验密码是否匹配匹配成功就生成Token返回给前端。这里密码存储用的是MD5加密当然在真实企业项目中这不够安全但作为毕设演示已经足够。如果你想在答辩时显得更专业可以改成BCrypt加密Spring Security自带这个工具学一下也不难。有了JWT之后还需要用拦截器实现权限控制。后端定义了一个LoginInterceptor在preHandle方法中从request的header中取出token调用JwtUtil解析解析成功就放行失败则返回401状态码。同时还要判断角色权限比如访问管理员接口必须是role为admin的用户患者只有资格访问患者端的接口。4.2 排班查询与预约挂号的并发处理排班查询接口的设计值得一说。患者端的核心诉求是我选择一个科室看到这个科室下的所有医生然后选择一个医生看到这个医生未来一段时间的出诊排班并且能看到每个排班还剩余多少号。这套系统的查询接口把这三层数据组装得很完整用到的SQL也是三层嵌套查询SELECT d.id, d.name, d.title, d.introduction, s.id as schedule_id, s.schedule_date, s.start_time, s.end_time, s.remain_count, s.fee FROM doctor d LEFT JOIN schedule s ON d.id s.doctor_id AND s.schedule_date CURDATE() WHERE d.department_id #{departmentId} ORDER BY s.schedule_date这里使用LEFT JOIN而不是INNER JOIN很关键。因为有的医生可能近期没有排班记录如果用INNER JOIN这个医生直接就不会出现在查询结果里患者会觉得医生“消失”了。LEFT JOIN保证了医生一定会展示出来只是排班信息可能为空前端再做了一层空状态处理显示“暂无排班”。预约挂号的接口是这套系统最有技术含量的地方。你以为的预约逻辑可能是简单地插入一条记录实际上的逻辑要严谨得多。核心代码在Service层Override Transactional(rollbackFor Exception.class) public Result createAppointment(Integer userId, Integer scheduleId) { // 1. 校验用户是否存在 User user userMapper.selectById(userId); if (user null) { return Result.error(用户不存在); } // 2. 查询排班信息加锁防止并发问题 Schedule schedule scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule null) { return Result.error(排班不存在); } // 3. 判断剩余号源 if (schedule.getRemainCount() 0) { return Result.error(号源已满请选择其他时间段); } // 4. 检查是否重复预约同一时段 Integer count appointmentMapper.selectCountByUserAndSchedule(userId, scheduleId); if (count 0) { return Result.error(您已预约该时段请勿重复操作); } // 5. 扣减号源并保存预约记录 scheduleMapper.decreaseRemainCount(scheduleId); Appointment appointment new Appointment(); appointment.setUserId(userId); appointment.setScheduleId(scheduleId); appointment.setStatus(0); appointmentMapper.insert(appointment); return Result.success(预约成功); }这里有两个关键的细节。第一个是Transactional注解这个注解保证了整个操作要么全部成功要么全部失败。假设扣减号源成功了但插入预约记录时因为未知异常失败了事务会回滚扣减的号源也会恢复数据不会出现不一致。第二个细节是selectByIdForUpdate这个方法底层的SQL是SELECT ... FOR UPDATE这是一种悲观锁。当两个患者同时点击预约最后一个号源时数据库会保证第一个事务先拿到这行数据的锁第二个事务必须等待第一个事务提交或回滚后才能读取这行数据。这样一来就不会出现两个人都看到还剩一个号源结果都预约成功导致超卖的情况。这个细节虽然小但在答辩时是可以作为亮点讲出来的东西。5. Vue前端构建与用户体验细节5.1 前端页面架构与路由权限设计Vue前端部分的价值不光是页面好看更在于它实现了完整的前后端分离交互模式。整个前端分为两套页面风格患者端采用简洁明快的界面管理端则使用Element UI的典型后台布局。路由设计上采用了动态路由与权限控制结合的方式。患者登录后可以访问首页、科室列表、医生排班、在线预约、个人中心等页面。管理员登录后可以看到科室管理、医生管理、排班管理、预约管理等后台页面。在vue-router的路由配置中每个路由都加了一个meta字段用来标记该路由需要的角色权限{ path: /admin, component: Layout, meta: { role: admin }, children: [ { path: doctor, component: DoctorManage, meta: { role: admin } }, { path: schedule, component: ScheduleManage, meta: { role: admin } } ] }全局前置守卫中判断用户Token和角色信息router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else { if (!token) { next(/login); } else { const role localStorage.getItem(role); if (to.meta.role to.meta.role ! role) { next(/); } else { next(); } } } });5.2 预约页面的交互逻辑与Axios封装预约挂号是前端交互最复杂的页面它的用户旅程是这样的用户点击“预约挂号”进入科室列表页选择一个科室系统跳转到科室详情页展示该科室的医生列表用户选择一个医生进入排班选择页排班按照日期分成多个卡片每个卡片展示上午或下午的号源信息用户选择一个有剩余号源的排班点击“预约”按钮弹出确认窗口确认后系统提示预约成功用户可以在个人中心看到这条预约记录。这个过程的每一步都对应着后端接口的调用。为了代码复用前端把所有接口请求封装在src/api目录下按模块拆分文件。比如department.js文件封装了科室相关的接口调用schedule.js封装了排班相关的接口调用。每个接口函数的定义也很规范import request from ../utils/request; // 获取科室列表 export function getDepartmentList() { return request({ url: /api/department/list, method: get }); } // 根据科室ID获取医生列表 export function getDoctorsByDepartment(departmentId) { return request({ url: /api/doctor/department/ departmentId, method: get }); }这里的request是一个封装好的axios实例它在请求拦截器中统一把Token加到请求头在响应拦截器中统一处理错误状态码。这样做的好处是业务代码里不需要关心Token怎么存、错误怎么处理每行代码只关心业务本身代码看起来干净清爽。实际开发中我见过不少同学的Axios请求遍布各个页面组件到处都在重复写请求头设置那是非常糟糕的代码习惯。个人中心页面的预约记录列表也是一个值得注意的功能点。用户需要看到预约状态已预约、已完成、已取消。如果是已预约状态且就诊日期在当前日期之后用户可以执行取消操作。取消预约的后端逻辑同样要保证数据一致性把预约记录的状态改为已取消同时把对应排班的剩余号源加回去。这样一来被取消的号源可以重新被其他患者预约整个流程是闭环的。6. 接口文档与联调的那些坑6.1 接口文档怎么写的才能让前后端不打架这套项目配套的接口文档我翻过写得算是比较完整的。所有接口都遵循RESTful风格以/api为统一前缀每个接口的说明包含请求方法、请求路径、请求参数、返回结果四个部分。以登录接口为例文档的格式大致是接口功能用户登录 请求方式POST 请求路径/api/user/login 请求参数username用户名、password密码 返回结果{ code: 200, message: 登录成功, data: { token: eyJhbGciOiJIUzI1NiJ..., userId: 1, role: user } }接口文档的价值对于联调来说至关重要。你可以回想一下自己以前做前后端联调是不是有过这样的场景前后端各自开发完接接口的时候发现前端传的参数名和后端接收的参数名不一致或者后端返回的数据结构和前端预期的不一样。这种问题本质上就是缺少一份契约。有了接口文档前后端开发可以完全并行。前端照着文档mock数据开发页面后端按照文档实现接口逻辑最后联调的时候只要保证两者遵循的是同一份文档对接就会非常顺畅。我建议你在做毕设的过程中养成写接口文档的习惯不要偷懒。不用写得多精致但每个接口的入参出参一定要列清楚这既是为了答辩时展示你的工程化能力也是对自己负责。6.2 联调过程中最容易踩的三个坑第一个坑是跨域问题。前后端分离开发时前端跑在8080端口后端跑在8081端口浏览器会因为同源策略阻止跨域请求。这套项目的解决方案是后端加一个CorsConfig配置类通过实现WebMvcConfigurer接口重写addCorsMappings方法实现跨域。实际开发中这个方案很有效配置如下Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CrossRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里需要解释一下为什么要有allowedMethods(OPTIONS)。浏览器在发起跨域请求时如果是非简单请求会先发一个OPTIONS预检请求来确认目标服务器是否允许跨域。如果后端没放行OPTIONS方法浏览器的预检请求就会失败后续的正式请求根本发不出去。第二个坑是时间格式问题。数据库中的datetime类型数据传到前端后默认会被序列化成类似“Apr 26, 2025 8:00:00 PM”的格式非常难以阅读。解决方案是配置统一的JackSon序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个坑是前后端联调时404或者数据异常。这种情况大概率不是后端逻辑错了而是前端请求的路径写错了。比如后端Controller的映射是/api/user/login前端请求写的却是/api/users/login多了个s少了个s接口根本对不上。这个问题的排查思路也很简单打开浏览器的开发者工具查看网络请求的具体URL和状态码基本一眼就能定位问题。7. 常见问题与排错经验速查根据我自己做同类项目踩过的坑再加上测试这套系统时遇到的情况整理一份常见问题清单方便你拿来直接用。现象可能的根因排查思路与解决方式启动SpringBoot服务报数据库连接失败数据库服务没启动或连接账号密码错误先确认MySQL服务是否启动再检查application.yml中url、username、password是否正确前端页面请求接口全部返回404后端服务没启动或前端代理配置错误确认后端启动成功检查vue.config.js中proxy的target端口是否指向后端端口登录时一直提示用户名或密码错误数据库初始化数据中的用户被改动或登录逻辑有误直接在MySQL客户端里查一下用户表确认初始化用户的数据预约时提示号源已满但排班显示有号排班表的remain_count可能被并发事务修改了检查是否在Service层加了事务管理以及号源扣减逻辑是否正确前端页面样式错乱Element UI组件库没有正确引入检查main.js中是否完整引入Element UI的JS和CSSIDEA中运行Maven项目依赖报错本地Maven仓库缺少依赖包Maven - Reimport重新导入依赖确认网络可访问Maven中央仓库排错这件事本质上是一个“缩小问题域”的过程。先判断是哪种类型的错误再逐层排查。比如页面显示异常先去浏览器开发者工具的Network面板看网络请求是否正常再去看后端控制台或日志输出一层一层往下追基本没有解决不了的问题。8. 项目答辩时的加分技巧与扩展思路这套系统做完整后答辩环节有几个点可以着重讲既有技术深度又容易引起老师共鸣。第一个加分点是并发控制。预约抢号这个场景天然适合聊并发问题你可以讲清楚为什么会出现超卖问题以及你用乐观锁还是悲观锁来规避。老师比较喜欢听这种有实际场景背书的方案选择。第二个加分点是接口文档和工程化意识。很多毕设项目只把代码跑通就完事了根本没有什么接口文档的概念。你拿出这一套完整的接口文档配合RESTful风格的API设计老师会觉得你有工程规范意识这在本科阶段是比较难得的。第三个加分点是异常处理和统一返回结果。这套项目封装了统一的Result对象所有接口返回格式都是code、message、data三段式。前端通过判断code是否为200来确定接口是否成功统一了错误处理逻辑。这种设计在企业开发中是标配在毕设中却是少数。至于扩展方向有三条路可以考虑。第一条是引入Redis缓存热点科室和排班数据减少数据库查询压力第二条是加入验证码功能包括图形验证码和滑块验证码提高系统的安全性可以在答辩时专门讲一讲验证码怎么生成、怎么校验第三条是增加手机短信通知功能预约成功或取消时给患者发送消息提醒。这些扩展任何一个做出来都能让项目的复杂度上一个大台阶。这个项目做下来我个人的体会是毕设的价值不在于功能多炫而在于把每一个功能点做扎实、讲清楚。把预约挂号的完整闭环跑通把前后端分离的协作模式理顺把并发和数据一致性这两个核心问题弄明白你收获的东西比单纯“完成一个毕设”要多得多。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询