1. 项目到底在做什么
先说结论:这是一个基于 Java 技术栈、SpringBoot 作为核心框架的疫苗预约系统,完整覆盖了用户注册登录、疫苗信息浏览、接种点选择、时间段预约、预约记录管理、后台审核与统计等一整套业务流程。
很多计算机专业的同学拿到这个题目时,第一反应往往是“这不就是一个 CRUD 项目吗,有什么好写的”。其实这个判断只对了一半。疫苗预约系统表面上是典型的 Web 增删改查,但它背后涉及的高并发预约场景、库存扣减一致性、时间段容量控制、接口幂等问题,恰好是面试和答辩环节最容易深挖的点。把这些问题想清楚、做扎实,这个项目就不只是一个“交差”的毕设,而是一份能写进简历的真实工程经验。
从需求方的角度看,这个系统解决的是什么问题?传统排队接种模式需要人工登记、纸质记录、电话通知,效率低还容易出错。预约系统把整个流程搬到线上后,居民可以随时查看可预约的接种点和疫苗余量,选择适合自己的时间段;工作人员在后台统一管理疫苗批次、库存和预约记录,接种点能提前知道每天的到诊人数,合理安排医护人员和疫苗供给。这就实现了从“人找疫苗”到“疫苗等人”的转变。
系统适合谁来参考?如果你正在准备 Java 方向的毕业设计,或者想做一个能展示完整业务闭环的项目来充实简历,这个题目很值得认真做。它不要求你掌握分布式、微服务这些重架构,但又能把 SpringBoot、MyBatis、MySQL、Redis 这些主流技术串起来,难度曲线刚好在本科毕设的可控范围内。
我会按照一条完整的项目落地路径来讲:先拆业务需求,再定技术方案,然后到数据库设计、核心编码、部署上线,最后是常见坑的排查方法。整个过程中会穿插我自己实测踩过的坑和解决方案,尽量让你拿着这篇文章就能完整复现。
2. 需求拆解与整体设计思路
2.1 三类角色与核心业务闭环
做系统设计第一步永远不是写代码,而是把“谁在用这个系统、他们要干什么”梳理清楚。疫苗预约系统的用户角色分为三类:居民用户、系统管理员、接种点工作人员。
居民用户端的功能相对标准化:注册登录、查看疫苗列表与库存、选择接种点和时间段、提交预约、查看预约状态、取消预约、查看接种记录和电子凭证。
管理员端承担的是全局配置职责:维护疫苗种类与批次信息、管理接种点资料、审核预约数据、查看统计报表、管理所有用户账号和权限。
接种点工作人员是很多同学容易忽略的角色,但在实际场景里非常重要。他们需要查看本接种点当日和近期的预约名单,确认用户到诊后执行“核销”操作,把预约单状态从“已预约”变成“已完成”,同时处理用户爽约后的释放逻辑。
这三类角色构成了完整业务闭环:用户发起预约 → 系统锁定疫苗库存和时间段名额 → 工作人员现场核销 → 系统更新库存与统计数据。
2.2 功能边界与优先级划分
毕设项目最忌讳一上来就想做“大而全”,结果每个功能都只做了个壳。疫苗预约系统的功能优先级可以这样划分:
- P0(必须实现):用户注册登录、疫苗信息展示、接种点与时间管理、在线预约、预约取消、后台预约管理、库存扣减与恢复
- P1(建议实现):验证码登录、接种记录生成、统计报表、短信通知(可模拟)
- P2(加分项):疫苗批次追溯、爽约限制策略、操作日志、数据导出
P0 功能保证的是一个“能真实跑起来”的完整业务闭环,P1 是答辩时的亮点,P2 是展示设计深度的加分项。我建议先把 P0 全部做扎实,再按时间精力逐级往上加。
2.3 业务流程图的逻辑化描述
实际的系统流程可以按“预约前—预约中—预约后”三个阶段来理解,这里不用图形,用文字把逻辑串清楚:
预约前:用户登录系统,查看疫苗列表(疫苗名称、生产厂商、适用年龄段、剩余库存),选择接种点。每个接种点会展示剩余总名额和日期段的可预约情况。
预约中:用户选择接种日期和具体时间段(比如上午 9:00-11:00),系统实时校验:该时间段是否还有名额?该疫苗在该接种点是否还有库存?用户是否已存在未完成的预约?校验通过后创建预约单,同时扣减时间段名额和疫苗库存。
预约后:用户可在“我的预约”中查看预约单详情。若临时有事,可以在规定时间内取消,取消后系统自动释放名额和库存。用户到接种点后,工作人员核销预约单,系统生成接种记录。
注意:整个流程中最核心的约束是“一个用户同一时间只能有一条有效预约单”,这个约束必须在数据库层面和代码层面双重保证,否则高并发下必然出重复预约。
3. 技术选型与其背后的理由
3.1 后端框架为什么锁定 SpringBoot
SpringBoot 在毕设项目里几乎是事实标准,原因是它极大降低了 Spring 体系的搭建成本。传统 SSM 项目要写大量 XML 配置,光一个 Spring 容器整合就能耗掉两三天;SpringBoot 的自动配置机制把这些重复劳动全部消解掉了,内置 Tomcat 让 Web 应用可以直接以 Jar 包方式运行。
更重要的是,SpringBoot 的生态完备,几乎任何需要的功能都能找到官方或社区提供的 Starter。做权限可以用 Spring Security,做缓存可以用 Spring Cache 整合 Redis,做参数校验有现成的 Validation 注解。这对毕设周期很短的学生来说非常友好。
我建议统一使用 Java 8 作为开发环境。虽然 JDK 17、21 已经发布很久了,但很多学校实验室、服务器和答辩演示环境的 JDK 版本还停留在 8。用 Java 8 写出来的代码几乎不会遇到环境兼容问题,这不是技术落后,而是对交付负责。
3.2 持久层选择 MyBatis-Plus 的真实原因
数据访问层的选型我推荐 MyBatis-Plus。它本质上是 MyBatis 的增强工具,核心价值是提供了通用 Mapper 接口和 Service 层封装。单表操作完全不需要手写 SQL,条件构造器可以链式查询,分页插件直接支持物理分页。
选它的原因很简单:毕设项目 80% 的数据库操作是单表 CRUD,用 MyBatis-Plus 能省下大量时间;剩下 20% 的多表关联查询(比如预约单关联用户和接种点),依然可以手写 XML SQL,灵活性并没有丢。
在实体映射上,记得开启驼峰映射配置,这样数据库字段create_time可以自动映射到实体类的createTime,少写很多@TableField注解。
3.3 缓存层为什么引入 Redis
疫苗预约系统有一个真实的并发痛点:疫苗库存和时段名额在预约高峰期会被大量请求同时读写。如果每次都查数据库,不仅响应慢,还会在高并发下触发数据库连接池耗尽。
Redis 在这里承担两个核心任务:一是缓存热点数据(疫苗列表、库存余量),降低 MySQL 读压力;二是做分布式锁和原子扣减计数,保护预约核心链路。Redis 的decr指令对库存扣减是原子性的,能在并发场景下保证不超卖。
如果服务器资源有限,Redis 也可以适度简化,比如先用程序内的synchronized+ 数据库乐观锁兜底。但这个取舍我会在后面单独讲。
3.4 前端层 Vue 与管理端的实现方案
前端部分推荐使用 Vue 2 + Element UI,这套组合在中文社区资料最丰富,模板组件齐全,遇到问题几乎都能搜到现成方案。
用户端可以设计成移动端风格的 H5 页面,用 Vue Router 做页面路由,通过 Axios 调用后端接口;管理端则采用经典的后台布局——顶部导航、左侧菜单、右侧内容区,用表格组件展示预约记录和统计数据。
这里有个建议:不要自己从头写 CSS 样式,直接使用现成后台模板或者 Element UI 的响应式布局。毕设评分看重的是业务逻辑完整度和技术方案合理性,而不是 CSS 写得有多漂亮。
4. 数据库设计与状态流转机制
4.1 核心数据表结构规划
数据库设计是业务逻辑的基石。疫苗预约系统的表结构建议分 7 张核心表,下面把每张表的重点字段列出来:
用户表(sys_user)
id:主键,自增username:用户名,登录账号,唯一索引password:密码,使用 BCrypt 加密存储real_name:真实姓名id_card:身份证号,用于身份校验和实名预约phone:手机号,用户接收预约通知role:角色标识(0 用户 / 1 管理员 / 2 接种点人员)create_time:创建时间
疫苗信息表(vaccine)
id、vaccine_name:疫苗名称manufacturer:生产厂商vaccine_type:疫苗类型(如灭活疫苗、重组蛋白疫苗等)dosage_count:需要接种的剂次suitable_age:适用年龄段description:疫苗详细说明status:上下架状态
疫苗批次表(vaccine_batch)
id、vaccine_id:所属疫苗batch_no:批次号production_date:生产日期expiry_date:有效期至quantity:该批次库存数量
接种点表(vaccination_site)
id、site_name:接种点名称address:详细地址contact_phone:联系电话open_time:开放时间(如 08:00-17:00)daily_capacity:每日最大接待量status:营业状态
预约时段表(vaccine_schedule)
id、site_id:所属接种点vaccine_id:可预约的疫苗schedule_date:预约日期time_slot:时间段描述(如 09:00-10:00)total_quota:该时段总名额booked_quota:已预约名额status:可预约状态
预约单表(appointment)
id、appointment_no:预约单号,唯一user_id:预约用户site_id:接种点vaccine_id:疫苗schedule_id:关联的具体时段appointment_date:预约日期status:状态(待接种、已完成、已取消、已爽约)order_type:第几剂次create_time、cancel_time、complete_time:关键时间节点
接种记录表(vaccination_record)
id、appointment_id:关联预约单user_id、vaccine_id、site_idinjection_date:实际接种日期dose_number:剂次序号operator_id:操作人员(接种点工作人员)remark:备注,如注意事项
4.2 预约单状态机的设计
预约单状态是整个系统里最容易被轻视、实际最容易出设计问题的模块。状态不能写死成随意变更,需要定义清晰的流转路径:
PENDING(待接种):用户预约成功后进入的状态,此时库存和名额是锁定状态COMPLETED(已完成):用户到接种点完成接种后,工作人员核销时更新CANCELLED(已取消):用户主动取消或管理员强制取消,系统会释放库存和名额NO_SHOW(爽约):超过预约时间且未取消的预约单被标记为爽约,释放资源
状态流转规则为:PENDING → COMPLETED、PENDING → CANCELLED、PENDING → NO_SHOW。为什么设计成这么简单?因为复杂状态机在毕设阶段容易引入难以排查的 bug,而且答辩老师通常会针对状态一致性问题追问,简单清晰反而加印象分。
4.3 预约单号生成规则
预约单号不是数据库自增 ID,而是业务单号。我采用的生成方案是:日期(8位) + 随机数(4位) + 自增序列(4位),例如2024052718320001。
生成逻辑:获取当前日期做格式化,拼接一个四位随机数,再拼上用 RedisINCR生成的四位递增序列。如果查询到该单号已存在,则重新生成。这种方式的好处是单号本身能反映出预约日期,便于联调排查问题,同时不依赖数据库自增的双表耦合。
5. 核心业务实现与并发控制细节
5.1 关键接口设计与参数校验
后端接口设计遵循 RESTful 风格,统一返回结构。建议封装一个通用响应体Result<T>,包含code、message、data三个字段,前端 Axios 拦截器统一处理。这样接口的返回结构一致,前端解析逻辑只需要写一遍。
预约接口是核心中的核心,设计时需要考虑三个方面:
第一,参数校验。预约时间不能早于当前时间;时间段必须存在且处于可预约状态;用户身份证号格式必须合法;同一用户不能对同一时段重复预约。用注解式校验@Validated+@NotNull等配合全局异常处理器,比在 Controller 里手写判断干净得多。
第二,前置业务校验。检查用户是否已有未完成的预约单,检查时段的booked_quota是否小于total_quota,检查疫苗批次库存是否大于 0。
第三,数据一致性操作。创建预约单、扣减时段名额、扣减疫苗库存这三个操作必须放在同一事务中,任何一个失败都要全部回滚。
5.2 库存扣减不超卖的实现方案
疫苗预约场景的库存资源分为两级:疫苗批次库存(全局资源)和预约时段名额(时段资源)。两级资源都要防止超卖。
方案一:Redis 原子操作方案。预约前先从 Redis 缓存中查询疫苗库存余量和时段余量,在 Java 代码中做预校验;通过预校验后,使用 Redis 的decr命令原子扣减库存;扣减成功后执行数据库操作。若数据库操作失败,则调用incr回补。
方案二:数据库乐观锁方案。在库存表增加version字段,执行UPDATE vaccine_batch SET quantity = quantity - 1, version = version + 1 WHERE id = ? AND quantity > 0 AND version = ?。更新影响行数为 0 则说明版本冲突或库存不足,需要重试或返回失败。
我两个方案都实现过,最终推荐的做法是:在预约核心链路上用 Redis 的 Lua 脚本保证原子性,在数据库层用quantity > 0作为兜底校验。两者的关系不是二选一,而是双保险——Redis 挡住绝大多数并发流量,数据库保证极端情况下不超卖。
5.3 预约接口伪代码与实现要点
下面这个关键方法的实现逻辑,可以直接参考:
@Transactional(rollbackFor = Exception.class) public Result<AppointmentVO> createAppointment(AppointmentCreateDTO dto) { // 1. 校验用户是否存在且状态正常 User user = userMapper.selectById(dto.getUserId()); if (user == null) { return Result.error("用户不存在"); } // 2. 校验用户没有未完成的有效预约单 Integer pendingCount = appointmentMapper.selectCount( new LambdaQueryWrapper<Appointment>() .eq(Appointment::getUserId, user.getId()) .in(Appointment::getStatus, "PENDING", "NO_SHOW")); if (pendingCount > 0) { return Result.error("您已有待接种的预约单"); } // 3. 查询时段,校验是否可预约 VaccineSchedule schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule == null || "DISABLED".equals(schedule.getStatus())) { return Result.error("该时间段已关闭预约"); } if (schedule.getBookedQuota() >= schedule.getTotalQuota()) { return Result.error("该时段名额已满"); } // 4. Redis 预扣减时段名额,防止并发超卖 String quotaKey = "schedule:quota:" + schedule.getId(); Long remainQuota = redisTemplate.opsForValue().decrement(quotaKey); if (remainQuota == null || remainQuota < 0) { redisTemplate.opsForValue().increment(quotaKey); return Result.error("该时段名额刚刚被抢完"); } try { // 5. 扣减疫苗库存 int updated = vaccineBatchMapper.deductStock(schedule.getVaccineId()); if (updated == 0) { throw new BusinessException("疫苗库存不足"); } // 6. 构建预约单 Appointment appointment = new Appointment(); appointment.setAppointmentNo(generateAppointmentNo()); appointment.setUserId(user.getId()); appointment.setSiteId(dto.getSiteId()); appointment.setVaccineId(schedule.getVaccineId()); appointment.setScheduleId(schedule.getId()); appointment.setAppointmentDate(schedule.getScheduleDate()); appointment.setStatus("PENDING"); appointment.setCreateTime(new Date()); appointmentMapper.insert(appointment); // 7. 更新时段已预约数量 scheduleMapper.increaseBookedQuota(schedule.getId()); return Result.success(toVO(appointment)); } catch (Exception e) { // 8. 失败回补 Redis 已扣减的计数 redisTemplate.opsForValue().increment(quotaKey); throw e; } }这段代码的关键点在于:库存扣减和预约单创建在同一个事务里——要么全部成功,要么全部回滚。Redis 的扣减在事务提交过程中若发生异常,还要在 catch 中手动回补,避免缓存计数和数据库数据不一致。
提示:这里如果只用数据库乐观锁,不用 Redis,也能在小并发场景下正常工作。但如果你在答辩时说“我的系统支持高并发预约”,那建议还是把 Redis 这层做进去,实际的性能表现和数据一致性表现会更经得起追问。
5.4 取消预约与资源释放的一致性
取消预约和预约创建是对称操作,同样要保证一致性。用户取消后,需要同时完成:预约单状态置为CANCELLED、时段已预约数量减一、疫苗库存回补。三个操作放同一事务里。
这里有一个细节:取消预约可以设置一个时间限制,比如“预约接种日当天不可取消”。实现时在 Service 层判断appointmentDate与new Date()的关系,用LocalDate.now()比较即可,不需要复杂定时任务。
6. 前端页面与接口联调实现
6.1 用户端核心页面
用户端重点是预约流程的顺畅度。页面结构建议按“首页 → 疫苗列表 → 接种点与时段选择 → 确认预约 → 我的预约”这条主线设计。
首页用卡片或列表展示疫苗基本信息,点击进入详情页。预约流程中,时间选择控件很关键,建议根据后端返回的VaccineSchedule数据结构动态渲染日期和时间段。某个时间段若bookedQuota >= totalQuota,前端直接置灰不可点击,减少无效请求。
6.2 管理端页面与服务接口对应
管理端的功能模块与后端接口一一对应,主要接口清单如下:
GET /api/admin/site/list:接种点分页查询POST /api/admin/site:新增接种点PUT /api/admin/site:修改接种点DELETE /api/admin/site/{id}:删除接种点GET /api/admin/appointment/list:预约记录分页查询PUT /api/admin/appointment/cancel:管理员取消预约GET /api/admin/stats/daily:按日统计预约与完成人数GET /api/admin/vaccine/list:疫苗列表管理POST /api/admin/vaccine/batch:录入疫苗批次
6.3 Token 鉴权与拦截器配置
系统采用 JWT 做无状态登录认证。用户登录成功后,后端返回 Token,前端存储在localStorage,Axios 请求拦截器统一在请求头加上Authorization字段。后端用拦截器校验 Token,解析出用户信息并存入ThreadLocal或RequestContext,供 Service 层随时获取当前用户。
# 前端 Axios 拦截器伪代码 axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; });配置类继承WebMvcConfigurer,注册拦截器并排除登录接口和静态资源路径。同时要注意 CORS 跨域配置,前后端分离部署时如果端口不同,必须处理跨域问题。
6.4 统计报表功能的实现思路
统计报表是管理端展示项目完整度的标志性功能。按日期维度统计每日预约人数、实际完成人数、取消人数,可以画折线图和柱状图。
SQL 层面核心是分组聚合查询:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS stat_date, COUNT(*) AS total_count FROM appointment WHERE create_time >= #{startDate} GROUP BY stat_date ORDER BY stat_date;前端可以用 ECharts 把数据渲染成图表。这部分工作量不大,但视觉效果很加分。
7. 部署环境与上线配置要点
7.1 本地开发环境搭建
开发环境建议:JDK 1.8、Maven 3.6+、MySQL 5.7+、Redis 5.0+、Node.js 14+。
项目结构采用 Maven 多模块或单模块分层结构均可。保守建议单模块按包分层:controller、service、mapper、entity、dto、config、common、utils。不引入多模块是为了避免 Maven 模块依赖关系复杂化,本质原因还是“低复杂度优先”。
7.2 打包部署上线流程
SpringBoot 项目通过 Maven 打包成可执行 Jar 包:
mvn clean package -DskipTests打包后复制到服务器,使用 nohup 方式后台运行:
nohup java -jar vaccine-app.jar --spring.profiles.active=prod > app.log 2>&1 &上线前的配置必须在application-prod.yml中指定生产环境的数据库地址、Redis 地址,并关闭控制台 SQL 输出。
7.3 答辩环境准备
答辩时最怕演示现场翻车,我建议提前准备一套本地演示数据:5 个以上用户、多个接种点、不同状态的疫苗批次和预约单。务必提前预演“预约成功→取消预约→管理员核销→生成统计报表”的完整演示链路,确保每一步的数据变化都能在页面上直观呈现。
8. 常见问题与排查技巧
8.1 部署与运行常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 项目启动报连接池初始化失败 | MySQL 没启动 / 账号密码错误 / 数据库不存在 | 检查 MySQL 服务状态,核对application.yml连接配置 |
| 前端请求接口 404 | 前端代理路径配置错误 / 后端接口路径不匹配 | 检查 Vite 或 Nginx 代理配置,用 Swagger 确认实际接口地址 |
| JSON 返回日期格式异常 | 默认序列化格式不是预期格式 | 配置 Jackson 全局日期格式,或字段加@JsonFormat注解 |
| 预约名额超卖 | 检查库存扣减没加原子性保障 | 参考 5.3 节,补 Redis 扣减或数据库乐观锁 |
| 修改代码后不生效 | 打包产物缓存 / 热部署未开 | 清理 target 目录重新mvn package,或用 DevTools 热部署 |
8.2 踩坑实测:事务不生效的根因
我在实际项目中遇到过@Transactional注解不生效的情况,排查后发现是同类 Service 内部方法调用导致的。Spring 事务默认基于 AOP 代理,同类方法内部调用时不会经过代理对象,事务注解被绕过。
场景:在AppointmentServiceImpl中,createAppointment方法调用了同类里的deductStockAndCreateAppointment方法,后者标了@Transactional,但这个注解没有起作用。
解决方式有两个:一是把内部方法抽到独立的类中,通过注入的代理对象调用;二是在 Controller 层的事务起点设置事务。理解了这个原理,以后遇到类似问题就不会浪费几个小时调试了。
8.3 前端跨域问题排查
前后端分离项目中跨域问题出现的频率极高。前端运行在localhost:5173,后端在localhost:8080,端口不同则典型触发跨域。
处理方式推荐在 SpringBoot 中统一配置 CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)时,AllowedOrigin不能直接用*,要匹配具体域名或用allowedOriginPatterns。这一条是很多人踩过的坑。
8.4 数据一致性异常的排查思路
如果出现预约记录已创建,但时段已约数量没更新,或者库存扣了但预约单丢失,优先排查事务边界。检查所有数据库操作是否在同一个事务方法内,特别关注 Redis 操作是否混入事务中,因为 Redis 不支持@Transactional回滚。我在设计中把 Redis 扣减放在事务外层,异常时手动回补,就是基于这个考量。
9. 项目扩展与打磨建议
如果时间充足,有四个方向可以继续打磨,每一项都能在答辩时成为亮点:
一是引入消息队列削峰。在预约流量大的场景,可以把创建预约的请求先放入 MQ 队列,异步处理核心业务。这样接口响应速度更快,系统在短时高并发下更稳定。
二是增加疫苗批次追溯。对每一批次疫苗从入库、分配到接种完的完整链路做记录,这是疫苗系统特有的业务纵深,体现你对行业的理解。
三是完善操作审计日志。管理员做任何变更(修改疫苗信息、强制取消预约)都留下日志,保证操作可追责。用 AOP 自定义注解实现,代码侵入性低。
四是做移动端适配和扫码核销。管理端生成预约二维码,用户到现场后工作人员扫码完成核销,这个功能在实际场景中的真实价值非常高。
我个人在实际操作中的体会是,毕设项目的深度不取决于功能数量,而取决于你在核心链路上是否想清楚了“数据一致性”和“异常恢复”这两个问题。能把预约不超卖、取消不丢单、状态不乱跳这三件事讲清楚,比堆十个华而不实的模块更能打动评审。做项目的过程里,多问自己“如果这一步失败了会发生什么”,顺着这个思路去完善代码和设计,整个系统的质量会有质的提升。希望这篇文章能帮你把这个项目做得扎实,也能在你的答辩和求职路上真正派上用场。