计算机毕业设计选“Spring Boot 疫苗接种系统”的人真不少前几天还有学弟问我这个题该怎么做。表面上看就是一个预约加接种记录管理但社区疫苗预约与接种管理平台一旦落到真实场景要把用户预约、疫苗库存、接种点排班、核销记录、统计报表全部串起来工作量并不小。这篇文章就基于我带过的实际项目把整套系统的整体拆分、核心数据表、关键代码实现、部署和答辩需要注意的坑全部过一遍适合拿来做毕设也适合想快速上手一个完整 Spring Boot 实战项目的开发者参考。先给还没动手的人一个善意提醒不要再把思路停留在“用户注册、管理员增删改查”这种两层结构上。这套系统真正的难点在于多角色业务闭环、疫苗库存并发控制和预约状态流转。下面我会按实际开发顺序讲而不是按教材目录讲。1. 项目整体设计与需求拆解1.1 系统定位这不是一个普通的CRUD很多同学看到“疫苗接种系统”第一反应是“疫苗表 接种记录表”然后就开始写代码。但社区疫苗预约与接种管理平台明显是一个面向三类用户的业务系统普通居民需要在线预约、查看疫苗库存、收到接种通知接种点工作人员需要核销预约、登记接种、录入不良反应平台管理员需要维护疫苗信息、设置接种点、查看全量统计。这意味着你需要划分三种角色戴上角色视角去设计功能而不是把所有接口堆在同一个 Controller 里。我在给学弟做技术评审时发现凡是后期返工严重的项目几乎都是从“所有用户共用一套接口”开始的。推荐的模块划分是用户端注册登录、疫苗查询、预约登记、我的预约、取消预约、接种记录查询接种点端当日预约列表、核销确认、接种登记、留观计时、异常情况记录管理端用户管理、接种点管理、疫苗管理、库存调整、预约规则配置、数据看板这三个端可以共用同一个后台项目用 Spring Security 或自定义拦截器按权限分流但接口路径和逻辑必须分开。毕设答辩时评委最常问的就是“如何做权限控制”这块你能讲清楚基本能拿到一半的分数。1.2 核心业务流程预约、接种、核销、记录在动手写代码前一定要先把主流程画清楚否则后面编码时会频繁改表结构。我这边经过几轮落地改动最后沉淀下来的核心流程是用户先注册登录在疫苗列表中选择接种点看到当前可预约的日期和剩余号源选定时间段后提交预约系统校验“是否已预约过同类疫苗”“是否在限制周期内”预约成功生成预约单状态为待接种用户到现场后工作人员根据预约编号或用户二维码进行核销状态变成已核销接种完成后创建接种记录用户可在“我的记录”中查询到第几针、疫苗批号、接种时间。这个流程里最关键的是状态的流转。我建议把预约单状态用枚举维护而不是散落在代码里的魔法数字。状态机如下待接种0用户提交预约成功等待现场核销已核销1工作人员核销完成准备接种已完成2接种信息已登记整个流程闭环已取消3用户取消或超时未到自动取消已过期4预约日期已过且未核销系统自动标记实际上“已过期”和“已取消”可以合并但分开的好处是数据统计时能清楚看到“爽约率”。爽约率这个指标在毕设答辩时很好用因为它是评委能感知到的“真实业务思考”比单纯讲 CRUD 有说服力得多。1.3 技术选型Spring Boot 3还是2.7MyBatis Plus够不够用标题里写了 Spring Boot我建议直接用 Spring Boot 3.x JDK 17如果学校环境只支持 JDK 8那就退到 Spring Boot 2.7.x。选型逻辑很简单毕设项目要的是稳定性、生态资料多、问题能被搜索引擎快速解决。Spring Boot 3 现在已经很稳网上踩坑记录也很多不会卡住你。持久层我推荐 MyBatis Plus而不是 MyBatis 原生或 JPA。原因就两条分页插件很好用避免手写大量 XML条件构造器可以直接拼查询条件比如后台按“疫苗名称 日期 状态”组合筛选一两行代码搞定。假如你用原生 MyBatis这种组合查询的 SQL 会写到你怀疑人生。前端方案上我的建议是不要硬上前后端分离。毕设项目如果时间紧直接在 Spring Boot 里用 Thymeleaf 渲染后台页面用户端用一个 H5 页面包起来稍作适配就能在手机浏览器里演示。你要选 Vue 反而容易把自己拖进“跨域”“Token 过期”“打包部署”的坑。不要贪多先跑通全流程。2. 数据建模与数据库表设计2.1 用户体系单表加角色字段还是RBAC三张表很多毕设喜欢做 user、role、user_role 三张表理由是“显得规范”。但疫苗接种这类系统角色很固定做成 RBAC 反而让权限判断变复杂。我的做法是user 表加一个 role 字段用 tinyint 存角色类型0 普通用户、1 接种点工作人员、2 管理员。查询当前用户时只做一次判断轻量直接。真正需要拆表的是用户扩展信息。普通用户要记录姓名、身份证号、手机号工作人员要关联接种点 ID。如果你全都塞进 user 表表会变得臃肿。但毕设为了演示方便也可以不分用字段冗余解决。从“能跑通”和“好答辩”两个角度讲user 表设计成下面这张就够了。字段包括id、username、passwordBCrypt 加密、real_name、id_card、phone、role、vaccine_point_id工作人员关联接种点、create_time、update_time。登录名用手机号或用户名都可以身份证号只做实名展示不在登录逻辑里用。密码千万别存明文答辩时评委看到明文密码印象分会掉很多。2.2 疫苗与接种点资源建模疫苗信息表和接种点表是资源侧的实体也要提前设计好。vaccine 表至少要有vaccine_name、manufacturer、dose_total、interval_days、stock、status。疫苗的批次信息我建议单独建表 vaccine_batch记录 batch_no、production_date、expire_date、quantity因为实际社区接种时同一名称的疫苗可能有不同批号接种记录要精确到批号。接种点表 vaccine_point 要包含point_name、address、open_time、max_count_per_day、enabled。这里有个容易忽略的点每个接种点当天的剩余号源不是简单地等于库存而是等于“每日最大预约量 - 当日已预约人数”。这个值需要在预约接口里动态计算不能直接存库否则并发下会脏读。实体核心字段说明vaccinevaccine_name, manufacturer, dose_total, interval_days, stock疫苗基础信息与总库存vaccine_batchbatch_no, production_date, expire_date, quantity批次追踪精确到批号vaccine_pointpoint_name, address, max_count_per_day, enabled接种点与每日预约上限appointment_orderorder_no, user_id, vaccine_id, point_id, appointment_date, status预约主表状态流转核心appointment_recordorder_id, batch_no, inject_position, operator_id接种记录与预约对应2.3 预约单状态机和关键索引预约单表 appointment_order 是整个系统的心脏核心字段order_no、user_id、vaccine_id、batch_id、point_id、appointment_date、time_slot、status、cancel_reason、create_time、update_time。很多人忽略索引设计数据量一上来后台筛查预约记录就非常慢然后被评委问到性能优化时哑口无言。推荐加两个联合索引idx_user_dateuser_id, appointment_date用于查某用户某天的预约idx_point_statuspoint_id, appointment_date, status用于统计接种点当日各状态数量。状态字段不要用字符串“已预约”“已完成”直接存存整数枚举显示时再映射中文。我给学弟改过一次代码他直接在 Java 枚举里写了“PENDING”等但 Redis 缓存里又存了中文最后排查了半天。规范做法是数据库存 int枚举类里做转换前端展示通过接口返回的文本字段。状态值状态名触发时机0待接种用户提交预约成功1已核销工作人员现场核销2已完成接种记录已生成3已取消用户主动取消4已过期定时任务标记未到者2.4 核心建表SQL参考这里给一套简化但能直接用的核心 DDL有两个点要注意一是 order_no 要唯一建议用雪花 ID 或时间戳加随机数二是所有金额相关字段用 decimal疫苗虽然很多是免费但自费疫苗也存在。预约表 SQL 大致如下CREATE TABLE appointment_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 预约单号, user_id bigint NOT NULL, vaccine_id bigint NOT NULL, batch_id bigint DEFAULT NULL, point_id bigint NOT NULL, appointment_date date NOT NULL, time_slot tinyint NOT NULL COMMENT 0上午 1下午 2全天, status tinyint NOT NULL DEFAULT 0 COMMENT 0待接种 1已核销 2已完成 3已取消 4已过期, cancel_reason varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_date (user_id, appointment_date), KEY idx_point_status (point_id, appointment_date, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我用的是 utf8mb4不是 utf8。原因不用多说现在谁还存不了 emoji但在 MySQL 8 里 utf8mb4 是默认老项目还喜欢 utf8插入生僻字姓名时就容易报错这种细节也值得在答辩时提一句。接种记录表 appointment_record 和预约单是 1 对 1 关系但如果一针对应一条记录一个预约单也只能对应一条接种记录。字段上增加 vaccine_name、batch_no、inject_position、adverse_reaction、operator_id这样在数据统计和溯源时就有据可查。3. 核心功能实现与踩坑记录3.1 用户登录鉴权JWT拦截器的正确写法用 JWT 做前后端接口鉴权是毕设项目的主力方案比 Session 更适合分布式部署演示。具体做法用户登录成功后用 userId 和 role 生成 token返回给前端前端每次请求在 Header 里携带 Authorization: Bearer xxx后端写一个拦截器解析 token并把用户信息放进 ThreadLocal 或 request attribute。这里有个很多新手会踩的坑拦截器写好了但静态资源和登录接口也被拦截了。我习惯的做法是写一个 HandlerInterceptorexclude 掉 /user/login、/user/register、/vaccine/list、/static/**、/doc.html 这些公开路径。Spring Security 如果配置复杂不如直接用拦截器减轻学习成本和出错的概率毕设完全够用。JWT 本身有一个“无法主动失效”的问题用户修改密码后旧 token 仍然有效。毕设项目里可以不做处理但答辩时如果被问到你可以说方案是加一层 Redis 黑名单或者把 token 版本号存进用户表。能说出这个改进点会显得你考虑过安全问题。3.2 疫苗库存扣减乐观锁防超卖预约时最怕出现“同一个人重复预约”和“库存超卖”。库存超卖的核心原因是多个请求同时读到 stock10同时判断大于 0然后同时扣减。要解决必须用数据库层面的原子操作。我的做法是 update 语句带上库存条件比如int rows vaccineMapper.update(null, new LambdaUpdateWrapperVaccine() .set(Vaccine::getStock, vaccine.getStock() - 1) .eq(Vaccine::getId, vaccineId) .gt(Vaccine::getStock, 0)); if (rows 0) { throw new BusinessException(疫苗库存不足); }这条语句在 MyBatis Plus 里直接调用即可返回的 rows 表示影响的行数。如果并发请求同时进来只有一个能更新成功其余影响行数为 0就能拿到“库存不足”的异常。这比普通的 select-then-update 要安全得多而且不用引入悲观锁代码量很小。还要注意库存扣减和预约单生成必须在同一个数据库事务里否则会出现预约单建好了、库存没扣减的情况。事务注解加在 Service 层方法上并且要避免同类内部调用 this.save() 导致事务失效这个细节后面单独讲。3.3 预约时段控制与重复预约拦截重复预约的校验很多人只想到在代码里 select 查一遍但并发下两个请求同时查到没有预约记录都会通过校验最后产生两条订单。最稳妥的做法是建一个唯一索引让数据库兜底。我的建议是给 appointment_order 加一个 user_id vaccine_id appointment_date 的联合唯一索引表示“同一个用户同一天不能针对同一种疫苗预约两次”。但要注意不同疫苗同一天可能都可以约所以索引不能只放在 user_id appointment_date 上这里要和业务正确对齐。另一个容易忽略的是“两针间隔天数”校验。因为疫苗表里有 interval_days所以用户在预约第二针时需要查一下最近一次接种该疫苗的记录如果当前日期减上次接种日期小于 interval_days直接拒绝。这个逻辑我建议放到 Service 层做不要在 Controller 里写一堆 if否则慢慢就变成面条代码。3.4 接种核销与记录生成现场核销推荐用“预约单号 手机号后四位”的组合比单纯扫二维码更稳定。毕设项目一般没有真扫码枪用 H5 页面输入单号也能演示。核销接口的逻辑是根据单号查出预约单判断状态是否为待接种如果是改成已核销并返回前端确认信息随后工作人员填写接种信息生成接种记录。这里要处理好“核销和接种记录生成”两个操作的关系。我的建议是把核销动作和接种记录生成放到同一个事务中或者至少保证核销成功后记录一定能生成。因为实际场景里可能“核销了但没打成针”需要人工处理。但毕设可以简化核销即接种但记录要现场填写完整。接种记录里 operator_id 一定不要用 session 里的 userId 直接塞应该用当前登录人的角色判断“是否有接种操作权限”如果没有直接抛 403。很多同学权限校验只做菜单隐藏不校验接口这是不安全的也容易在答辩时被追问。4. 管理后台与数据统计4.1 后台接口设计RESTful约定的取舍管理后台接口建议统一前缀 /admin用户端接口统一 /api避免权限拦截规则混乱。比如POST /api/user/login 用户登录GET /api/vaccine/list 查询可预约疫苗POST /api/appointment/create 创建预约POST /admin/appointment/cancel 管理员取消预约GET /admin/stats/daily 每日预约/接种统计统一返回体也很重要我一般用 ResultT包含 code、message、data 三个字段。全局异常处理器把业务异常也转成统一返回前端不会拿到乱七八糟的堆栈信息。代码很固定但少了它在对接前端、调试 Postman 时会有很多麻烦。我的一个心得是给管理员接口单独做一个“操作日志”AOP 切面记录谁在什么时间对哪个接口做了什么操作。这样演示时后台能看到操作日志列表答辩时讲“操作审计”很有说服力实现也不复杂也就是一个自定义注解加 AOP 拦截。4.2 接种数据统计按日、按疫苗、按接种点数据统计是管理后台的重头戏千万别等到最后再做。我见过不少学弟把预约流程写完统计模块只放一个空的 ECharts 页面结果演示时无法展示数据非常尴尬。最基础的统计接口要覆盖三个维度按日统计预约量和接种量按疫苗统计各疫苗预约占比按接种点统计每日核销人数。SQL 写法上用 GROUP BY date(create_time) 会很简单但要注意 MyBatis Plus 的分页插件和 GROUP BY 联用时的 Total 问题。我建议统计接口不要用分页插件直接返回 List数据量也不大避免 Total 被错误改写。如果使用 ECharts 展示后端返回的数据结构可以直接组织成 xAxis series 两个数组前端少做一层转换。比如按日统计{ dates: [2025-04-01, 2025-04-02], appointmentCount: [35, 48], vaccinatedCount: [30, 42] }这样无论你写 JS 还是用 Vue渲染都很顺畅。不要把后端返回明细记录让前端自己去聚合既浪费带宽又增加前端复杂度。4.3 基础监控与定时任务Spring Boot Admin和Scheduled毕设做到这个程度再加一个“系统监控”模块非常加分。可以选择引入 Spring Boot Admin服务端注册一个 admin 项目业务模块作为 client 暴露 actuator 端点就能看到内存、线程、健康状态等实时数据。做法不复杂但能撑起一个“平台运维”的演示点。定时任务用 Scheduled 就能满足需求。系统里最常见的定时场景是每天凌晨清理“过期未核销”的预约单把 status 从待接种变成已过期并释放当天的可预约名额。要注意定时任务里修改大量数据时尽量分批更新避免一次性 update 所有历史记录导致锁表时间过长。我用过的一个经验在定时任务开始和结束都打印一条日志记录处理条数与耗时。因为毕设演示时如果定时任务出问题你至少能从日志里快速定位不然连“有没有执行”都不知道。5. 常见问题与排查实录5.1 事务失效this调用与代理对象最典型的场景是 AppointmentServiceImpl 里有一个 public 方法 createAppointment()它内部直接调用了 this.updateStock()、this.insertOrder()结果其中一个失败库存还是被扣了。原因就是 Spring 事务是基于 AOP 动态代理的同类内部调用不会经过代理对象所以 Transactional 注解失效。排查办法是自己注入自己在 Service 里注一个 AppointmentService self然后用 self.updateStock() 这种写法或者干脆把涉及多表写操作的方法拆到独立 Service再注入那个 Service。如果是老代码可以用 AopContext.currentProxy()但初学者不推荐。5.2 Scheduled定时任务没生效有几种可能主启动类没加 EnableScheduling任务方法被 private 修饰或者任务抛出异常后没有日志。最隐蔽的是默认单线程执行如果上一个任务没跑完下一个任务就排队遇到大量数据会感觉“定时任务偶尔失灵”。我建议在配置里指定线程池或者用 Scheduled(cron) 的同时任务方法内部用 try-catch 包住所有业务逻辑防止异常把任务线程干掉。这个坑连续坑了我两次都是在演示前一天发现定时清理没跑最后发现是异常被吞了。5.3 时区问题导致预约日期错乱数据库连接串如果不加 serverTimezoneAsia/Shanghai而服务器和本地时区不一致你会碰到“预约日期显示成前一天”的灵异问题。另外前端传“2025-05-01”这个字符串后端如果直接 LocalDate.parse不会有问题但如果你用 Date 类型接收跨时区转格式时就容易出偏差。解决方案是统一用 LocalDate/LocalDateTime并在 application.yml 里配置 jackson 的时间格式。这是一个小而关键的经验但我见过不下五个项目在这里栽过跟头值得单独写进避坑列表。5.4 本地能跑部署到服务器就404常见原因是静态资源和前端页面打包路径不对。如果用了 Thymeleaf把页面放在 templates 目录下是对的如果用了 Vue 打包后的 dist要记得放到 static 或配置资源映射。还有一点Spring Boot 的默认 context-path 如果设置了 /api注意前端请求是否都要带这个前缀我帮别人排查过项目本地能跑是因为前端代理部署后代理没了所有请求全 404。部署时我建议直接用 java -jar 启动不要用外置 Tomcat。然后通过 nohup 或者 systemd 做后台运行设置环境变量区分 dev/prod 两个 profile。服务器上端口、数据库密码通过环境变量注入不要把生产配置硬编码在 application.yml 里。5.5 常见问题速查表问题典型现象推荐解决办法事务失效一个方法里 this 调用另一个事务方法注入自身代理或拆分 Service定时任务不执行日志无输出状态未更新加 EnableScheduling方法 publictry-catch 包逻辑日期多一天预约日期变成前一天数据库连接加 serverTimezone统一 LocalDate部署后 404本地接口正常服务器请求所有路径 404检查 context-path、静态资源目录、前端代理库存超卖库存显示负数预约单多于库存update set stockstock-1 where stock06. 毕设答辩、扩展与个人经验总结6.1 演示路径与答辩高频问题答辩演示不要从注册开始慢慢走时间不够。我建议的演示顺序是先用管理员账号登录展示今日预约总览、疫苗库存和统计图表然后切到普通用户端进行一整个“查询疫苗-提交预约-查看我的预约”流程再用工作人员账号核销生成接种记录最后回管理后台看数据变化。这个闭环在三分钟内能讲完而且每个环节都能被评委看到。高频问题先准备好为什么选 Spring Boot库存超卖怎么解决预约状态怎么流转数据一致性怎么保证如果被问到“如果预约系统访问量巨大怎么办”可以答Redis 缓存热点疫苗库存、MQ 削峰、数据库乐观锁兜底。不要只答“加服务器”太虚。6.2 可以继续加的功能点如果你的时间充裕或者想做出区分度有几个扩展方向性价比很高消息通知模块预约成功或取消时发送邮件或短信疫苗批次追溯扫码查批次有效期预约二维码核销小程序端入口。加一个消息通知就够写一个章节而且功能闭环更完整。我还建议顺手做一个“接种进度提醒”的定时任务比如第二针到期前三天给未接种第二针的用户发短信通知。这个功能既涉及数据查询又涉及外部接口封装答辩时能展示的东西非常多。6.3 写在最后我的实操体会这套系统我从 Spring Boot 2.6 时代做到现在版本换过三轮最深的体会是不要被“单表 CRUD”骗了业务系统真正的复杂度一定在状态流转和并发控制上。我见过太多同学花大把时间调表格样式却在“重复预约”和“库存扣减”上只写了简单的校验最后演示时被评委一追问就露馅。如果你还在起步阶段我建议先按我上面第 2 节的数据表把库建出来哪怕什么都不写先把预约状态机用枚举类定义好后续的代码逻辑会顺很多。最后再提醒一句IntelliJ IDEA 社区版完全够开发 Spring Boot不需要折腾其他版本用 Spring Initializr 初始化项目选好 JDK 版本比对着网上的老教程一步步建 Maven 项目省心得多。祝你的毕设顺利过审答辩不翻车。