Spring Boot疫苗接种预约系统毕业设计:从需求拆解到并发控制全解析 做Spring Boot疫苗接种系统当计算机毕业设计这个选题在近两年算是非常主流了。我辅导过不少学生做类似的管理系统课题自己也维护过一套生产环境里跑着的预约类服务。疫苗接种这个场景其实很有意思表面上是个典型的CRUD系统真做起来却涉及预约并发、状态流转、消息提醒、权限管理好几个容易翻车的点做好了完全能撑起一篇像样的毕设论文。这篇就把我从需求拆解到代码落地的完整过程捋一遍包括表结构怎么设计、并发怎么防超约、答辩喜欢问什么给正在做或者准备做这个题目的同学一个能直接上手的版本。1. 这个系统到底要解决什么问题1.1 从社区接种场景倒推需求清单我在实际工作中接触过基层卫生服务站的信息化系统很多接种点以前是Excel记名单、电话约时间、接种当天人工核对汇总数据靠月底手动统计。毕设选疫苗接种系统本质就是把这条手工流程数字化所以第一步不是急着建工程而是把真实场景里的角色和动作拆出来。一个完整的社区疫苗接种场景里有四个关键角色在协作居民用户查疫苗、选时间段、在线预约、到点接种、之后查自己的接种记录。接种医生看到当天预约名单、核对身份、录入接种信息、判断有没有禁忌症。管理员维护疫苗批次和库存、发布到苗信息、审核预约、看各类统计报表。系统本身承担规则引擎的角色比如每个时间段限约多少人、两针间隔是否满足、到期未接种怎么提醒。把这四个角色列出来之后功能模块基本就清晰了用户端的注册登录、疫苗浏览、预约下单、记录查询医生端的当日预约看板、接种登记管理端的疫苗批次管理、预约管理、数据统计。再加上公告管理和健康申告这些辅助模块整个系统的功能边界就有了。1.2 必备功能和加分项怎么取舍毕设和真实商用系统的差别在于你需要控制工作量同时还得让论文有东西可写。我把这个系统的功能分成两档大家按自己时间和精力选。必备功能保证主流程跑通用户注册登录建议手机号加密码配上角色字段。疫苗列表展示至少包含疫苗名称、厂家、剩余库存。预约功能选择日期和时段后提交预约状态能流转。接种记录登记医生端录入已接种并生成记录。管理员维护疫苗信息和预约审核。加分功能答辩亮点建议至少做一两个二维码预约核销到现场扫一下就能完成确认。第二针到期提醒用定时任务扫描应种未种人群。健康申告问卷预约前填写几个基础问题。ECharts统计图表按疫苗种类、每日接种量出可视化报表。Redis缓存热点数据比如疫苗库存和当日预约数。我的建议是把疫苗预约 接种记录 管理后台闭环做扎实再挑一到两个加分项做深这比功能列一大堆但每个都很糙要划算得多。2. 技术架构与数据库设计先把底子打牢2.1 技术栈选择为什么Spring Boot是合理答案这个项目选Spring Boot除了它是目前Java后端的事实标准之外更实际的原因是答辩的时候你几乎不需要解释为什么要选它。自动配置、内嵌Tomcat、Starter生态这三个特性让项目搭建成本极低一个空的Web工程几分钟就能跑起来。我给这个课题推荐的组合是Spring Boot 2.x网上资料最多遇到报错一搜就有答案生产环境大量部署的也是2.7这个版本线。MyBatis-Plus单表CRUD几乎不用写SQL分页插件直接用写论文时数据访问层代码量少而且它的逻辑删除和自动填充能让代码简练不少。MySQL 8.0存储业务数据注意用utf8mb4字符集建表时引擎选InnoDB。Redis不强制但如果你做了库存缓存和分布式锁这是一个非常好的答辩加分点。前端Vue 3 Element Plus或者Thymeleaf都行。我推荐Vue因为预约系统的交互选日期、选时段、看余量用前后端分离方式做展示效果明显更好。2.2 后端分层和包结构照着搭就行一个清晰的包结构不仅代码好写论文里画架构图也轻松。我习惯这样组织com.example.vaccine ├── controller # 接口层接收参数、返回结果 ├── service # 业务逻辑层事务边界在这里 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端交互对象避免直接用实体 ├── vo # 返回给前端的视图对象 ├── config # 配置类拦截器、跨域、WebMvc ├── common # 公共类统一返回结果、异常处理、常量 └── utils # 工具类JWT等controller层只做参数接收和结果包装service层承载业务规则mapper层做数据访问。这个分层的价值在预约接口上体现得最明显AppointmentController里十几行代码真正扣库存的业务逻辑都在AppointmentService的createAppointment方法里事务和锁都加在service上。2.3 核心表结构五张表撑起整个系统表设计是答辩必问环节我按实际使用情况把核心表列出来字段不追求多但每一张都要能讲出设计理由。用户表 user字段类型说明idbigint主键usernamevarchar(50)用户名唯一passwordvarchar(100)存bcrypt加密后的密文real_namevarchar(50)真实姓名id_cardvarchar(18)身份证号phonevarchar(20)手机号rolevarchar(20)取值USER/ADMIN/DOCTORcreate_timedatetime创建时间密码用BCryptPasswordEncoder加密存储这是安全方面最基础也最容易被问的一步答辩时一定要能说出来。疫苗表 vaccine和时间段表 vaccine_slot疫苗表记录疫苗的通用信息比如名称新冠/流感/HPV等、厂家、剂次类型。关键点在vaccine_slot这张表它把哪一天哪个时间段开放多少名额单独拆了出来字段类型说明idbigint主键vaccine_idbigint关联疫苗slot_datedate接种日期start_time / end_timetime时段起止capacityint该时段总容量booked_countint已预约人数versionint乐观锁版本号把容量和已约数放在一张单独的时段表里是防超约设计的前提后面我会专门讲这里面的并发处理。预约表 appointment字段类型说明idbigint主键user_idbigint预约人slot_idbigint关联时段vaccine_idbigint关联疫苗appointment_novarchar(32)预约号用于核销statusvarchar(20)PENDING/ CONFIRMED / COMPLETED / CANCELLEDcreate_timedatetime预约时间状态字段是这张表的灵魂我用四个状态串联主流程用户提交后是PENDING管理员或现场确认后变CONFIRMED接种完变COMPLETED用户取消或者逾期则置为CANCELLED。论文里的流程图、状态转换表都可以围绕它展开。接种记录表 inoculation_record这张表记录每一次真实的接种动作字段包括关联的预约、用户、疫苗、接种医生、接种部位、第几剂、实际接种时间。注意它和appointment是一对一关系但设计成独立表的好处是后续扩展历史查询、统计报表都方便。健康申告表 health_declaration可选预约前填写的几个问题比如近期有无发热、是否过敏体质字段简单但做出来很贴近真实业务答辩时加分。3. 核心业务闭环预约、核销、接种、提醒3.1 预约流程设计状态机是核心整个系统的业务主线就是预约到接种的闭环。我用一个状态图来理清流程然后在代码里严格按这个状态机控制流转用户选疫苗和时段后创建预约状态为待确认医生端核销或管理员确认后转为已确认接种完成后写入记录并标记已完成用户主动取消或者预约时段过期未核销则自动标记已取消。状态设计时有一个容易忽略的点取消权限的边界。我建议只允许用户在待确认状态下取消预约已确认甚至已完成的记录不允许再取消这在service层做一个状态判断即可同时要给前端返回明确提示否则用户会以为系统报错了。健康申告建议放在预约提交的前置步骤用户先填写申告问卷服务端做一次最简单的规则判断比如体温超过37.3℃直接拦截通过后才能选择时段。这既是业务合理性也是论文里可以写的规则引擎体现。3.2 余量扣减与并发控制最容易被问的技术点预约系统最经典的坑就是超约。想象一下某个时段剩下最后一个名额两个用户同时点击预约如果代码逻辑是先查一下booked_count小于capacity就插入两个请求都查到了同样结果结果都插入成功数据库里就出现了超卖。我在这个项目里用了两套方案按项目的并发预期来选择方案一数据库条件更新最推荐给毕设直接在更新语句里带上容量条件数据库行锁保证原子性UPDATE vaccine_slot SET booked_count booked_count 1 WHERE id ? AND booked_count capacity返回影响行数等于1说明扣减成功等于0说明名额已满。这是最简单可靠的防超约方式不依赖额外中间件代码也容易解释。对应MyBatis-Plus实现Update(UPDATE vaccine_slot SET booked_count booked_count 1 WHERE id #{slotId} AND booked_count capacity) int deductStock(Param(slotId) Long slotId);配合service方法上的Transactional先扣减库存、再插入预约记录任何一步失败都会整体回滚。方案二Redis Lua 脚本加分项如果论文里想展示高并发处理能力可以用Redis做库存预扣。Lua脚本保证检查余量和扣减两步原子执行再把结果异步同步回MySQL。这个方案适合讲秒杀场景下的架构优化但注意必须说清楚它带来的一致性问题Redis和MySQL之间需要补偿机制。提示如果用了Redis扣库存方案一定要在论文中解释最终一致性和库存对账的概念。答辩老师对只加缓存、不讲一致性问题的回答非常敏感。3.3 管理员端和医生端把主流程收口系统的管理端不应该是一个独立的高大上平台而是和用户端共用一套登录体系靠角色字段区分菜单权限。Spring Boot里可以用拦截器加注解的方式实现比如自定义RequireRole(ADMIN)注解在拦截器中解析JWT里的角色信息权限不够直接返回403。医生端当天的工作台是主流程的关键页面展示当天所有已确认预约的列表按时间段排序每条记录能一键切换到接种登记。做这个功能时思路很简单但有一个细节值得处理一下——核销二维码。预约创建时生成一个带预约号的二维码图片用Hutool或者ZXing都行医生扫码就能查出预约并进入登记页。这个功能演示效果好论文截图也好看工作量不大。3.4 第二针到期提醒用定时任务实现疫苗按剂次接种的场景下提醒功能是天然的刚需。用Spring Boot的Scheduled就能实现一个轻量级的到期提醒任务Component public class ReminderTask { Scheduled(cron 0 0 8 * * ?) public void sendReminder() { // 查询应种未种找到已完成第一针、且按间隔要求已到第二针时间的用户 ListDueUser dueList appointmentService.findDueSecondDose(); dueList.forEach(u - { // 实际项目中发送短信、微信模板消息 // 毕设建议落库为站内信或公告记录表方便演示 }); } }这里的核心计算逻辑是到期判断第一针接种日期加上疫苗规定的间隔天数比如14天小于当前日期且没有第二针记录就是需要提醒的人群。这要求vaccine表里有interval_days字段我在设计时专门留了这个字段它也是答辩里一个不错的业务细节。4. 关键代码落地从登录鉴权到预约接口4.1 基于JWT的登录鉴权前后端分离项目里JWT几乎是标配方案。我在这个项目里用的是jjwt库整体流程是登录成功后生成token前端放到请求头Authorization里后端拦截器解析并放行。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register); } }JWT工具类的核心方法public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }拦截器里对preHandle方法做token解析验证失败返回401同时把userId和role塞进request attribute后续controller里可以直接取。使用token时的一些避坑点token密钥不要硬编码在业务代码里放到application.yml配置中。利用ControllerAdvice做全局异常处理把JWT失效和参数校验失败统一返回一个规范的JSON结构前端处理起来会舒服很多。不要在自定义拦截器里直接输出response.getWriter()而不设置返回格式前后端联调时最容易在这种细节上报错。4.2 预约接口一整个流程在一段代码里预约提交是整个项目里业务逻辑最集中的接口。我用一段伪代码级的实现来说明当时的思路Transactional(rollbackFor Exception.class) public Appointment createAppointment(Long userId, Long slotId, HealthDeclaration declaration) { VaccineSlot slot vaccineSlotMapper.selectById(slotId); if (slot null || !slot.getSlotDate().isAfter(LocalDate.now())) { throw new BizException(该预约时段不存在或已过期); } // 防超约条件更新 int result vaccineSlotMapper.deductStock(slotId); if (result 0) { throw new BizException(该时段预约已满请选择其他时段); } // 保存健康申告 declaration.setUserId(userId); healthDeclarationMapper.insert(declaration); // 创建预约记录 Appointment appointment new Appointment(); appointment.setUserId(userId); appointment.setSlotId(slotId); appointment.setVaccineId(slot.getVaccineId()); appointment.setStatus(AppointmentStatus.PENDING); appointment.setAppointmentNo(generateAppointmentNo()); appointmentMapper.insert(appointment); return appointment; }这里有几个细节我特别想提醒事务与锁的顺序。Transactional加在service方法上数据库条件更新依赖的是行锁事务提交时才释放所以整个流程走完再提交是安全的。但要注意不要在事务里做耗时的外部调用比如发短信否则会拉长行锁持有时间并发上去就会锁等待。接口幂等性。如果前端网络超时用户可能重复提交请求导致同一用户同一个时段预约多条。解决方案很简单在预约表上加一个user_id slot_id的唯一索引插入时捕获DuplicateKeyException转成友好提示。这一个细节能体现出你对真实业务的理解写论文时也值得写一小段。**日期判断要规范。**我见过不少同学用字符串比较判断日期结果格式不一致导致判断错误。正确做法是数据库字段用date类型Java实体里用LocalDate比较时直接用isAfter、isBefore这些API。4.3 接种记录登记与统计报表接种登记的接口逻辑相对简单但有一个事务边界需要注意登记信息和预约状态更新必须在一个事务里否则会出现记录有了但预约还是待确认的脏数据。Transactional public void recordVaccination(RecordRequest request) { // 校验预约状态必须是CONFIRMED Appointment appointment appointmentMapper.selectById(request.getAppointmentId()); if (!AppointmentStatus.CONFIRMED.equals(appointment.getStatus())) { throw new BizException(预约状态不允许接种登记); } // 插入接种记录 InoculationRecord record buildRecord(request); inoculationRecordMapper.insert(record); // 更新预约状态 appointment.setStatus(AppointmentStatus.COMPLETED); appointmentMapper.updateById(appointment); }统计报表部分建议用MySQL的GROUP BY加时间函数实现比如近7天每日接种量SELECT DATE(vaccinated_at) AS day, COUNT(*) AS count FROM inoculation_record WHERE vaccinated_at DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(vaccinated_at)把统计接口包装成一个/api/admin/statistics/daily前端用ECharts画折线图或柱状图视觉效果和答辩说服力都很好。5. 常见问题与排查实录这些坑我都替你踩过5.1 循环依赖与序列化报错最经典的问题User实体里有ListAppointmentAppointment实体里又有User对象接口一返回JSONJackson直接报Infinite recursion堆栈溢出。出现这个问题的根源在于懒加载和没有处理好关联关系。我的经验是实体类的关联字段不要直接返回给前端。接口层统一用DTO/VO组装数据比如预约列表VO里只放用户姓名、疫苗名称、状态这些展示字段没必要把整个嵌套对象全序列化。这样既能规避循环引用也避免了把不敏感的数据库字段暴露出去。如果确实要直接返回实体暂时的救急办法是给字段加JsonIgnore或JsonIgnoreProperties但这不是好方案会让其他需要该字段的场景也失效。毕设代码我建议直接用VO方案这也是真正生产项目的标准做法。5.2 时间字段的时区与格式问题另一个高频报错是日期时间对不上比如预约日期存储后差了8个小时或者前端传2024-05-20 10:00:00后端解析失败。根本原因有两类一是Date类型配合Timestamp在JDBC和MySQL之间有时区转换问题二是前后端传递时间用了错误格式。我的建议是数据库时间字段统一用datetimeJava实体用LocalDateTime中间层别用Date。application.yml里加jackson时间格式配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8数据库连接串上显式设置时区url: jdbc:mysql://localhost:3306/vaccine_db ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai就这3个配置能省掉一大半跟时间有关的玄学报错。5.3 前端跨域问题和联调技巧前后端分离时Vue跑在8080端口Spring Boot跑在8081端口浏览器直接请求会报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)和allowedOriginPatterns(*)在较新版本Spring里是可以共存的但老的Spring版本里allowedOrigins(*)不能和allowCredentials(true)一起用。网上很多旧帖子的配置会直接报错遇到的话换成allowedOriginPatterns即可。联调时我还会让同学在Controller里加CrossOrigin作为备用方案但生产上不推荐这么做因为每个接口都加注解太啰嗦而且代码看着乱。5.4 答辩高频问题与应答思路这个课题的答辩问题我整理了出现频率最高的几类提前准备基本能覆盖问题应答要点为什么选Spring Boot自动配置、Starter生态、嵌入Tomcat简化部署、Java生态成熟数据库为什么这三张表这么设计从业务流程出发说明每张表负责什么为什么拆时段表、独立记录表怎么防止同时预约超员讲条件更新SQL说明数据库行锁机制和原子性系统安全性怎么考虑的密码加密、JWT鉴权、角色权限拦截、参数校验如果上线后用户量很大怎么办引Redis缓存、读写分离、接口限流落到具体方案即可答辩核心思路就一条所有设计决策都能讲出为什么。哪怕答案是因为简单你也可以说作为课程设计选型首要考虑可靠性和可维护性MyBatis-Plus在满足需求的前提下显著减少了样板代码让业务逻辑更聚焦这比简单回答大家都用要加分得多。6. 我的一些实际操作心得和后续扩展建议带这个课题带了这么多轮最后分享几点实在的体会。第一先把数据库和三张核心表设计好再写代码。我发现很多同学上来就搭项目写Controller写到预约业务发现表结构支撑不了状态流转又回头改表白白浪费两天。这个系统的数据模型不算复杂花一晚上把表建好、字段约束定清楚后面写代码会非常顺畅。第二演示环境一定要准备干净。答辩演示最怕当场报错我建议预约、核销、接种记录、统计页这些关键流程提前录一遍视频同时也准备一套干净的演示数据。注意把系统时间调整到有可预约时段的近期日期我见过不止一个同学因为演示当天的日期和录入的预约日期错开列表一片空白手忙脚乱现场改数据。第三论文里一定要画三张图功能结构图、系统架构图、业务流程图而且图的层级要和实现的代码严格对应。很多老师翻论文就找这三张图一眼能看出工作量。图中的专业术语要和代码注释保持统一比如状态名PENDING、COMPLETED在图上和代码里写同一套。后续要扩展的话这个题目还可以加线上疫苗知识科普模块、社区公告的推送、家庭成员的代预约功能grandma不会用手机子女帮她约、疫苗不良反应上报。这些都是真实世界里社区接种服务的实际需求选任意一个延伸成毕业论文的研究方向都比为了凑字数硬写强得多。最后提醒一句做这个项目的时候每个核心决策都记一下当时为什么这么做哪怕是我试过A方案失败了才换B方案也很好。答辩时讲述设计过程中的权衡和失败经历往往比展示堆满功能的成果更能打动老师因为这体现的是真正的工程思维。