每年毕业设计选题的时候,总能看到一批“网上挂号就诊系统”出现在Java方向的备选清单里。说实话,这个题目的热度一直居高不下,核心原因就一条:业务场景足够真实,技术点足够全面,难度又刚好卡在一个能独立完成、又不会因为太简单被人说水的区间里。今天要拆的这个达明医院网上挂号就诊系统,就是一个标准的SpringBoot + Vue前后端分离项目,包含了用户注册登录、科室医生查询、在线预约挂号、挂号记录管理、后台排班维护等完整模块。这篇文章会把整个项目的设计思路、核心实现和踩坑过程掰开揉碎讲一遍,给正在做毕设或者准备做同类项目的同学一份能直接照抄的参考答案。
我得先说明白,这套系统不是什么高并发的互联网大厂架构,就是计算机毕设里常见的中小型业务系统。但正因为它的功能边界清晰、数据表关联合理,反而很适合拿来练手和答辩展示。你做完之后,既能讲清楚SpringBoot的后端接口设计和MyBatis-Plus的持久层操作,也能说明白Vue前端组件化开发的思路,这些恰恰是答辩评委最爱问的点。下面我从项目定位开始,一层层往里拆。
1. 项目定位与技术选型:为什么是SpringBoot + Vue
1.1 这个题目到底在考什么
网上挂号就诊系统,往大了说叫“互联网医疗”,往小了说就是一套典型的预约业务系统。它的核心业务链路很清晰:用户登录后选择科室,看医生的排班信息,选择合适的时间段提交挂号,后台生成挂号记录,用户可以在个人中心看到自己的预约历史。这套链路拆开之后,几乎覆盖了JavaWeb开发的全部基础知识点:用户鉴权、数据表关联查询、事务处理、状态流转、接口设计、前端交互。
选这个题目的聪明之处在于,它不是那种“空调泛泛”的增删改查系统。挂号的业务逻辑里有一个非常经典的并发问题——号源扣减。两个用户同时抢同一个医生的最后一个号,怎么保证不超挂?这个问题一抛出来,整个项目的技术含金量就上去了。哪怕你只是用数据库乐观锁解决,也足够在答辩时讲上十分钟。
所以说,选题目别只看“是不是太简单”,要看它能不能让你把学到的东西串起来。达明医院这个项目正好踩在了这个平衡点上:功能不复杂到失控,技术点不简单到无话可说。附带的源码里把基础代码都铺好了,更适合在此基础上做二次理解和扩展。
1.2 技术栈选型的现实逻辑
现在Java方向的毕设,SpringBoot + Vue基本是事实上的标准答案。原因很直白:一是资料多,遇到问题搜一下就能找到方案;二是企业里用得广,哪怕毕业后做其他方向,这套技术栈写在简历上也不丢人;三是Vue框架做出的界面效果明显好于传统的JSP模板渲染,答辩演示的时候观感好很多。
具体到这套系统,后端我建议用SpringBoot 2.7.x搭配MyBatis-Plus,原因有三个。第一,SpringBoot的自动配置和内嵌Tomcat能省掉大量环境配置时间,不用像SSM那样手工写一堆XML配置。第二,MyBatis-Plus把单表CRUD都封装好了,你只需要定义实体类和Mapper接口,基础的增删改查就不用手写SQL了,能把精力放在核心业务逻辑上。第三,它的分页插件对列表页是刚需,尤其后台管理的医生列表和挂号记录列表,数据一多就必须分页。
前端选Vue的话,直接搭配Element UI或者Element Plus组件库。这个组合能让你在半天之内把页面骨架搭出来,表格、表单、弹窗、消息提示这些组件都是现成的,不需要自己造轮子。数据库就用MySQL 8.0,本地安装一个Navicat或者DataGrip做可视化操作就行。别小看这套组合,它对应的是互联网公司里最常见的CRUD业务开发场景,做完这个项目,你去实习的时候接需求不会发怵。
提示:如果你在配置环境时遇到Maven依赖下载慢的问题,可以把镜像源换成阿里云的maven仓库,在settings.xml里加上mirror配置,速度能快好几倍。
2. 数据库设计:核心表结构与字段逻辑
2.1 五张核心表的关系梳理
网上挂号系统的数据结构并不复杂,但表与表之间的关系一定要理清楚。我见过不少同学一上来就建十几张表,结果关联查询把自己绕晕了。达明医院项目里只需要五张核心表就能跑通整个业务:用户表、科室表、医生表、排班表、挂号记录表。
| 表名 | 说明 | 核心字段 |
|---|---|---|
| user | 用户表 | id, username, password, real_name, phone, role |
| department | 科室表 | id, dept_code, dept_name, description |
| doctor | 医生表 | id, doctor_name, dept_id, title, intro |
| schedule | 排班表 | id, doctor_id, dept_id, work_date, time_slot, total_count, remain_count, status |
| appointment | 挂号记录表 | id, user_id, schedule_id, doctor_id, dept_id, work_date, time_slot, status, create_time |
这五张表的关联关系很直观:一个科室下有多个医生,一个医生有多条排班记录,一条排班记录可以被多个用户挂号,挂号记录里冗余保存了医生和科室信息。这里有一个容易忽略的设计点:为什么挂号表里已经有了schedule_id,还要冗余doctor_id和dept_id?因为用户在个人中心查看“我的挂号”时,如果每条记录都要通过schedule_id去关联查询排班表,再关联医生表和科室表,至少要三次JOIN。冗余之后,查询挂号列表就只需要查一张表,节省查询开销,这在互联网项目里叫空间换时间。答辩的时候能解释清楚这个设计意图,是非常加分的。
2.2 排班表与号源字段的设计思路
排班表是整个挂号系统的枢纽,它决定了用户在页面上能看到什么、能挂什么。设计的时候有几个字段必须想清楚。
首先是work_date,代表医生出诊的日期。这个字段需要用date类型而不是varchar,因为后续要按日期范围查询和比较大小。其次是time_slot,代表时间段,常见的取值是“上午”和“下午”,你也可以细化成“08:00-10:00”“10:00-12:00”这种更精确的时段。字段值用字符串存储,前端直接展示,简单直观。
然后是total_count和remain_count这对黄金搭档。total_count是排班总号源数,比如上午放30个号,remain_count是剩余可挂号数。每次有人挂号成功,remain_count减一。这个设计比只用total_count然后统计挂号记录数量要高效得多。total_count还有一个作用是被扣除的号源数会被记录在挂号记录表里,需要对账的时候可以互相校验。
status字段我建议用int类型,0表示正常排班,1表示已停诊。停诊的含义是医生临时不能出诊,管理员后台改状态之后,前端就该把这条排班置灰,用户不能再挂号。注意这里不能用删除的办法,因为已经挂号的用户需要保留历史记录,这就是逻辑删除比物理删除更适合业务系统的原因之一。
2.3 唯一约束:防重复挂号的最后防线
排班表和挂号记录表设计完之后,有一个很容易忽略的坑:用户可能用同一个账号重复挂同一个医生同一天的号。业务代码里可以做校验,但代码有bug或者并发请求同时进来的时候,校验可能失效。最保险的做法是给挂号记录表加一个数据库层面的唯一索引。
在appointment表上,对user_id、doctor_id、work_date三个字段创建联合唯一索引。这样一来,哪怕代码逻辑漏掉了,数据库也会在第二次插入相同记录时直接报错。这个设计在答辩时非常值得提,因为它体现了你对数据一致性的理解——业务校验是逻辑保障,唯一索引是物理兜底,两层防线缺一不可。
注意:创建联合唯一索引的SQL语句是
ALTER TABLE appointment ADD UNIQUE KEY uk_user_doctor_date (user_id, doctor_id, work_date);,索引字段顺序尽量把区分度高的字段放前面,实际执行时优化器会更灵活。
3. 核心功能模块:从登录到挂号的全流程实现
3.1 登录鉴权:JWT无状态认证
前后端分离架构下,最常用的鉴权方案就是JWT。原理不复杂:用户登录成功后,后端生成一个加密的token返回给前端,前端把token存到localStorage里,之后每次请求都在请求头里带上这个token,后端通过拦截器校验token是否有效。整个过程不需要在服务器端保存Session,天然适合水平扩展和多端登录。
生成token的工具类可以用jjwt库来实现,代码很简短。关键是token里放什么内容,我建议只放userId和username这类非敏感信息,不要放密码。过期时间设置成7天,保证用户体验。用户退出登录时,前端直接删除本地token就行,不需要后端参与。
后端拦截器的逻辑更简单:拦截除登录、注册、科室列表等公开接口之外的所有路径,从请求头Authorization字段里取出token,调用JWT工具类解析,解析成功就放行,解析失败返回401状态码。这块代码一定要提前写好并测试到位,因为整个系统除了登录页面,几乎所有的接口都需要登录后才能访问。
3.2 排班管理:后台维护的核心操作
排班功能虽然听着不复杂,做起来的细节却不少。管理员登录之后,需要能选择医生、选择出诊日期、选择时间段、填写号源总数,然后提交生成一条排班记录。这里最需要处理的是排班冲突问题:如果某个医生在同一个日期的同一个时间段已经排过班,就不能再插入一条新的记录。
排班冲突的判断逻辑是查询schedule表,看是否存在doctor_id、work_date、time_slot三字段相同的记录。这里有一个需要注意的性能问题:如果后台不做校验,数据库也没有联合唯一索引,那么并发情况下可能生成重复排班。解决办法和挂号表一样,给schedule表加上doctor_id、work_date、time_slot的联合唯一索引,把冲突拦截在数据库层面。
前端排班页面用Element Plus的日期选择器加上下拉框就能搞定,提交之前的确认弹窗不能省。我实际做过一个项目,有个同学在排班页面不小心点到了重复提交,导致同一个医生一周的排班重复了两次。加一个简单的二次确认,错误率能降低一大半。
3.3 挂号核心流程:并发场景下的号源扣减
挂号是整个系统最核心的接口,也是最能体现技术水平的环节。前端页面展示可预约的排班列表,用户点击“挂号”按钮,提交预约请求,后端需要完成四个步骤:校验排班是否存在且未停诊、校验号源是否充足、校验用户当天是否重复挂号、扣减号源并生成挂号记录。
这里面的关键点是号源扣减的并发控制。如果直接用一个普通的update语句,先查remain_count再判断是否大于0,然后在代码里减1再更新,高并发下一定会出问题。两个请求同时读到remain_count=1,都判断可以挂号,然后都执行更新,结果最后一个号被挂了两次,这就是经典的超卖问题。
解决思路其实很多,最简单可靠的是用数据库乐观锁。在schedule表里加一个version字段,更新号源时带上版本号条件:
UPDATE schedule SET remain_count = remain_count - 1, version = version + 1 WHERE id = #{scheduleId} AND remain_count > 0 AND version = #{version}MyBatis-Plus本身支持@Version注解的乐观锁功能,配置一个乐观锁插件,然后在实体类的version字段上加上注解就生效了。如果更新影响行数为0,说明号源已被别人抢走,直接返回“号源已满”。
挂号接口的Service层代码结构大致是这样的:
@Transactional(rollbackFor = Exception.class) @Override public AppointmentDO createAppointment(AppointmentCreateDTO dto) { // 1. 查询排班信息 ScheduleDO schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule == null || schedule.getStatus() != 0) { throw new BizException("排班不存在或已停诊"); } // 2. 校验该用户当天是否已挂此医生的号 Integer exists = appointmentMapper.countByUserAndDoctorDate( dto.getUserId(), schedule.getDoctorId(), schedule.getWorkDate()); if (exists != null && exists > 0) { throw new BizException("请勿重复挂号"); } // 3. 乐观锁扣减号源 int updated = scheduleMapper.deductRemainCount( schedule.getId(), schedule.getVersion()); if (updated == 0) { throw new BizException("号源已被抢完,请选择其他时间段"); } // 4. 生成挂号记录 AppointmentDO appointment = new AppointmentDO(); appointment.setUserId(dto.getUserId()); appointment.setScheduleId(schedule.getId()); appointment.setDoctorId(schedule.getDoctorId()); appointment.setDeptId(schedule.getDeptId()); appointment.setWorkDate(schedule.getWorkDate()); appointment.setTimeSlot(schedule.getTimeSlot()); appointment.setStatus(0); appointmentMapper.insert(appointment); return appointment; }注意一点,这个方法上加了@Transactional注解。因为扣减号源和生成挂号记录这两步必须保证原子性——要么同时成功,要么同时失败。如果生成挂号记录失败而号源已经扣了,用户就白白损失了一个号,这是业务上不能接受的。
还有一个细节:重复挂号的校验是查appointment表,但此时挂号记录还没插入,查到的结果一定为空,这就可能出现同一用户同时提交两个挂号请求时,两次都能通过校验,然后插入两条记录。这时候就需要依靠前面说的联合唯一索引来兜底,捕捉到唯一键冲突异常后返回友好提示。数据库规范和业务逻辑结合,才是完整的解决方案。
4. 前后端联调与运行源码的实操指南
4.1 统一返回结构与axios封装
前后端分离开发的时候,最怕的就是接口返回格式不一致。今天这个接口返回{success: true},那个接口返回{code: 200},前端写起来会非常痛苦。所以项目一开始就要约定统一的返回体结构。
我建议用Result 包装类,包含code、message、data三个字段。code为200表示成功,500表示业务异常,401表示未登录。后端所有Controller的返回值都写成Result类型,前端axios拦截器统一处理后,代码会干净很多。前端工具类封装好后,页面请求代码基本就是调接口、取data、赋值给表格或表单这种固定套路。
axios的请求拦截器里要统一加上token,响应拦截器里统一处理code不等于200的错误提示,并且遇到401时跳转到登录页。这些封装工作在项目初期就要做好,否则后面几十个接口改起来会让人怀疑人生。
4.2 源码目录结构与启动步骤
拿到源码之后,第一件事不是急着打开IDE,而是先看目录结构和README文档。这个项目的标准结构分三块:backend后端、frontend前端、sql数据库脚本。后端是标准的Maven项目,按controller、service、mapper、entity分包结构组织;前端是Vue CLI创建的项目,views目录下按页面拆分组件;sql目录下是整个项目的初始化脚本,包含建库建表语句和演示数据。
启动步骤也别乱,按顺序来:先在MySQL中执行sql脚本,导入数据库;然后修改后端application.yml里的数据库连接配置,把用户名密码改成自己的;启动后端项目,看到端口启动成功的日志后,再进入frontend目录执行npm install安装依赖,依赖装完执行npm run serve启动前端开发服务器。最后浏览器访问http://localhost:8080,能看到登录页面就说明环境通了。
这里有一个非常容易踩的坑:npm install在国内环境下下载速度很慢,甚至可能卡住不动。解决办法是设置npm的镜像源为淘宝源,执行命令npm config set registry https://registry.npmmirror.com,之后速度会有质的提升。
4.3 前后端接口联调的关键配置
前后端联调时第一个遇到的就是跨域问题。前端跑在8080端口,后端跑在8081端口,浏览器会拦截跨域请求。解决方式有两种:后端配置CORS过滤器,或者前端在vue.config.js里配置代理转发。
我更推荐前端代理的方式,因为生产部署时可以无缝切换。开发环境下,在vue.config.js里配置devServer的proxy,把所有/api开头的请求代理到后端地址;部署时用Nginx做同样的反向代理。这样就避免了前后端实现不一致的问题,代码不用改。
前端请求统一走代理,在后端接口路径设计上也要注意规范。建议所有接口都以模块前缀开头,比如登录注册走/apis/user,排班走/apis/schedule,挂号走/apis/appointment,这样后端Controller的@RequestMapping值清晰,前端也容易维护。
5. 常见问题排查与避坑实录
5.1 五个高频问题的速查表
做这类系统,有几类问题出现频率极高,我直接把解决方案整理成表格,遇到的时候照着排查即可。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 前端请求接口跨域 | 后端未配置CORS或前端未配代理 | 在vue.config.js中配置devServer.proxy |
| LocalDateTime格式显示为数组 | Jackson序列化时区问题 | 配置spring.jackson.date-format和time-zone |
| 登录成功后刷新页面就失效 | token存到sessionStorage而不是localStorage | 改用localStorage存储token |
| 删除科室提示外键约束失败 | 医生表或排班表有关联数据 | 先处理关联数据,科室改为逻辑删除 |
| 部署到服务器后前端请求404 | 静态资源路径或路由模式问题 | Vue Router改用history模式,Nginx配置try_files |
5.2 我踩过最深的坑:事务失效与懒加载异常
第一个坑是事务失效。我最早写挂号接口的时候,把@Transactional注解加到了Controller层的方法上,结果发现号源扣了但挂号记录没生成,事务根本没生效。原因是Spring的事务管理默认基于代理实现,只有通过代理对象调用方法时才能拦截事务注解,而Controller层往往不是代理目标。正确做法是把事务注解加到Service实现类的public方法上,并且不能通过同类内部方法自调用,否则事务一样会失效。
第二个坑是MyBatis-Plus查询医生列表时,医生实体里包含一个Department对象的引用,也就是一对一关联查询。我用的是简单的关联查询SQL直接查出了科室名称,但有时候懒加载配置打开后,前端拿到对象再访问department属性,会抛出懒加载初始化异常。解决办法要么改成关联查询一次性查出数据,要么在配置里关闭懒加载。如果你在查询用户挂号记录时也遇到类似问题,检查一下实体类里的关联属性是怎么配置的,避免在循环里触发N+1次查询。
5.3 答辩高频追问与准备建议
答辩环节评委老师最关注的其实不是代码本身,而是你对项目的理解程度。我建议围绕下面几个问题提前准备:为什么选择JWT而不是使用Session?讲讲你对前后端分离的理解?号源并发超挂的解决方案是什么?数据库表为什么这么设计?用户和管理员是怎么区分权限的?
每个问题准备一段两分钟左右的口头阐述,最好能结合实际场景说明,比如“当时发现直接用普通update会导致超挂,于是给schedule表加了version字段,用乐观锁解决”。能说出问题、分析和解决过程,比单纯背概念强得多。
另外建议你把项目跑起来之后,截几张关键页面的图放进答辩PPT里,特别是挂号成功后的提示页、个人中心的挂号记录列表页、后台的排班管理页。这些图配合演示视频,视觉冲击力比空口讲强很多。有条件的话还可以录一段包含新增排班、用户挂号、后台查询记录全流程的操作视频,两分钟左右就够了,但展示效果非常加分。
按照这套思路做完,你对这个系统的理解会比单纯刷一遍源码深很多。拿源码做参考没问题,关键是要跑通、读懂、能改。把这个项目的核心链路吃透,再去做其他业务系统,你会发现很多设计思路都是相通的。祝各位毕设顺利,答辩一次通过。