最近好些人找我聊同一个选题:毕业设计想做疫苗发布和接种预约系统,Java后端、Vue前端、MySQL做存储。说实话,这个题每年都有人做,但真正能讲清楚“预约不超卖”“库存和批号怎么挂钩”“部署时踩哪些坑”的作品并不多。多数成果停在简单的增删改查层面,答辩时一问业务规则就露怯。这里把我实际开发这套系统时的建模思路、表设计、关键代码和排错笔记整理出来,目标是让拿到同样选题的同学,能少走几周弯路。
1. 这套系统真正难的从来不是CRUD,而是业务规则
很多人拿到需求第一反应是“用户表、疫苗表、预约表,三个表就完了”。等做到一半才发现,疫苗和普通商品不一样,它的库存、有效期、接种剂次都绑定批次。比如同一款疫苗,不同批号的有效期不同,你不能只存一个总数量,否则临期批次根本没法管理。
再往下拆,预约流程里还有两个最容易失控的时刻:一个是超卖,一个是重复提交。用户看到一个有号源的时段,点完预约后库存扣减了,可他一分钟内连点三次,如果后端不做幂等,就会产生三条预约记录;多人同时抢最后一个号,如果用先查后更新的方式,也极易把库存扣成负数。这些才是系统核心复杂度所在。
另外是角色边界。系统至少要有三类人:普通用户负责浏览公告、查疫苗、约号、取消预约;接种护士或工作人员负责按预约名单核销;管理员负责发布疫苗批次、维护号源、管理公告。三者的菜单、接口、页面都该分开。不做权限体系,后面导师一问你“怎么防止用户直接调接口改库存”,你会很被动。
所以我建议从一开始就把系统定义为:SpringBoot + Vue + MySQL的前后端分离项目,后端只写 API,前端负责交互,权限走 Token 校验,库存扣减走乐观锁或 Redis 预减,所有状态流转落到数据库约束上。下面我会按这个思路一步步展开。
1.1 疫苗的“批号”是库存管理的核心维度
疫苗批次通常包含几个关键属性:批号(如 L20240701)、生产日期、有效期、生产厂家、接种方式、剂次信息。要把这些信息落到表里,最常用的做法是拆两张表:
vaccine:疫苗目录,存疫苗名称、适用人群、接种部位、禁忌说明等基础信息;一个疫苗可以对应多个批次。vaccine_batch:批次表,vaccine_id关联疫苗,存批号、生产日期、有效期、总库存、剩余库存。
我做项目时,疫苗详情页会把“当前可预约批次”单独列出来。前端展示剩余库存时优先取批次维度,而不是只展示疫苗总数。因为用户在预约时必须选择具体批号,生成订单时也要把batch_id一并带上,后续核销才能追溯到究竟是哪一批疫苗打给了这个人。
1.2 预约状态机:从“待接种”到“已完成”的流转
预约单不能只有两个状态,至少要拆出四到五个:
PENDING待接种:预约成功,但还没到接种时间。CANCELLED已取消:用户主动取消,或管理员取消超时未接种的预约。VACCINATED已完成:工作人员核销确认已完成接种。NOSHOW爽约:预约了但未按时到场,也未提前取消。EXPIRED已过期:接种时段已过,系统自动置为过期。
这个状态设计在答辩时非常加分。因为导师可以追问:“爽约和取消的区别是什么?”回答清楚“取消是用户主动操作释放号源,爽约是没来且号源作废”,就能体现出你对业务域的理解。为了把状态机落实,建议在appointment表里加一个status字段,所有状态变更都走 Service 层统一方法,不要直接在 Controller 里到处改状态。
1.3 权限模型:用 RBAC 还是直接写死角色
我推荐轻量级 RBAC。表结构是sys_user、sys_role、sys_user_role。接口上通过一个拦截器解析 Token,从 Redis 或数据库里拿到用户角色标识。简单做法是给用户表直接加一个role字段,值为USER、STAFF、ADMIN,配合后端注解校验角色,前端再用动态路由控制菜单显隐。这套模型足以应付毕设课设,也能让代码结构保持清晰。
2. 数据库设计:七张核心表把整条接种链路串起来
这一节我会给出可直接落地的表结构和设计理由,避免大家重复踩我的坑。
2.1 核心表关系与字段参考
我实际项目里主要用了这几张表:
| 表名 | 作用 | 关键字段 |
|---|---|---|
sys_user | 用户登录账号 | id, username, password, real_name, role, status |
vaccine | 疫苗目录 | id, name, manufacturer, applicable_population, dosage, description |
vaccine_batch | 疫苗批次与库存 | id, vaccine_id, batch_no, produce_date, expire_date, total_stock, remaining_stock, version |
appointment | 预约订单 | id, user_id, batch_id, appointment_date, time_slot, status, cancel_reason |
vaccination_record | 接种记录 / 核销记录 | id, appointment_id, operator_id, vaccinated_time, batch_id |
announcement | 公告信息 | id, title, content, create_time |
vaccination_site | 接种点和时段配置 | id, name, address, slots_config |
vaccine_batch表我特意加了version字段,这是乐观锁的关键。每次扣库存时用UPDATE ... SET remaining_stock = remaining_stock - 1, version = version + 1 WHERE id = ? AND remaining_stock > 0 AND version = ?,影响行数为 0 就说明库存不足或版本冲突,操作失败。这样在高并发场景下也能避免负库存。
2.2 唯一索引和约束:把业务规则下沉到数据库
预约最怕重复提交。我的方案是给appointment表建一个联合唯一约束,比如(user_id, batch_id, appointment_date, time_slot)。用户在同一时间段、同一批次上只能有一条有效预约记录。新增预约时直接捕获数据库唯一索引异常,比在代码里先查再插要可靠得多。
另外一个容易被忽略的约束是外键逻辑。虽然项目上不强制建物理外键,但业务层必须保证:删除疫苗前检查是否有未过期批次;删除批次前检查该批次是否有未完成预约。建议在 Service 层写一个checkDeletable()方法,避免产生孤儿数据。
3. 后端实现要点:接口、鉴权与库存扣减
这一节重点讲 SpringBoot 端落地时最容易出错的地方。
3.1 鉴权选型:JWT 还是 Sa-Token
我对做毕设的同学推荐 Sa-Token 或 JWT 二选一。Spring Security 对于课设来说太重,配置链路太长,期末时间不够。JWT 最大的好处是无状态,后端不需要存会话;缺点是强制失效麻烦,但毕设场景里几乎用不到强制下线。Sa-Token 则更贴近国内业务开发习惯,拦截器写起来直观,文档也友好。
我在项目里用的是自定义 JWT 拦截器,核心类是AuthInterceptor。拦截器把token解析出的用户 ID 放到ThreadLocal或请求上下文里,后面 Controller 只要从上下文拿当前用户就行。注意:登录接口要放行,注册接口要放行,其余接口都走鉴权,这是最容易漏掉的一点。
3.2 预约扣库存:乐观锁的代码写法
先看核心 Mapper 方法:
@Update("UPDATE vaccine_batch SET remaining_stock = remaining_stock - 1, version = version + 1 " + "WHERE id = #{batchId} AND remaining_stock > 0 AND version = #{version}") int deductStock(@Param("batchId") Long batchId, @Param("version") Integer version);Service 层调用时,先查到当前批次版本号,再执行更新。扣减成功后才插入预约记录。这里必须加事务:
@Transactional(rollbackFor = Exception.class) public AppointmentDO createAppointment(CreateAppointmentRequest request) { VaccineBatchDO batch = batchMapper.selectById(request.getBatchId()); if (batch == null || batch.getExpireDate().isBefore(LocalDate.now())) { throw new BusinessException("该批次已过期"); } int rows = batchMapper.deductStock(batch.getId(), batch.getVersion()); if (rows == 0) { throw new BusinessException("当前批次库存不足,请选择其他批次"); } AppointmentDO appointment = new AppointmentDO(); // 设置用户ID、批号、日期、时段,状态设为 PENDING appointmentMapper.insert(appointment); return appointment; }有两点提醒。第一,不要把“查库存”和“扣库存”拆成两次查询,一定要用一条带条件的更新语句完成原子操作;第二,@Transactional要加在rollbackFor = Exception.class,否则遇到RuntimeException子类之外的异常可能不会回滚。
3.3 取消预约与核销的配合
取消预约很简单:校验当前用户确实是该预约单的创建者,且状态为PENDING,然后更新状态为CANCELLED,同时把批次剩余库存加回一。这里要注意,加库存必须发生在“取消成功”同一个事务内,不然会出现用户取消成功但号源没有释放的问题。
核销则由工作人员操作,一般输入用户手机号或预约单号,查询出待接种的预约单,核对身份后把状态更新为VACCINATED,同时插入一条vaccination_record。我建议核销接口也要限制操作权限,不能在用户端暴露。
3.4 Service 层事务边界怎么划分
一个经验是:预约单和批次库存表之间的事务同步,不要靠分布式事务,单体项目里用一个@Transactional方法包住两步就够了。但要注意不要在事务里做耗时的远程操作,比如发短信验证码。项目规模不大时,短信可以先做成日志占位,答辩时说明“生产环境会对接短信 SDK”即可。
4. 前端:Vue3 + Element Plus 落地顺序
前端如果从零开始手写会耗费大量时间,尤其是路由、表格、表单这些基础组件。建议直接选Vue3 + Vite + Element Plus + Pinia这套组合,页面代码量能压缩一半。
4.1 页面清单与路由设计
按角色把路由拆成三块:
- 用户端:首页、疫苗目录、公告列表、预约接种、我的预约。
- 工作人员端:待核销列表、核销记录、今日时段统计。
- 管理端:用户管理、疫苗管理、批次管理、号源管理、公告管理、预约统计。
导航菜单根据用户角色的role字段动态渲染。前端不要暴露管理员菜单给普通用户,但真正的安全边界必须依赖后端接口权限。这块我会在后端接口上用注解或拦截器拦截,双保险。
4.2 预约页面如何做得顺手
预约页有几个关键交互:选择疫苗 → 选择批次 → 选择接种时段 → 填个人信息 → 确认提交。时段按钮建议由后端返回的剩余号源决定是否禁用,而不是前端写死日期列表。接口可以返回形如[{date: "2025-06-10", slots: ["09:00-09:30", "10:00-10:30"], available: 5}]的数据结构。
页面上最好加一个“点击一次后按钮变为 loading”的防重复提交逻辑,同时后端再做幂等兜底,两边都不能省。
4.3 与后端对接时的接口约定
统一响应结构是关键:
{ "code": 200, "message": "success", "data": {} }Axios 封装时做三件事:请求拦截器附加Authorization请求头;响应拦截器先判断code,非 200 统一弹错误信息;遇到 401 跳转登录页。这样写完后,后面每加一个模块,写业务的效率会大幅提高。
5. 部署、打包、排错:这些坑我建议你先踩一遍
写代码只算完成了七成,部署时出的问题往往更磨人。这里把我遇到过的几个典型问题列一下。
5.1 MySQL 8.0 连接问题
如果用 MySQL 8 且驱动版本较新,连接的driver-class-name不再是com.mysql.jdbc.Driver,而是com.mysql.cj.jdbc.Driver。同时 URL 要显式带时区参数,比如?serverTimezone=Asia/Shanghai,否则容易报时区错误。另外,MySQL 8 默认的认证插件是caching_sha2_password,有些老版本驱动连不上,建议统一用 mysql-connector-j 8.x,避免蛋疼的认证问题。
5.2 前端是打进 JAR 包还是独立部署
两种方式都可行。如果图省事,把dist构建产物拷贝到 SpringBoot 的static/resource目录下,直接打包打成单体 JAR 也能跑,适合演示。但如果要体现前后端分离架构,就用 Nginx 独立托管前端,并把/api反向代理到后端localhost:8080。这样做跨域也最简单,因为所有请求都走同一个域名,不需要在 SpringBoot 里配置 CORS。
5.3 Vue 路由 404 问题
前端用history模式打包后,直接部署到服务器,刷新页面经常会 404。原因是没有配置 Nginx 回退到index.html。正确的配置片段是:
location / { try_files $uri $uri/ /index.html; }如果你没有独立服务器,也可以用hash模式回避这个问题。毕设现场演示时刷新页面 404,是每年答辩都会出现的尴尬场面,提前配好能省不少心。
6. 三周迭代路径与论文切入点参考
项目从零到能答辩,建议按三周规划,每周只做一件事。
6.1 第一周:搭骨架和认证
先做用户登录注册、JWT 鉴权、RBAC 拦截器、统一响应结构、前端路由和菜单。这些是全局能力,做完后面所有模块都能复用。很多同学一上来就做疫苗管理,结果没认证体系,后面补安全模块时改得到处都是,最后只能重写。建议第一周不做任何业务大模块。
6.2 第二周:核心业务闭环
做疫苗目录、批次管理、号源发布、预约、取消、核销、接种记录。这一周重点验证库存不超卖、重复预约被拦截、取消释放库存。建议写一张表格记录测试用例,比如“同一批次最后一个号源被同时抢两次只成功一笔”,这些在论文里也能直接引用。
6.3 第三周:收尾和演示准备
做公告管理、统计页面、数据可视化图表(可以用 ECharts 统计每日预约量)、部署上线。再补一下移动端适配。答辩时不要只演示功能,还要准备两个非功能性亮点:一个是乐观锁防超卖,一个是权限拦截器防越权。导师一般会抓住这两个点问,能答清楚,项目的完成度评价会明显不一样。
论文主线建议不要写成“前端有什么页面、后端有什么接口”的说明书,而是围绕“疫预约业务中的状态一致性问题”展开。你可以在论文里重点写如何设计批次库存模型、如何用乐观锁解决超卖、如何通过状态机管理预约单生命周期。这样论文的逻辑性和工作量会显得更扎实。
如果时间还有富余,可以考虑扩展这些模块:疫苗库存预警(低于阈值时给管理员发提醒)、预约后的消息通知(站内信模拟即可)、接种点地图展示、Excel 导出预约名单。这些模块的代码量不大,但足以让系统从“课设级别”跨到“毕设优秀级别”。
我个人最后再啰嗦一句:最影响效率的永远是数据库模型和状态机设计,花三天把表结构和状态流转理清楚,后面写代码会顺得像抄答案;如果你上来就写页面,后面大概率要推翻重来。这套方案我亲测下来,从零到完整系统,两周半跑完是可行的,希望对你也有参考价值。