ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue疫苗预约系统实战:从数据库设计到部署避坑

SpringBoot+Vue疫苗预约系统实战:从数据库设计到部署避坑 标题看起来像是一个典型的SpringBoot Vue前后端分离项目但实际上一套“疫苗发布和接种预约系统”牵扯到的业务点并不少疫苗库存、批次管理、预约时段、用户接种记录、健康问询、排队号生成……这些模块几乎把Java后端最常见的增删改查、事务处理、权限校验、定时任务都覆盖了一圈。对于毕设或者课设来说选题面不大不小恰好能把SpringBoot、Vue、MySQL这几样东西都练到位又不至于复杂到做不完。我见过不少人拿到这种源码就开始跑跑通之后以为万事大吉结果一答辩就被问到数据库为什么这么设计、预约冲突怎么解决、权限怎么控制直接卡壳。所以这篇不打算只讲“怎么启动”而是把整套系统从结构设计、数据库设计、后端接口、前端对接、部署避坑到答辩加分点全部拆开说一遍。不管你准备直接拿源码做二次开发还是打算仿照这个题目自己从零写一套都能找到能直接落地的思路。1. 项目结构拆解前后端分离架构的选择逻辑1.1 为什么毕设项目首选“SpringBoot Vue”组合在高校毕设和课程设计里SpringBoot加Vue几乎是统治级的存在。原因不复杂SpringBoot把Spring那套繁琐的XML配置全部拿走内嵌Tomcat打成一个jar包就能跑非常适合学生阶段快速搭建后端Vue则是现阶段前端招聘和课程教学里最常出现的框架组件化写法、数据双向绑定、路由管理都有成熟的生态学习成本比React低一截做管理后台和预约页面效率很高。再往后端看SpringBoot整合MySQL用的是Spring Data JPA或MyBatis-Plus。疫苗预约系统里大量操作是“按条件查数据”、“更新库存”、“插入记录”这类业务用MyBatis-Plus非常顺手不需要写一堆XML基类方法就能覆盖八成需求。项目如果源码里带了MyBatis-Plus的封装你在答辩时可以重点讲“多条件查询如何用LambdaQueryWrapper动态拼装”这比“我会写CRUD”要有说服力得多。1.2 前后端分离下的目录规划与通信方式前后端分离不是代码分开就算完了真正要注意的是“边界”。我的习惯是前端只做视图和交互所有数据校验、权限判断、业务流转都放后端处理。前端通过HTTP请求调用后端提供的RESTful接口数据格式统一用JSON。实际工程里后端一般按这种包结构组织com.example.vaccine ├── controller # 接口层接收请求、回传JSON ├── service # 业务层处理核心逻辑、事务 ├── mapper # 数据访问层对应MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象用于接口入参和出参 ├── config # 配置类如跨域、拦截器、MybatisPlus分页配置 ├── utils # 工具类如JWT工具、日期工具 └── common # 通用返回结果封装、异常处理说到接口通信有个很多新手容易忽略的点跨域问题。Vue开发环境默认跑在8080端口SpringBoot后端一般在8081或9090前端直接请求后端会被浏览器拦截。解决办法是在后端写一个CorsConfig配置类允许本地开发端口访问或者在后端Controller上加CrossOrigin注解。源码里如果已经做好了跨域配置答辩前最好能说清楚CorsConfiguration里allowedOriginPatterns的含义属于很加分的细节。2. 疫苗预约系统的核心场景与功能模块规划2.1 用户端到管理员端的角色划分一套疫苗发布和接种预约系统如果只是做一个“用户查疫苗、填表单预约”的页面那更像静态网页撑不起毕设的体量。要想把系统做完整至少要拆出三种角色普通用户、接种门诊的管理员、系统运维层面的超级管理员。普通用户能做的事情集中在注册登录、查看疫苗公告、查看可预约的疫苗批次和剩余数量、在线选择接种时间段、填写健康自测问卷、提交预约、查看自己的预约记录、接种后查看电子接种凭证。管理员端则是发布疫苗信息、维护疫苗批次与库存、设置预约时间段和限制人数、审核用户预约、登记实际接种结果、生成接种统计报表。如果再把系统做到更深一层还可以加“疫苗到期提醒”和“库存预警”这些功能虽然属于锦上添花但很能体现你对业务的理解。2.2 预约流程的状态设计从发布到接种完成预约系统的重点不在页面多华丽而在“状态机”设计。一个预约记录从产生到结束至少要经历这几个状态待确认、预约成功、已接种、已取消、已过期。我建议在数据库里用status字段存int或tinyint比如后端统一用0、1、2、3、4表示不要直接存中文。前端根据状态码展示不同标签后端根据状态码做业务判断。状态之间的转换必须有明确规则比如“已取消”只能由“待确认”和“预约成功”状态流转过来“已接种”只能由“预约成功”流转不能让用户跳过预约直接接种。这些规则放在Service层做不要放在Controller里否则代码会越写到后面越乱。2.3 疫苗发布前端的展示逻辑疫苗发布信息建议分成“疫苗基础字典”和“批次库存”两层。基础字典存疫苗名称、生产厂家、适用年龄段、接种剂次等相对固定的信息批次库存存该疫苗每个批次的到货数量、剩余数量、有效期、存放地点。为什么这么拆因为同一个疫苗可能分多次到货批次不同有效期也不同混在一张表里会出现大量重复字段。我当时自己设计表的时候也没想那么深后来做批次预警的时候发现基础信息和批次信息混在一起写SQL都想摔键盘最后重构才顺畅。前端疫苗展示页面建议用卡片列表而不是单纯表格卡片上显示疫苗名称、库存余量、预约截止时间、适用人群标签。用户点击“立即预约”后进入预约页预约页需要动态显示当前可选日期和剩余号源这里涉及到可用库存的实时查询后面会在并发处理部分细讲。3. MySQL数据库表设计从实体关系到字段规划3.1 核心表结构用户表、疫苗表、批次表、预约表数据库设计是疫苗预约系统最容易暴露水平的地方。我见过很多项目就建两张表user和appointment疫苗信息全用字符串硬塞做是能做但业务逻辑写起来处处别扭。比较标准的设计至少要有五张以上核心表。用户表主要字段id、username、password、real_name、id_card、phone、age、role角色区分普通用户和管理员、status、create_time。注意password字段建议存BCrypt加密后的字符串长度定64位不要用明文这在答辩时是个明显的安全加分项。疫苗信息表放固定属性vaccine_id、vaccine_name、manufacturer、disease_prevention、dosage_times几针、age_min、age_max、description。这里把适用年龄拆成最小值和最大值是为了后续做“年龄不符合自动拦截”的判断。疫苗批次表是库存控制的核心batch_id、vaccine_id、batch_no、quantity_total、quantity_remaining、production_date、expiry_date、status。quantity_remaining每次预约成功都需要扣减同时要保证不能扣成负数这块牵扯到并发控制具体方案后面单独说。预约表是业务核心appointment_id、user_id、vaccine_id、batch_id、appointment_date、time_slot选上午还是下午、appointment_no排队号、status、health_questionnaire健康自测结果汇总、remark、create_time、update_time。3.2 索引设计和状态字段的规范写法预约表是查询频率最高的表用户在“我的预约”页面按user_id查管理员按日期和疫苗类型查。所以至少要在user_id、appointment_date、vaccine_id上建组合索引。MyBatis-Plus里可以在实体字段上加TableIndex注解或者在数据库初始化SQL里直接CREATE INDEX。关于状态字段建议再带一个update_time字段这样预约状态从“预约成功”变成“已接种”时能记录操作时间。对于疫苗批次过期问题可以写一个定时任务每天去扫描expiry_date小于当前日期的批次把status自动改成“已过期”前端疫苗列表里就不会再把过期批次展示出来。SpringBoot里做定时任务只要在启动类或者配置类上加EnableScheduling然后用Scheduled(cron 0 0 2 * * ?)定义每天凌晨两点执行即可实现成本非常低但显得系统很完整。3.3 初始化SQL脚本的实操建议很多毕设源码带的SQL脚本只有建库建表数据全靠手动制造演示起来特别干。建议做一份完整的初始化脚本至少插入一个管理员账号、三四个疫苗批次、若干条模拟用户和预约记录。为了让答辩演示效果好可以把预约记录跨到不同日期和状态比如既有已接种的、又有待确认的、还有已取消的这样前端页面点开不同标签时都有数据展示不用现场临时造数。另外MySQL版本差异也是个坑。以前我在本地用MySQL 5.7写了SQL交到朋友8.0环境直接报utf8mb4配置问题折腾了半天。初始化脚本里尽量统一用utf8mb4排序规则用utf8mb4_general_ci兼容性最好。4. 后端SpringBoot核心实现接口设计、权限校验、并发控制4.1 RESTful接口的命名风格和返回结构后端接口不要随手写一堆不规范路径建议统一用RESTful风格。资源用名词复数动词交给HTTP方法。比如POST /api/auth/login # 登录 POST /api/auth/register # 用户注册 GET /api/vaccines # 查询疫苗列表 GET /api/vaccines/{id} # 疫苗详情 POST /api/appointments # 提交预约 GET /api/appointments/my # 当前用户的预约记录 PUT /api/appointments/{id}/cancel # 取消预约 PUT /api/appointments/{id}/status # 管理员更新预约状态返回结果统一封装成Result对象包含code、message、data三部分。code用200表示成功500表示系统异常401表示未登录403表示无权限。前端axios响应拦截器里统一判断code避免每个页面都写重复的报错逻辑。4.2 JWT登录鉴权与角色权限控制一个系统只要有用户登录就绕不开权限控制。疫苗预约平台建议用JWT做无状态鉴权用户登录成功后后端生成一个token返回前端把token存在localStorage每次请求都在Header里带Authorization: Bearer token。后端用一个拦截器或者Spring Security配置去校验token解析出用户id和角色。如果不引入Spring Security用HandlerInterceptor加JWT工具类也能实现。核心逻辑是写一个AuthInterceptor在preHandle方法里取Header中的token调用JWTUtil解析失败就返回401成功就把用户信息放request的attribute里后续Controller直接拿。管理员接口单独加一个RequireRole注解或者在拦截器里判断路径前缀比如/api/admin/**必须roleadmin。答辩如果想讲这块重点说清楚“为什么用JWT而不是Session”——因为前后端分离部署时后端没有Session共享机制JWT把用户身份信息放在签名后的令牌里服务器无状态化更适合接口服务。这个点说完老师一般会点头。4.3 预约库存扣减的并发坑防超卖设计预约系统最容易出事故的地方不是页面前端而是多人同时预约最后一个号源。经典的场景是用户A和用户B同时请求预约后端的查询都发现剩余数量是1都执行了insert预约记录然后都去update库存最后库存变成-1超卖就发生了。解决思路有多种从简单到复杂排列简单派做法在更新库存的SQL上追加条件UPDATE vaccine_batch SET quantity_remaining quantity_remaining - 1 WHERE batch_id #{batchId} AND quantity_remaining 0这样做的好处是不需要额外引入分布式锁靠MySQL行锁和条件判断保证不会减成负数。问题在于如果业务逻辑复杂比如还要同时检查用户是否重复预约就需要在同一个事务里串行化处理。进阶做法利用唯一索引做防重约束。在预约表上加一个user_id batch_id appointment_date的组合唯一索引数据库层面直接拒绝同一个用户同一天预约同一个疫苗批次两次代码层面再配合捕获DuplicateKeyException返回友好提示。我在实际项目中采用的做法是启动事务先查预约是否已存在再用带quantity_remaining 0条件的方式更新库存更新影响行数如果为0则直接抛异常回滚防止超卖。这里要强调的是“先更新后插入”还是“先插入后更新”都行但整个操作必须放在同一个事务里并且给库存记录加行锁否则并发依旧出问题。4.4 排号生成与预约时段约束预约成功后的排队号不要用数据库自增id直接展示那样会暴露当天预约总人数而且不同批次混在一起也看不出来。建议生成规则用日期加时段加当天序号比如20250620上午第8号可以生成20250620AM08。实现方式是每次预约成功后查当前日期下最大序号然后加1。如果并发量高可以在预约表上加一个appointment_date和time_slot的组合索引然后通过select max来获取当前序号再统一生成。时段约束指的是每个时间段有人数上限比如上午限30人下午限40人。后端提交预约时先统计已成功预约的人数如果超过上限直接返回“该时段已约满”。这个统计SQL用count就行了但要注意状态条件只统计状态为预约成功或待确认的记录已取消的不算在内。4.5 健康问询和异常处理健康问询在毕设里通常不做特别复杂的逻辑但也不建议只存一个“是否健康”字段。我建议以二选一问卷的形式设计比如近14天内是否有发热、咳嗽等不适症状是/否是否有过敏史是/否是否处于孕期或哺乳期是/否近一个月是否接种过其他疫苗是/否后端提交预约时接收一个JSON数组存储时把结果转成文本或者JSON字符串比如{fever:false,allergy:true,pregnant:false}。如果存在“是”的项不强制拦截但预约记录里要带上标记接种医生可以提前看到。这样设计既简单又显得考虑周全。全局异常处理也是必做项。写一个RestControllerAdvice类拦截业务异常、参数校验异常、数据库异常统一返回Result格式。不然一报错前端就收到一堆带堆栈信息的明文错误既不安全也不美观。5. 前端Vue实现要点页面结构、接口封装、交互体验5.1 Vue项目的目录与核心依赖Vue前端建议使用Vue CLI或Vite创建配合Vue Router做路由跳转Pinia或Vuex做状态管理Element Plus作为UI组件库。Element Plus的表单、日期选择器、表格、分页、弹窗组件都非常成熟做后台管理界面效率特别高。典型目录结构src ├── api │ ├── auth.js │ ├── vaccine.js │ └── appointment.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── Login.vue │ ├── Register.vue │ ├── Vaccines.vue │ ├── VaccineDetail.vue │ ├── Appointment.vue │ ├── MyAppointments.vue │ └── admin │ ├── VaccineManage.vue │ ├── BatchManage.vue │ └── AppointmentReview.vue ├── utils │ └── request.js ├── App.vue └── main.js5.2 axios封装与接口调用规范不要在每个组件里直接写axios.get那是后期维护的灾难。统一封装在utils/request.js里创建axios实例设置baseURL、超时时间、请求拦截器带token、响应拦截器处理code和401跳转。封装之后每个页面的接口调用写成独立函数放到api目录对应的文件里组件里只import这些函数代码立刻干净很多。响应拦截器里最容易踩坑的是后端返回的Blob文件流和JSON的判断。如果接口需要导出Excel或者PDFresponseType要设置成blob响应拦截器不能再用res.code判断而要判断res.type是否为application/json否则下载会变成乱码文件。我当年在这个坑上耗了一下午后来改成了后端直接返回JSON格式的下载链接前端用window.open打开新窗口下载虽然不算最优雅但省心。5.3 预约页面的日期与号源联动预约页面的交互核心是“选日期 → 显示号源 → 填信息 → 提交”。日期选择器建议禁用过去日期和超过预约周期的日期这一步前端快速过滤掉一部分非法输入但真正的校验还是要在后端再做一遍前端限制只是体验优化。用户选了日期后前端要调用后端的号源查询接口比如GET /api/vaccines/{id}/slots?date2025-06-20返回上午和下午的剩余数量。展示方式用两个卡片上午和下午分别显示“剩余12人”不够的时候置灰不可选。这种设计比下拉框直观得多答辩演示时也很耐看。提交预约后的回执页面建议展示预约流水号、疫苗名称、接种地点、接种日期、温馨提示等内容。再加一个“添加到日历”或“预约提醒”按钮用第三方插件或者干脆弹窗提示功能量不在大小关键是体现你考虑了用户体验。5.4 管理员端的数据看板管理员后台除了传统的表格管理建议加一个简易的统计首页用ECharts做柱状图和饼图。比如最近一周每天的预约人数、各疫苗预约占比、各时段预约分布。ECharts在Vue中使用并不复杂先npm install echarts然后按需引入即可。统计接口可以在后端写聚合查询用GROUP BY按日期和疫苗分组统计返回Map结构给前端前端用ECharts的Option直接渲染。这类可视化页面放在毕设的演示环节非常加分因为一眼就能看出系统不是一个简单的增删改查。而且代码量不大后端一个查询接口前端两个组件完全可控。6. 本地运行与环境配置从源码到跑通的完整流程6.1 环境准备与版本匹配跑这套系统之前先把环境对齐版本不对后面全是问题。建议这样搭配后端JDK 1.8或11Maven 3.6SpringBoot 2.7.xMyBatis-Plus 3.5.x。前端Node.js 16或18npm或yarnVue 3Element Plus。SpringBoot 3.x不建议毕设使用因为版本改动大有些旧教程的配置会失效尤其是javax改成jakarta这个点很多同学踩坑。MySQL用5.7或8.0均可但初始化SQL脚本要注意两种版本的兼容性尽量别用太新的语法。6.2 数据库初始化与后端配置拿到源码先看SQL目录下的脚本通常在README里会写明执行顺序。用Navicat或命令行执行建库脚本然后确认application.yml里的数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/vaccine_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver注意serverTimezone必须设置否则连接MySQL 8.0会报时区错误。如果用的是MySQL 5.7driver-class-name写法没区别但可以去掉useSSLfalse之外的参数。后端启动如果出现Port 8080 was already in use说明端口被占用要么改application.yml里的server.port要么用netstat -ano | findstr 8080查占用进程杀掉。注意前端和后端端口不要重复。6.3 前端启动与打包部署前端在本该正常的流程中无非是两步先安装依赖再启动开发服务器。npm install npm run serve但npm install这一步很让人头疼国内网络环境下经常卡住。建议先执行npm config set registry https://registry.npmmirror.com切换成国内镜像源再重新安装。如果依赖装完启动报Vue版本相关的错多半是npm把依赖装乱了直接删掉node_modules目录和package-lock.json重新install一次基本能解决。开发阶段前端请求后端接口配置跨域可以在vue.config.js里设置devServer的proxy把/api前缀全部代理到后端地址module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端代码里请求路径直接写/api/xxx就不需要后端再单独开跨域了。但很多源码为了省事直接在后端加了CorsConfig两种方式共存会冲突建议二选一。我实际中更喜欢用前端proxy因为上线部署时前端静态文件和后端可以配置Nginx反向代理路径风格更统一。部署时若想演示一个完整系统避免开两个终端可以对前端执行npm run build生成dist目录然后把dist里的静态文件放到后端SpringBoot的static目录下重新打包成单个jar。但更推荐的方式是部署时用Nginx托管前端静态文件同时把/api反向代理到SpringBoot服务的端口这样项目结构更贴近真实生产环境答辩时说出来会显得更有经验。7. 毕设与课设场景下的加分项和避坑实录7.1 功能之外的代码规范性不少同学的毕设系统功能都实现了但代码看起来像“堆出来的”。真正让老师眼前一亮的往往是规范感。这里有几个随手就能做的改进一是所有Controller接口都要有统一参数校验。新增预约时对appointment_date和time_slot做非空校验使用Validated注解加NotBlank而不是在方法里写一堆if判断。二是业务操作日志尽量留痕。可以写一个简单的操作日志表用户取消预约、管理员审核预约、修改疫苗库存时都insert一条记录。这个功能不用做成复杂框架哪怕是AOP写个切面也说明你考虑到了系统的可追溯性。三是敏感信息脱敏。列表接口不要把用户的身份证号全部返回给前端只在需要时返回脱敏后的字符串比如前三位和后四位保留中间打星号。虽然毕设没有等保要求但这个小细节在答辩时拿出来说能让老师觉得你有实际工程意识。7.2 演示环节准备清单答辩演示最容易翻车的就是现场数据不够、操作路径不完整。建议提前准备好一个演示脚本按顺序走完一套完整业务流程管理员登录发布一个新的疫苗批次设置库存和预约时段用户注册登录查看疫苗列表选择当天批次进入预约填写健康问询并提交管理员审核预约修改状态为预约成功用户在“我的预约”里看到预约记录取消其中一个管理员端查看统计报表展示图表数据变化。走完这一遍系统各模块基本都覆盖到了而且每一步都有前后呼应比零散点按钮有说服力得多。如果条件允许提前录屏一份保底视频省得现场网络抽风。7.3 常见启动和运行Bug排查把最常遇到的问题整理成了一张速查表如果你跑源码时遇到相似报错可以直接参考。报错或现象可能原因与处理后端启动报Access denied for userMySQL账号密码配置错误检查application.yml后端启动报Unknown database数据库没有执行初始化脚本或数据库名不一致前端启动报Module not found依赖没装全重新执行npm install前端请求接口返回404代理路径配置错误确认/api路径和后端RequestMapping一致预约时报库存不足库存表quantity_remaining为0去管理员端补一批库存登录后刷新页面就退出token没有持久化检查localStorage和路由守卫中文乱码数据库连接url没加characterEncodingutf8打包后接口无法访问前端proxy只在开发环境生效部署时需要Nginx配置反向代理7.4 代码扩展思路还能往哪些方向深化如果课设做完还有精力或者导师要求你加复杂度可以考虑三条路一是加入消息通知模块比如预约成功和接种提醒通过邮件或短信发送Java可以用Spring Mail或第三方短信SDK实现前端展示通知列表和已读未读状态二是引入Redis做缓存把疫苗列表和号源信息缓存起来减少MySQL压力同时用Redis的分布式锁进一步优化预约并发这个点写进论文里绝对是技术亮点三是做成多院区版本在疫苗批次表里增加门诊关联字段用户根据所在地区选择最近的门诊接种点系统复杂度提升一个档次但核心逻辑还是建在现有表结构上的扩展。我个人做这类系统的最大体会是不要因为功能简单就轻视数据库设计和并发细节预约系统练的就是“业务拆解”和“边界控制”的能力。把一张表拆清楚、把一个状态流转设计严密比多写一百行CRUD都有价值。源码拿到手先别急着跑拿它当参考自己动手把核心模块重新敲一遍才是毕设真正的收获。
返回列表