资讯详情

企业级疫苗预约系统落地实践:SpringBoot+Vue+MyBatis+MySQL

📅 2026/10/8 20:36:06 | 华诺云谱 👁 阅读
企业级疫苗预约系统落地实践:SpringBoot+Vue+MyBatis+MySQL
企业级疫苗发布与接种预约系统的落地实践做这个项目之前我被疫苗预约这个场景折磨过一阵子。社区卫生院的朋友跟我吐槽每到流感季或者新疫苗到货电话被打爆、现场排长队、库存对不上光靠Excel登记根本撑不住。聊完我就明白这玩意儿不是简单做个能预约的小程序就完事的它牵涉疫苗批次发布、库存分配、预约时段管理、接种记录追溯、多机构协作是一个典型的企业级信息管理系统。用SpringBootVueMyBatisMySQL这套组合去落地老实说是性价比最高的选择——团队好招、社区资料多、部署不挑环境而且这套架构对一个管理型系统来说完全够用。这篇博客我会按我做这个项目的实际流程来写从业务梳理、表结构设计、后端核心逻辑、前端工程化到部署踩坑都过一遍。如果你正准备做类似的管理系统或者想拿一套完整的疫苗预约项目做学习参考这篇内容应该能帮你少走不少弯路。1. 疫苗预约场景下的业务痛点和系统定位1.1 这个系统到底要解决什么问题疫苗预约系统跟普通电商预约不一样它有几个非常特殊的业务约束。第一个是库存与批次强绑定。疫苗不是普通商品每一批疫苗都有批号、生产企业、有效期、冷链记录用户预约的不是一盒疫苗而是某个批次下的一支疫苗。这个特性导致库存表不能只存总数必须按批次维度管理还要能追溯某个用户接种的是哪一批。第二个是时段承载量有限。接种门诊每天的接待能力是固定的比如上午8:30-11:30、下午14:00-17:00每个时段能打多少针取决于医生数量和接种台数量。这意味着预约系统必须做时段库存控制而不是简单地看总库存。第三个是爽约和取消的库存释放。很多人预约了不来如果系统不处理超时释放库存会被无效占用。我在设计时专门做了预约状态超时自动取消的定时任务保证被占用的库存能回流。第四个是多机构多角色协作。疾控中心发布疫苗接种门诊管理预约用户选择门诊和时间管理员做数据统计。所以系统天然要有RBAC权限模型至少分管理员、门诊操作员、普通用户三个角色。1.2 系统核心业务边界划分我把整个系统拆成四个核心业务域疫苗管理域疫苗品类维护、批次登记、库存入库和出库记录。预约发布域创建预约活动、配置可预约批次、设置时段容量、发布公告。预约交易域用户选择疫苗和时段下单、支付/免费确认、取消、爽约处理、接种核销。统计报表域按疫苗、机构、时间段统计预约量和接种量生成接种率报表。一个合格的系统设计最重要的是一开始就把边界划清楚。做这个项目的时候我最开始犯过的错就是把预约单和接种记录混在一张表里后来发现状态流转完全理不清才拆成独立的预约主表和接种记录表。这个教训后面讲表结构的时候会细说。提示如果你也想做类似系统千万别一上来就写代码。先把业务域拆开哪怕是用纸画一下也比直接建表靠谱得多。2. 技术选型的取舍逻辑SpringBootVueMyBatisMySQL为什么够用2.1 不选微服务不选NoSQL甚至不引入Redis你可能听说过很多企业级项目一上来就上Spring Cloud、引入Redis缓存、用Elasticsearch做搜索。但这个项目的真实场景里用户量级是一个城市日活几千到几万的预约请求而不是双十一的百万并发。对我来说技术选型最重要的是匹配业务规模和团队维护成本其他都是伪需求。我最终敲定SpringBoot 2.7 Vue 2.7 MyBatis MySQL 5.7的组合理由很简单技术组件选型理由实际效果SpringBoot简化Spring配置内置Tomcat快速构建REST API开发效率高打包成jar就能跑Vue 2.7组件化开发生态成熟Element UI配套完善后台管理界面开发快MyBatisSQL可控性强预编译防注入适合复杂业务查询预约报表SQL可以直接调优MySQL事务支持可靠InnoDB行级锁满足预约并发控制数据一致性有保障2.2 MyBatis在预约场景的优势很多团队现在喜欢用MyBatis-Plus但我在这个项目里选择了原生MyBatis。原因很简单预约系统的查询有大量多表关联和条件组合比如查询某机构某时间段内某疫苗的预约明细这种SQL用XML写出来语义非常清晰调整条件也方便。而且MyBatis的一级缓存和二级缓存机制在报表类查询上能发挥很大作用。当然纯手写实体类映射确实多花了一点时间但稳定性是值得的。注意如果你习惯用MyBatis-Plus直接把单表CRUD交给它复杂报表查询用原生SQL这样是效率最优方案。不要因为是原生MyBatis就排斥所有便捷工具架构设计讲究的是各取所长。2.3 前后端分离的协作模式前端Vue项目通过Vite构建开发时用proxy代理解决跨域生产环境打包成dist目录扔给Nginx托管。后端只暴露REST API统一返回{code, message, data}结构。这个模式不用我多说是当前主流做法。但我要提醒一点接口文档一定要先定好。我在这项目里用YApi管理接口定义前端后端各自按文档开发联调阶段几乎没有因为字段名不一致返工。3. 数据库建模与关键表设计3.1 用户与疫苗主数据数据库设计是这个系统最核心的部分。我实际建了12张表这里挑最关键的几张讲。用户表包含账号、密码BCrypt加密、姓名、身份证号、手机号、角色类型、所属机构ID。设计上注意一个细节身份证号用于疫苗接种的身份核验所以必须加密存储不是明文存。疫苗品类表存储疫苗名称、适用人群、接种剂次如新冠疫苗需要2针或3针、生产厂家、规格。这个表属于基础主数据跟具体批次无关。疫苗批次表这是关键表。每一批疫苗到货后管理员登记批号、生产企业、有效期、到货数量、冷链温度记录。一个批次可以对应多个接种机构也可以分配给一个机构后单独管理。这个表的设计直接决定库存追溯的能力。CREATE TABLE vaccine_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_no VARCHAR(50) NOT NULL COMMENT 疫苗批号, vaccine_id BIGINT NOT NULL COMMENT 疫苗品类ID, manufacturer VARCHAR(100) COMMENT 生产企业, production_date DATE COMMENT 生产日期, expire_date DATE COMMENT 有效期至, total_count INT COMMENT 入库总数量, available_count INT COMMENT 剩余可用数量, org_id BIGINT COMMENT 所属接种机构ID, status TINYINT COMMENT 1正常 2售罄 3过期, create_time DATETIME );3.2 预约流程的状态机设计预约单表是整个系统的交易核心。我在设计这张表的时候把预约单状态设计成一系列有序的流转状态。状态0待确认用户提交预约锁定库存状态1已确认管理员/系统确认预约成功状态2已接种用户到现场完成接种状态3已取消用户主动取消状态4已过期预约当天结束未接种自动过期预约单主表主要字段预约单号、用户ID、疫苗批次ID、机构ID、预约日期、预约时段、状态、创建时间、取消时间、接种时间、核销人ID。为了完整性我还设计了明细子表记录接种的疫苗批号、接种部位、接种人员、留观时间等信息。这样主表只管状态流转详细医疗信息放在子表职责分明。提示这里强烈建议给预约单号建唯一索引。预约单号我用时间戳机构ID随机数生成一方面业务需要用户报手机号单号核销另一方面也方便分库分表虽然目前用不到但预留了扩展空间。3.3 库存扣减与并发控制疫苗预约一定会遇到并发问题同一时间很多人抢同一时段同一批次的疫苗。如果代码先查库存再update妥妥的超卖。我的处理方式是用MySQL的乐观锁条件更新在MyBatis的update语句里加库存判断只有影响行数为1才算扣减成功。UPDATE vaccine_batch SET available_count available_count - 1 WHERE id #{batchId} AND available_count 0这条SQL的精髓在于把检查库存和扣减库存合并成一个原子操作数据库的行锁帮我们挡住了并发穿透。当然有人会问要不要用Redis做分布式锁我的看法是在这个量级下数据库条件更新已经足够稳没必要引入额外的中间件增加运维复杂度。预约时段容量控制也是同样的逻辑。时段容量存在appointment_slot表中一个时段配置了最大容量扣减时用同样的条件更新方法。我在设计时把时段跟批次库存做了两层校验避免用户选了一个有库存但没放号的时段。4. 后端核心业务实现4.1 疫苗发布管理模块疫苗发布是管理员的核心操作。我把它拆成两个动作新增批次入库和配置预约活动发布。批次入库流程管理员选择疫苗品类填写批号、厂家、有效期、数量、冷链温度提交后系统生成批次记录并把数量写入available_count。这里我做了个校验同一机构下批号不允许重复因为批号是追溯的根。配置预约活动是我做得比较细的地方。管理员选择一个批次、设置可预约日期范围、配置每个日期的时段容量比如上午50人、下午80人提交后系统自动生成一组appointment_slot记录。这样发布的不是一个笼统的有苗而是清晰到每个时段可约多少人的精确计划。4.2 预约下单与锁库存的实现链路预约接口是用户端最核心的API也是并发压力最大的地方。整个调用链路我分五步校验用户登录态和实名认证状态。校验预约日期是否在活动发布范围内。锁批次库存条件更新影响行数为0则提示疫苗库存不足。锁时段容量条件更新影响行数为0则释放步骤3的库存。生成预约单状态为待确认返回预约单号。注意到第4步失败时必须回滚第3步的库存。我用Transactional包住这个流程但这还不够——如果手动用条件更新语句扣库存事务回滚只能回滚数据库操作但前提是你要在同一个事务方法里完成所有操作。所以我特意把所有扣减逻辑放在一个事务方法内执行保证任何一步抛异常整体回滚。Transactional(rollbackFor Exception.class) public String createAppointment(CreateAppointmentReq req) { // 1. 校验用户 // 2. 校验活动时间段 // 3. 扣批次库存 int batchResult vaccineBatchMapper.deductStock(req.getBatchId()); if (batchResult 0) throw new BusinessException(批次库存不足); // 4. 扣时段容量 int slotResult appointmentSlotMapper.deductCapacity(req.getSlotId()); if (slotResult 0) throw new BusinessException(该时段预约已满); // 5. 生成预约单 appointmentMapper.insert(buildAppointment(req)); return appointmentNo; }4.3 超时取消与库存释放的定时任务预约了不接种是真实世界的常态。我设计了定时任务每小时跑一次把预约时间为当天之前、状态仍为待确认或已确认的预约单批量置为已过期同时释放对应批次和时段的库存。这个定时任务的核心难点是批量释放库存的正确性。我的实现方式是先查出所有过期预约单的批次ID和时段ID列表然后按预约单维度逐条恢复库存而不是按批次聚合恢复——这样可以保证每一单的释放都能追溯到具体的批次和时段。另外我给任务加了分布式锁的预留字段防止多实例部署时重复执行。4.4 接种核销与追溯记录用户到现场后操作员通过预约单号或用户身份证号查询预约信息确认身份后点击核销接种。核销时做两件事更新预约单状态为已接种写入接种记录表。接种记录表我单独建了一张记录用户ID、疫苗批次ID、接种机构、接种时间、接种第几剂、接种医生、疫苗批号、留观状态。这个表的存在让整条链路真正闭环——从发布批次到预约到核销到追溯任何一环出了问题都能查到底。5. 前端工程化与核心页面5.1 Vue项目结构与路由设计前端我用Vue2.7 Element UI Vue Router Pinia。虽然是Vue2版本但配合Vite构建开发体验已经相当顺滑。项目结构按模块划分src/views页面视图分为user用户端预约、admin管理后台、auth登录注册。src/api按业务模块封装的axios请求方法。src/store用户信息、机构信息等全局状态。路由的设计我用了路由守卫做权限控制。管理员路由配置meta: { requiresAdmin: true }用户在进入前从store或本地缓存获取角色信息不匹配就重定向到首页。这块代码不难但一定要在前端和后端同时做权限校验单纯的前端守卫只是体验层面的后端接口的鉴权才是安全底线。5.2 预约流程的前端交互体验用户端预约流程我做了四步的向导式交互选择疫苗品类展示该品类下的可用批次和剩余数量。选择接种机构和预约日期此时会请求后端接口查出该机构在所选日期的时段容量。选择一个具体时段点击预约提交。预约成功页展示预约单号和核销码。环节上有个细节库存数量不要用实时值展示给用户。因为只要有人同时下单页面显示的数字就会和实际不一致。我的做法是页面只显示剩余10支这种模糊状态具体能不能约到以提交时的接口返回为准。这样从产品逻辑上规避了用户的明明有苗怎么约不上投诉。5.3 管理后台的数据看板与报表管理后台我做了三个重要的页面疫苗批次管理页表格展示批次信息、库存状态、操作按钮发布预约、编辑、停用。预约订单管理页支持多条件筛选按机构、疫苗、状态、日期支持手动取消预约。统计报表页用ECharts做图表展示每日预约量、疫苗接种率、各机构预约横向对比。报表接口是后端比较费心思的地方。比如接种率我定义为已接种数 / 已确认数统计口径必须跟前端确认清楚。尤其是Excel导出功能我用的是EasyExcel在导出前要设置响应头让前端触发下载这个属于老生常谈但很容易漏的细节。6. 部署、性能优化与踩坑记录6.1 标准部署架构由于没有引入Redis等中间件部署结构非常简单一台Linux服务器2核4G即可Docker或直接部署SpringBoot jar包MySQL单独跑。前端dist目录由Nginx托管同时Nginx配置/api前缀的反向代理指向后端服务的8080端口。前端Nginx配置我附在这里后端跨域问题的根源就是Nginx没配对server { listen 80; server_name vaccine.example.com; location / { root /opt/vaccine-front/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.2 短时高并发的性能优化疫苗预约往往集中在发布后的一小段时间比如早上9点放号可能就前几分钟流量最大。这个场景下MySQL的单库单表能扛住几千QPS吗我的实测经验是能但需要做两件小事。第一是连接池调优。默认的HikariCP最大连接数我调到了50minimumIdle调到10避免频繁创建连接。第二是把写操作尽量缩短。预约接口的事务中所有SQL都走了主键索引或唯一索引尽量不用慢查询。我给时段容量表加了一个唯一索引(org_id, slot_date, slot_time)这样条件更新的行锁冲突概率大幅降低。6.3 我实际踩过的几个坑坑一事务内远程调用。最开始我在预约事务里加入了一个发送短信通知的操作遇到短信服务超时会卡住数据库连接池甚至导致整个预约流程阻塞。后来我果断把短信发送改成异步MQ或线程池处理事务方法只保留数据库操作。这个教训很深刻事务里别干与数据库无关的事。坑二乐观锁扣库存导致的自增孤儿数据。一开始扣库存失败后我直接抛异常事务回滚但会有一个问题预约单的自增ID已经分配了回滚后ID出现空洞。这个其实不影响业务但是复查数据时看到AUTO_INCREMENT跳号会有点慌。有人会在事务外先查一次库存避免多余尝试但并发下还是有竞态所以最后我接受了跳号把精力放在保证业务一致性上。坑三前端Time Zone问题。用户预约选择的是2024-06-15但传到后端因为时区问题变成了前一天晚上16点。排查半天才发现是Jackson序列化时区没配置。最后在application.yml里统一设置了spring.jackson.date-format和time-zone: GMT8才解决。这个问题前端还看不太出来因为浏览器会自动转换显示只有对数据库记录时才会发现差了8小时。坑四MySQL 5.7版本下datetime默认值。在创建表时给create_time字段设置DEFAULT CURRENT_TIMESTAMP在MySQL 5.7里没问题但如果在8.0版本会提示不支持。建议统一用DATETIME(3)加毫秒精度并用Java代码显式赋值时间避免版本兼容问题。6.4 源码中使用到的关键依赖清单最后列一下后端pom.xml中用到的核心依赖方便你搭环境时对照spring-boot-starter-webWeb框架spring-boot-starter-security登录认证与权限控制mybatis-spring-boot-starter持久层框架mysql-connector-javaMySQL驱动lombok简化实体类代码hibernate-validator参数校验fastjson2或jacksonJSON序列化java-jwt或jjwt用户登录态令牌alibaba easyexcel报表导出前端package.json里用到的核心依赖vue 2.7vue-router 3.xpinia 2.xVue2可以用pinia但不支持Vue2的旧项目用了vuexaxioselement-ui 2.15echarts 5.xvite 4.x结合个人经验做这种企业级管理系统最有价值的不是代码本身而是对业务复杂度的理解和状态流转的把控。疫苗发布和接种预约这个领域看着简单但批次追溯、时段容量、并发扣减、爽约释放每一环都藏着真实世界的坑。如果你拿到这套源码我建议你先把表结构和状态机读懂再动手改代码会比直接上手快得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑