news 2026/10/24 8:39:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
企业级疫苗预约系统落地实践:SpringBoot+Vue+MyBatis+MySQL

企业级疫苗发布与接种预约系统的落地实践

做这个项目之前,我被疫苗预约这个场景折磨过一阵子。社区卫生院的朋友跟我吐槽,每到流感季或者新疫苗到货,电话被打爆、现场排长队、库存对不上,光靠Excel登记根本撑不住。聊完我就明白,这玩意儿不是简单做个"能预约"的小程序就完事的,它牵涉疫苗批次发布、库存分配、预约时段管理、接种记录追溯、多机构协作,是一个典型的企业级信息管理系统。用SpringBoot+Vue+MyBatis+MySQL这套组合去落地,老实说是性价比最高的选择——团队好招、社区资料多、部署不挑环境,而且这套架构对一个"管理型系统"来说完全够用。

这篇博客我会按我做这个项目的实际流程来写,从业务梳理、表结构设计、后端核心逻辑、前端工程化到部署踩坑都过一遍。如果你正准备做类似的管理系统,或者想拿一套完整的疫苗预约项目做学习参考,这篇内容应该能帮你少走不少弯路。

1. 疫苗预约场景下的业务痛点和系统定位

1.1 这个系统到底要解决什么问题

疫苗预约系统跟普通电商预约不一样,它有几个非常特殊的业务约束。

第一个是库存与批次强绑定。疫苗不是普通商品,每一批疫苗都有批号、生产企业、有效期、冷链记录,用户预约的不是"一盒疫苗",而是"某个批次下的一支疫苗"。这个特性导致库存表不能只存总数,必须按批次维度管理,还要能追溯某个用户接种的是哪一批。

第二个是时段承载量有限。接种门诊每天的接待能力是固定的,比如上午8:30-11:30、下午14:00-17:00,每个时段能打多少针取决于医生数量和接种台数量。这意味着预约系统必须做"时段库存"控制,而不是简单地看总库存。

第三个是爽约和取消的库存释放。很多人预约了不来,如果系统不处理超时释放,库存会被无效占用。我在设计时专门做了"预约状态超时自动取消"的定时任务,保证被占用的库存能回流。

第四个是多机构多角色协作。疾控中心发布疫苗,接种门诊管理预约,用户选择门诊和时间,管理员做数据统计。所以系统天然要有RBAC权限模型,至少分管理员、门诊操作员、普通用户三个角色。

1.2 系统核心业务边界划分

我把整个系统拆成四个核心业务域:

  • 疫苗管理域:疫苗品类维护、批次登记、库存入库和出库记录。
  • 预约发布域:创建预约活动、配置可预约批次、设置时段容量、发布公告。
  • 预约交易域:用户选择疫苗和时段下单、支付/免费确认、取消、爽约处理、接种核销。
  • 统计报表域:按疫苗、机构、时间段统计预约量和接种量,生成接种率报表。

一个合格的系统设计,最重要的是一开始就把边界划清楚。做这个项目的时候,我最开始犯过的错就是把"预约单"和"接种记录"混在一张表里,后来发现状态流转完全理不清,才拆成独立的预约主表和接种记录表。这个教训后面讲表结构的时候会细说。

提示:如果你也想做类似系统,千万别一上来就写代码。先把业务域拆开,哪怕是用纸画一下,也比直接建表靠谱得多。

2. 技术选型的取舍逻辑:SpringBoot+Vue+MyBatis+MySQL为什么够用

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,也是并发压力最大的地方。整个调用链路我分五步:

  1. 校验用户登录态和实名认证状态。
  2. 校验预约日期是否在活动发布范围内。
  3. 锁批次库存(条件更新,影响行数为0则提示"疫苗库存不足")。
  4. 锁时段容量(条件更新,影响行数为0则释放步骤3的库存)。
  5. 生成预约单(状态为待确认),返回预约单号。

注意到第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 预约流程的前端交互体验

用户端预约流程我做了四步的向导式交互:

  1. 选择疫苗品类,展示该品类下的可用批次和剩余数量。
  2. 选择接种机构和预约日期,此时会请求后端接口查出该机构在所选日期的时段容量。
  3. 选择一个具体时段,点击预约提交。
  4. 预约成功页展示预约单号和核销码。

环节上有个细节:库存数量不要用实时值展示给用户。因为只要有人同时下单,页面显示的数字就会和实际不一致。我的做法是页面只显示"剩余>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最大连接数我调到了50,minimumIdle调到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: GMT+8才解决。这个问题前端还看不太出来,因为浏览器会自动转换显示,只有对数据库记录时才会发现差了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-web(Web框架)
  • spring-boot-starter-security(登录认证与权限控制)
  • mybatis-spring-boot-starter(持久层框架)
  • mysql-connector-java(MySQL驱动)
  • lombok(简化实体类代码)
  • hibernate-validator(参数校验)
  • fastjson2或jackson(JSON序列化)
  • java-jwt或jjwt(用户登录态令牌)
  • alibaba easyexcel(报表导出)

前端package.json里用到的核心依赖:

  • vue 2.7
  • vue-router 3.x
  • pinia 2.x(Vue2可以用pinia,但不支持Vue2的旧项目用了vuex)
  • axios
  • element-ui 2.15
  • echarts 5.x
  • vite 4.x

结合个人经验,做这种企业级管理系统,最有价值的不是代码本身,而是对业务复杂度的理解和状态流转的把控。疫苗发布和接种预约这个领域看着简单,但批次追溯、时段容量、并发扣减、爽约释放,每一环都藏着真实世界的坑。如果你拿到这套源码,我建议你先把表结构和状态机读懂,再动手改代码,会比直接上手快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 8:39:44

VDC是什么?一文读懂虚拟设计与施工和虚拟数据中心

第一次见到VDC这个词的人,多半是带着问号搜进来的。这个词在两类完全不同的圈子里都能看到:建筑工地和IT机房。在施工项目的例会纪要里,它是Virtual Design and Construction(虚拟设计与施工)的缩写;在云服…

作者头像 李华
网站建设 2026/10/14 13:21:56

二分查找与搜索插入位置:循环不变量与边界条件全解析

作为一个常年跟数组、查找、排序打交道的程序员,我刷题和写业务代码时最常被问起的一个算法就是二分查找。尤其是"搜索插入位置"这类题,看似基础,却是我面试候选人和带新人时最愿意用的试金石——因为它能精准暴露一个人对循环不变…

作者头像 李华
网站建设 2026/10/14 13:22:40

Android滚动视图深度实战:ScrollView嵌套、事件冲突与性能优化

1. 滚动视图是什么,为什么现在还要写它 做Android的都应该清楚,滚动视图这事,说简单是真简单,布局里套个ScrollView,内容一多就能滑了。但说复杂也复杂,横向加纵向一起嵌套、跟ViewPager抢手势、列表数据大…

作者头像 李华
网站建设 2026/10/14 13:22:40

t3code:以可复用片段为核心的代码沉淀工作流

先说个事:我给自己的编码习惯起了个名字,叫 t3code。听起来像个开源项目代号,其实它就是一套我反复调整大半年、从无数次返工中总结出来的个人代码工作流:先想透、快速试、持续沉淀,然后把每一次真正有效的代码变成可复…

作者头像 李华
网站建设 2026/10/14 13:21:31

小动物救助领养系统实战:ThinkPHP+Laravel双框架与微信小程序开发

做过的业务系统里,小动物救助领养系统算是一个让我印象很深的项目。接这个需求时,我下意识觉得“这不就是个二手交易平台换层皮嘛”,但真和救助站、动保组织的负责人聊完业务流程后,我才发现这套系统的麻烦程度一点不比电商低&…

作者头像 李华
网站建设 2026/10/14 15:14:36

Java信号量Semaphore实战:从流量阀门到并发预算管理

多线程安全这块,前几篇咱们一直在聊锁,锁的核心是互斥。但真实系统里还有一个更常见的诉求:不是只允许一个线程进来,而是最多只允许 N 个线程同时进入——这就是流量阀门。Java 里对应的工具是信号量 Semaphore,一个携…

作者头像 李华