SpringBoot+Vue+MySQL这套组合,在毕业设计里算是出场率最高的组合了,没有之一。我当年带过不少学生做类似选题,也帮人排查过不少预约类系统的Bug。疫苗发布和接种预约这个题目,听起来不算新鲜,但它背后涉及到的权限管理、库存扣减、时间窗口控制、消息通知这些业务逻辑,做扎实了并不容易。这篇文章就结合这套疫苗发布和接种预约系统,聊聊从需求拆解到数据库设计、从后端核心逻辑到前端页面实现、最后到部署上线的完整链路。内容偏实战,很多细节是文档里不会写但实际开发中一定会碰到的。
1. 系统功能蓝图:一个疫苗预约平台到底要拆出哪些角色和流程
拿到这个题目的时候,我第一反应不是急着写代码,而是先梳理业务。很多同学做毕设容易一上来就建表、写接口,结果做到一半发现业务逻辑对不上,推倒重来。疫苗发布和接种预约这个系统,核心其实就三件事:疫苗信息的发布管理、用户的接种预约、预约后的接种记录跟踪。但要支撑这三件事,背后涉及的角色和状态流转比想象中复杂。
先拆角色。系统至少要分三类用户:普通注册用户、疫苗接种管理员、系统管理员。普通用户能做什么?浏览疫苗列表、查看疫苗详情和剩余库存、提交预约申请、查询自己的预约记录、接种完成后查看接种凭证。管理员能做什么?发布疫苗信息、维护疫苗库存、审核预约申请、确认接种结果、管理用户状态、查看预约统计报表。
这里有个点容易被忽略:疫苗不是普通商品,它有批次、有效期、库存、适用人群这些属性。所以疫苗表不能简单设计成"疫苗名称+库存数量",要考虑到批号管理和效期提醒。还有预约流程,很多人以为预约就是把表单提交上去就完事了,实际业务里还需要"审核"环节——管理员要确认预约人的身份信息和接种条件是否符合,然后才能确定预约成功。
预约状态流转也是核心设计点。我建议的状态机是这样:待审核 -> 已通过/已拒绝 -> 待接种 -> 已完成/已取消。每一步状态变更都要有对应的时间记录和操作人记录,方便后续追溯。很多学生做到这里就省略了审核环节,直接提交即成功,这会让论文答辩的时候被老师质疑业务逻辑不完整。
还有一个容易忽略的角色是"接种点"或者"接种机构"。同一个城市可能有多个接种点,不同接种点提供的疫苗批次不一样,预约名额也需要按接种点维度去控制。所以疫苗库存不能只放在疫苗表里,要设计成"疫苗-接种点-批次"关联库存表,这样才能支持多接种点独立管理。
整个核心流程我建议用一句话描述清楚:管理员发布疫苗批次并设置各接种点的可预约数量 -> 用户浏览疫苗列表并选择接种点提交预约 -> 管理员审核预约并确认 -> 用户按时到接种点完成接种 -> 管理员录入接种结果 -> 系统生成接种记录凭证。把这条主线理出来,数据库设计和接口设计就都有依据了。
2. 技术选型逻辑:SpringBoot+Vue+MySQL为什么是毕业设计的最优解
技术选型这块,我直接给结论:对于大部分学生来说,SpringBoot+Vue+MySQL就是现阶段的答案组合。如果你对系统并发要求极高、需要分布式事务、需要微服务拆分,那另说,但毕业设计场景下,这套组合在开发效率、学习成本、答辩可讲性上是最均衡的。
SpringBoot作为后端框架,核心优势在于"约定优于配置"。你不用像传统SSM那样写一堆XML配置,application.yml里配好数据源、MyBatis映射、端口号,就能跑起来。SpringBoot内置的Tomcat也省去了单独配置服务器的麻烦,打成一个Jar包就能运行,这对后文要讲的部署环节非常友好。另外SpringBoot对Spring MVC、MyBatis、Druid连接池、JWT鉴权这些常用组件的集成都很顺滑,写业务代码的时候可以专注在逻辑上。
为什么选Vue而不选JSP或者Thymeleaf?最直观的原因是前后端分离。Vue通过Axios调用后端接口,渲染交给前端框架,后端只负责提供JSON数据。这种模式有两个好处:一是接口清晰,前端工程师和后端工程师可以并行开发;二是部署灵活,前端打包后的dist目录可以被SpringBoot作为静态资源托管,也可以独立部署到Nginx,两种方式都行。Vue的组件化开发思路也贴近真实企业项目,答辩的时候解释起来比较有说服力。而且Vue的学习曲线相对平缓,配合Element UI做后台管理界面,几天就能上手,效果还很不错。
MySQL这边就没太多好纠结的了。它免费、轻量、资料多,Navicat或者命令行都能操作。对于预约系统这种规模的数据量,MySQL的性能完全够用。需要注意的是设计表时用好InnoDB引擎、设置好字符集(utf8mb4是标配,不然存不了生僻字和表情符号)、合理添加索引,这三件事做好了数据层就稳了。
最后说一句为什么这个组合"能讲"。毕业设计答辩的时候,老师一定会问"为什么用这个技术"。SpringBoot可以提高开发效率、Vue实现了前后端分离和更好的用户体验、MySQL是成熟稳定的关系型数据库,这三个理由每个都能展开讲,不会没话说。反过来如果你用了特别冷门的技术栈,老师不熟就容易多问,而且你也未必能解释清楚。
3. 数据库设计:预约系统最容易翻车的地方
数据库设计是这套系统能不能撑住业务的关键。我见过不少同学把疫苗库存直接放在疫苗信息表里,预约成功就减库存,取消预约就加库存,看起来没问题,但一旦涉及多接种点、多批次、审核流程,这种设计马上崩。下面直接给出我整理后的核心表结构和设计理由。
3.1 用户与角色表:权限控制的地基
用户表字段至少要覆盖:用户名、密码、手机号、身份证号、姓名、年龄、角色类型、创建时间。密码不要明文存储,建议用MD5加盐或者BCrypt加密。BCrypt是Spring Security自带的加密策略,每次生成的hash不同,安全性更好,推荐优先使用。
角色这块不建议在前端做死判断,更好的方式是设计用户角色表(user_role)支持一个用户多个角色。虽然毕设场景下用户角色基本固定,但用关联表实现更规范,答辩的时候也有细节可讲。JWT令牌里存储用户ID和角色信息,后端接口用拦截器校验角色权限,普通用户和管理员的接口就能隔离开。
3.2 疫苗与库存设计:批次、效期、接种点缺一不可
这是整套系统最有含金量的部分。我建议这样拆表:
- 疫苗信息表:疫苗名称、生产厂家、适用人群、接种剂次(第几针)、疫苗类型(灭活/重组/腺病毒载体)
- 批次表:批次号、生产日期、有效期至、入库时间
- 接种点表:接种点名称、地址、联系电话、服务时间
- 库存表:关联疫苗批次ID + 接种点ID + 可预约余量 + 已预约数量
这样的设计才能支撑起"某接种点某批次疫苗还有多少可预约名额"这种查询。库存表里的余量字段就是预约时候的防超卖关键。后面讲预约逻辑的时候会详细说怎么扣减。
疫苗效期是个容易遗漏的点。预约页面应该根据当前日期自动过滤掉已过有效期的批次,管理员登录后台也会有到期预警提示。这个功能实现起来不难,但很体现系统完整性,推荐加上。
3.3 预约与接种记录:状态流转的完整链条
预约表字段建议这样:预约编号、用户ID、疫苗批次ID、接种点ID、预约时间、预约日期、状态、审核人、审核时间、接种完成时间、取消原因。
这里我特别说明一下为什么要把预约和接种分成两张表而不是一张表。预约单创建的时候,接种还没有发生,是一系列状态中的"待接种";接种完成后,需要生成一条接种记录凭证,上面有疫苗批号、生产厂家、接种机构、接种时间、下次接种时间(如果有第二针),这些字段跟预约单的关注点不完全一样。分成两张表,业务边界更清楚,查询各自领域的数据时也不容易混淆。
预约状态字段建议用tinyint存数字,0-待审核、1-已通过、2-待接种(审核通过后进入待接种)、3-已完成、4-已取消、5-已拒绝。数字比字符串省空间,代码里用枚举常量定义,可读性也不差。
3.4 索引与事务:并发场景下的保命设计
预约表最频繁的查询条件是用户ID和状态,所以联合索引(user_id, status)建议加上。库存表查询条件是批次ID+接种点ID,也建议设置联合唯一索引。预约编号这种业务字段要设置唯一索引,防止重复预约。
事务这块重点讲一下。用户提交预约的接口里,涉及两步数据库操作:第一步查库存余量,第二步扣减库存并插入预约记录。这两步必须放到同一个事务里,否则可能出现"查询有余量但插入时库存已经被扣光"的情况。用@Transactional注解,搭配数据库的行锁(SELECT ... FOR UPDATE),才能在并发环境下保证不超卖。这个问题在答辩的时候几乎是必问的,提前准备好答案。
4. 后端核心模块实现:从登录鉴权到预约扣库的完整思路
后端这块我按照"入口 -> 鉴权 -> 权限 -> 核心业务"的顺序来拆。框架搭建无非是新建SpringBoot项目、引入依赖、配置数据源,这些基础操作不展开,重点讲几个核心模块的实现思路和细节。
4.1 JWT登录鉴权:无状态会话怎么设计
项目用JWT而不是HttpSession,主要是为了前后端分离的架构。前端登录成功后拿到一个token,存到本地(localStorage或sessionStorage),每次请求在请求头里加上Authorization: Bearer {token},后端拦截器解析token获取用户信息,不需要在服务端保存会话状态。
实现上分三步走。
第一步,定义一个JWT工具类,负责生成token和解析token。生成的时候把用户ID、用户名、角色类型作为claim放进去,设置过期时间(建议2小时),用HMAC256算法签名。
第二步,写一个拦截器,实现HandlerInterceptor接口。在preHandle方法里从请求头取出token,调用JWT工具类解析,解析成功就把用户信息放到request的attribute里传给后续的Controller,解析失败直接返回401状态码和统一返回体。
第三步,写配置类注册拦截器,并配置放行路径。登录接口、注册接口、验证码接口、疫苗列表和详情接口这些不需要登录就能访问的路径要放行。管理员的接口在拦截器里额外校验角色,保证只有管理员能访问。
这里有个经验要分享:JWT令牌在拦截器中解析的用户信息,建议直接作为参数传递到Controller层,而不是在Controller里再查一次数据库。减少无谓的查询,接口响应速度会明显提升。当然,敏感操作比如修改密码,需要到数据库里校验用户当前密码,这个另说。
4.2 疫苗发布与分页查询:Conrtoller层该瘦身
疫苗发布接口没什么高深技术,就是管理员提交表单,后端校验字段完整性,然后插入数据库。但有几个字段必须校验:疫苗名称不能为空,适用人群不能为空,批次号格式要合法。建议用Spring Validation框架的@Valid注解配合实体类上的@NotBlank、@NotNull做参数校验,代码干净且不容易漏。
分页查询疫苗列表是用户端核心接口,用MyBatis Plus的Page插件就可以。构造查询条件时,支持按疫苗名称模糊搜索、按疫苗类型筛选、按接种点筛选。返回的数据结构直接用Page对象或者自定义分页返回体,包含总条数、当前页数据列表、总页数这些字段。
需要注意的细节是疫苗列表返回给用户的字段要脱敏。管理员可见的字段比如批次成本、入库数量,不应该返回给普通用户。这里建议定义VO(View Object)类,只包含页面展示需要的字段,不要直接在Mapper层把DO返回给前端。很多人容易忽略这个,虽然毕设评审不一定抓到,但这是区分你和其他学生代码质量的关键点。
4.3 预约核心逻辑:防超卖、状态机、定时任务
预约接口是整个后端最核心的部分,我详细说实现。
前端传参:用户ID(从token里解析)、疫苗批次ID、接种点ID、预约日期和时段。
后端逻辑第一步,校验该用户是否已存在该疫苗批次的预约记录。比如这个疫苗需要打两针,一针和二针预约记录不能冲突,同一天不能有两个预约。用联合索引(user_id, vaccine_batch_id)可以快速查出是否有待完成状态的预约,有就直接拒绝。
第二步,查询库存表的可预约余量。这里必须用SELECT * FROM stock WHERE vaccine_batch_id = ? AND site_id = ? FOR UPDATE,也就是行级锁。锁住这一行之后,判断余量是否大于0,小于等于0就返回"该接种点已约满"。
第三步,扣减余量并插入预约记录。余量减1,预约记录状态设为"待审核"。这两步和第二步的查询放在同一个事务里,任何一步失败就整个回滚。
这里注意一个细节:加上FOR UPDATE之后,这个接口同一时间只有一个人能操作同一个批次的库存,并发预约时必须排队。如果预约请求量特别大,会有一定的性能损耗,但毕业设计这个量级完全不用担心。真要优化,可以用乐观锁字段加版本号的方式,但实现复杂度高一些,必要性不大。
审核操作也要说。管理员在后台看到待审核的预约单,点击通过后,预约状态从"待审核"变更为"待接种",同时生成一条接种记录草稿。点击拒绝则状态变为"已拒绝",并回补库存余量,就是让可预约数量加1。这个回补逻辑必须和状态变更放在同一个事务里,不能只改状态忘了加库存。
定时任务我建议做两个。第一个是自动取消未支付或未确认的预约,这个视具体业务而定,通常可以设定超过X小时未确认自动取消;第二个是预约接种日已过但管理员未录入结果的预约单自动标记为"已完成"或"超时未接种"。两种都用Spring的@Scheduled注解,配合cron表达式实现,扫描条件和更新时间做成配置项,方便后期调整。
4.4 统计与导出:给论文和答辩加分的隐藏模块
预约统计模块是容易被忽略但值得做的功能。用ECharts在前端展示每日预约量、疫苗预约热度、各接种点饱和度等图表,后端提供对应的聚合查询接口。聚合查询用MyBatis的XML文件写SQL,配合COUNT和GROUP BY就能实现。比如按日期统计预约数量,SQL大概长这样:
SELECT DATE(create_time) AS date, COUNT(*) AS total FROM appointment WHERE create_time BETWEEN #{start} AND #{end} GROUP BY DATE(create_time) ORDER BY date这个功能做出来,一方面让整个系统看起来更完整,另一方面答辩的时候有数据可讲,还能顺便展示你的SQL水平。导出Excel功能可以用Apache POI或者EasyExcel,EasyExcel更轻量,官方文档也清楚。把预约列表导出成Excel,管理员可以离线查看数据,这个功能也很加分。
5. 前端Vue实现要点:动态路由、Axios封装、关键页面
前端部分,我的建议是直接用Vue3 + Vite + Element Plus的组合。相比Vue2 + Vue CLI,Vue3的Composition API写起来更清晰,Vite的启动速度也秒杀Webpack。Element Plus的组件丰富,后台管理界面基本就是拼组件。
5.1 登录页和Token管理:前端鉴权的第一道门
登录页比较简单,表单加验证码提交。但Token管理这块有几个细节要处理好。
第一,登录成功后把token存储到sessionStorage还是localStorage?我建议localStorage,因为刷新页面token还在,不会因为浏览器关闭就要重新登录。第二,在Axios的请求拦截器里统一设置Authorization头,这样所有接口都会自动带上token,不用每个请求单独配置。
// request.js axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })第三,响应拦截器里统一处理token过期。后端返回401状态码时,前端清掉本地token并跳转到登录页,同时用Element Plus的Message提示"登录已过期,请重新登录"。唯一要注意的是,登录接口本身不能被这个逻辑拦截,不然登录的时候报401也会跳登录页,形成死循环。
5.2 路由守卫与动态权限菜单
路由守卫是前端权限控制的关键。在router.beforeEach里判断,如果用户访问的路径需要登录而本地没有token,就跳转到登录页。如果已登录但访问的是登录页,则跳转到首页。如果有token但访问的是管理员页面,就检查本地存储的角色信息,非管理员跳转到403页面。
这里多说一个进阶实现:根据后端返回的角色权限动态生成路由菜单。也就是管理员登录后能看到的菜单项和普通用户不同,而不只是页面跳转拦截。实现思路是登录后请求后端的"当前用户菜单"接口,拿到菜单数据后通过Vue Router的addRoute方法动态添加路由。这个功能在论文里可以单独立一节,展现你的前端功力。
5.3 预约页面的关键交互
预约页面有几个交互细节直接影响用户体验和系统的正确性。
第一个是疫苗批次选择和日期选择联动。用户选了一个疫苗批次后,要马上展示该批次在各接种点的剩余名额。这里要注意的细节是"已约满"的接种点要置灰不可点击,数据从后端实时拉取,不能只靠页面初始化时的缓存。第二个是日期选择器的范围限制,只能预约未来3-7天内的日期,之前的日期直接禁用,这一步在前端做一层限制,后端也要再做一次校验,防止绕过前端直接调接口。第三个是提交预约后的反馈。成功要提示"预约成功,等待管理员审核",失败要展示明确的失败原因,比如"该批次疫苗在你选择的接种点已约满"。
页面组件化是一个容易被忽略但很重要的点。疫苗卡片、预约表单、预约记录表格都可以抽成独立的Vue组件,父组件负责数据分发,子组件负责展示和交互。组件化之后代码的可维护性会明显提升,这在答辩的时候也是能体现工程能力的地方。
5.4 状态展示与用户体验细节
预约列表页有多个状态,要直观展示给用户。我建议用Element Plus的Tag组件,不同状态不同颜色:待审核橙色、待接种蓝色、已完成绿色、已取消灰色、已拒绝红色。点击"取消预约"按钮时,只有状态是"待审核"才能取消,已经审核通过的预约要联系管理员,前端把按钮禁用掉。
还有一个细节是移动端适配。虽然毕设评审不会强制要求移动端完美显示,但简单的栅格布局和流式宽度可以保证手机浏览器打开页面也能用。前端框架里用Element Plus的Row和Col布局,合理设置span就能做到基本的响应式。
6. 部署与打包:从源码到可演示项目的完整路径
部署这块是我辅导学生时踩坑最多的地方。很多同学代码都写完了,但部署环节反复出问题。我把最稳妥的流程整理出来,按步骤操作基本不会错。
6.1 本地打包前端
前端项目根目录执行npm run build,默认生成dist目录。Vite默认的base是"/",如果你的系统要部署在服务器根路径下,这个不用改;如果部署在子目录,比如http://ip:8080/vaccine/,就需要在vite.config.js里设置base: '/vaccine/',这个细节特别容易踩坑,部署完了发现页面白屏、资源404,多半就是这个问题。
打包完成后,dist目录下会有index.html、assets目录等文件。这部分文件要么拷贝到SpringBoot的static目录,要么部署到Nginx。
6.2 两种部署方式对比与选择
方式一:前端dist目录拷贝进SpringBoot的src/main/resources/static里,后端一起打包成单个Jar。这种方式最简单,一个Jar全搞定,演示的时候直接java -jar就能跑。但缺点是每次改前端都要重新打包后端,比较繁琐。
方式二:前端dist目录部署到Nginx,后端Jar单独运行,Nginx配置反向代理,把/api开头的请求转发到后端8080端口。这种方式更贴近真实生产环境,前端后端独立部署、独立维护,但配置稍微复杂一些。
对于毕业设计,我推荐方式一,省事。单个Jar直接跑,演示的时候不依赖额外环境。如果你想把部署写得更详细、显得更有深度,可以在论文中把方式二也介绍一遍,对比两种方式的优缺点,是一种很好的加分写法。
方式一的具体操作:前端build完成,把dist目录的内容拷贝到后端src/main/resources/static目录下,重新打包后端,然后用命令。
mvn clean package -DskipTests打完的Jar包在target目录下,运行命令:
java -jar vaccine-system-1.0.0.jar --server.port=8080打开浏览器访问http://localhost:8080,会自动跳转到前端的首页。
注意:SpringBoot会把static目录下的index.html作为默认欢迎页,所以拷贝时不用做额外配置。但要确认没有在Controller里写了根路径的@GetMapping("/"),如果有,会覆盖掉默认行为,跳不到index.html了。
6.3 服务器部署的环境准备
服务器上部署所需的运行环境:JDK 1.8+(建议JDK 8,绝大多数毕设的项目版本都兼容8)、MySQL 5.7+、Maven(如果没有用Docker)。
数据库导入用Navicat或者命令行都行。拿到项目里的sql文件后,先在服务器上创建数据库,再把sql文件导入。导入前检查一下sql文件里有没有指定数据库名的语句,有的话要先创建对应的数据库。还有一个常见的坑:字符集不一致导致中文乱码。创建数据库时要指定utf8mb4字符集:
CREATE DATABASE vaccine_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;6.4 部署后的自检清单
系统启动后不要急着演示,按清单快速自检一遍:
- 首页是否正常加载,疫苗列表接口是否返回数据
- 注册一个新用户,看验证码接口是否正常
- 登录后能否正常提交预约,预约后库存是否有变化
- 管理员账号能不能登录后台,疫苗发布后前台能否看到
- JWT过期后是否自动跳转登录页
这套自检流程是答辩前的最佳实践。同时建议把项目里的测试数据清空一部分、从管理员后台重新发布若干条疫苗信息、造几条不同状态的预约记录,这样演示的时候数据干净、状态完整,老师看到的系统是"活"的。
6.5 论文与部署文档的配合写法
论文里部署章节不用长篇大论贴后端源码,重点是让阅读者看完文档就能把系统跑起来。我建议的文档结构是:环境要求(JDK、MySQL、Node版本)、数据库配置(sql脚本位置、导入步骤)、前端打包(npm install、npm run build)、后端启动(maven打包、java启动)、访问地址与默认账号。每一步配一张截图,文档长度控制在10-15页就足够了。
"源码+数据库+论文+部署文档"这个组合里,部署文档的价值往往被低估。高质量的部署文档能减少大量答疑成本,答辩的时候老师也会认可你的工程交付能力。
7. 实测中的坑与优化方向:这些细节让你的项目真正经得起问
最后总结一下这套系统开发和实测过程中遇到的典型坑,以及后续可以扩展的方向。
第一个坑是库存超卖问题。有一个版本我没用FOR UPDATE,直接用update stock set remain = remain - 1 where remain > 0这个原子SQL去扣减,其实那条语句本身就具备防超卖能力,因为MySQL在update时会锁行。但如果前面先select后update,中间有时间差,高并发下就超卖了。一定要保证"查库存 + 扣库存 + 插预约"在同一个事务里,选一种方式一次性设计好。
第二个坑是JWT的密钥写死在代码里。虽然是毕设,但把密钥放到application.yml配置里,作为项目配置项,显得更规范。环境切换时也能用不同配置,不会因为代码打包泄露。
第三个坑是服务器时区问题。部署到服务器后,发现预约时间和本地时间差8个小时。原因是MySQL连接串里的serverTimezone没设置,或者设置成了UTC。解决办法是连接串加上serverTimezone=Asia/Shanghai,并且服务器系统时区也要确认是Asia/Shanghai。
第四个坑是前端动态路由刷新后404。用addRoute添加的动态路由,浏览器一刷新就丢失了,页面变成404。解决方案是在路由守卫里做一个标志位,刷新后如果发现路由表状态不一致,就重新创建动态路由,再执行进入操作。
优化方向上,根据这套系统的后续空间,我给出几个建议:
- 加入疫苗库存不足时的站内消息或者短信提醒,需要接入消息队列或者短信服务商,毕设做到站内消息就够了
- 加入接种凭证PDF的生成与下载,后端用iText或者POI的PDF组件实现
- 加入用户体检信息表,预约时自动校验适用人群条件,比如某些疫苗不适合特定年龄人群
- 加入Redis缓存疫苗列表和热点数据,降低数据库压力,这个优化在答辩时很有说服力
- 用Spring Security框架替换手动拦截器,实现更完整的安全控制体系
这里面每一项都可以展开成一个小的优化章节写进论文,扩充篇幅的同时还能体现你的技术视野。但实际代码改动建议控制在1-2个,太多反而给答辩增加压力。
这套系统从功能设计、数据库设计到代码实现和部署,如果是第一次做全栈项目,建议给自己预留三到四周的完整时间,不要指望一周就把所有功能全部写完。优先保证核心主流程跑通,再逐步往上面加功能。做项目的过程本身就是学习的过程,把SpringBoot、Vue、MySQL这三条线吃透,答辩的时候你自然底气十足。