基于SpringBoot+Vue的疫苗预约系统设计与并发超卖防控

发布时间:2026/10/11 0:29:35
基于SpringBoot+Vue的疫苗预约系统设计与并发超卖防控
1. 疫苗发布与预约的业务场景和技术选型做接种预约系统这个项目起源于一个很实在的现场问题社区接种点每天的疫苗针剂数量有限预约电话被打爆纸质登记表翻起来费劲还经常出现约了不来和没约直接跑来的混乱。于是我用 SpringBoot 搭后端、Vue 搭前端、MySQL 存数据做了这个疫苗发布和接种预约系统信息管理系统。整套源码设计成可以直接运行后端是 SpringBoot 的经典分层结构前端是 Vue 单页应用数据库脚本一导入就能用。对正在做毕业设计、或者刚接触前后端分离项目想找一个完整案例的人来说这套系统最大的价值在于——它不是一个只停留在能登录、能增删改查的演示品而是把疫苗发布、预约时段管理、接种记录查询、统计报表这些真实业务流程都串起来了而且最关键的并发预约场景有实际的锁处理方案。1.1 这个系统到底解决什么问题疫苗发布和接种预约听起来就六个字拆开看业务链条其实很长。接种点需要先发布一批疫苗的到货信息包括疫苗名称、生产厂家、可预约针次第一针、第二针、加强针、每个时间段的库存数量居民这边需要注册登录、选择接种点、选择时间段、提交预约然后到现场核销。中间的环节全靠人工的话会立刻暴露三个痛点。第一个是信息不对称。疫苗到没到货、今天还剩多少号全靠打电话问工作人员一天要接几十通相同内容的电话根本没有精力做别的。第二个是现场秩序混乱。预约没有约束大家集中在上午九点到十点来排队排到门外还有人因为约不上在窗口争执。第三个是数据难追溯。接种了哪一批疫苗、哪个厂家、什么批号真出了问题要查得去翻纸质本子效率极低还容易漏。这个系统就是把链路里的每个环节电子化。核心模块分成四块系统管理用户、角色、菜单权限、疫苗管理疫苗信息发布、批次维护、库存同步、预约管理放号、约号、取消、现场核销、数据统计预约量、接种量、爽约率。我实际开发时没有贪多先把这四条主链路跑通再往里面补细节后面每加一个功能都有明确的使用场景支撑。1.2 为什么选 SpringBoot Vue MySQL 这套组合技术选型这块很多人纠结要不要上微服务、要不要引入 Redis、要不要前后端彻底分离。我的判断标准很简单项目规模决定了技术复杂度不要过度设计。这套系统的用户量级是一个社区接种点 周边居民单体应用完全撑得住强行上分布式只会增加部署和运维成本。后端用 SpringBoot是因为它把配置做了大量简化一个 main 方法就能启动内嵌 Tomcat 不需要额外部署 war 包。对于这种业务体量的管理系统SpringBoot MyBatis-Plus MySQL 是最省心的组合MyBatis-Plus 的 BaseMapper 直接提供单表 CRUD省掉写大量 XML 的时间。前端用 Vue 2 Element UI组件生态成熟表格、表单、弹窗、日期选择器这些管理后台常用的组件都有现成的不需要自己造轮子。数据库用 MySQL 5.7 或 8.0 都可以教程多、资料全遇到问题容易排查。有人会问为什么不用 Redis 做分布式锁我在并发那一节会专门解释。一句话先放在这里单实例部署下数据库自身的行锁和条件更新就能解决超卖问题引入 Redis 是给自己增加运维负担。这套组合能直接跑起来、能覆盖真实预约场景对学习者和业务方都足够这就够了。1.3 项目整体功能清单模块功能点说明系统管理用户管理、角色管理、菜单管理基于 RBAC 的轻量权限控制疫苗管理疫苗信息维护、批次发布、库存管理管理员发布疫苗到货信息预约管理时段放号、用户预约、取消预约、现场核销核心业务模块涉及并发处理统计报表预约人数、接种人数、爽约人数按日期、疫苗、接种点维度统计用户端注册登录、疫苗查询、时段预约、接种记录面向普通居民这个清单是开发之前就写好的后面每一步开发都对着它检查既保证不漏功能也不做多余功能。实际动手写代码的时候你会发现业务边界越清晰开发效率越高。2. 后端 SpringBoot 分层设计与核心代码2.1 包结构与分层逻辑后端代码我按经典的三层架构组织Controller 负责接收请求和参数校验Service 负责业务逻辑Mapper 负责数据库操作。再加上 entity实体类、dto传输对象、vo视图对象、config配置类、common统一返回结果和异常处理整个包结构一目了然。com.example.vaccine ├── controller │ ├── AdminVaccineController.java │ ├── AppointmentController.java │ └── AuthController.java ├── service │ ├── VaccineService.java │ ├── AppointmentService.java │ └── ... ├── mapper │ ├── VaccineMapper.java │ ├── AppointmentMapper.java │ └── ... ├── entity │ ├── Vaccine.java │ ├── Appointment.java │ └── ... ├── dto ├── vo ├── config │ ├── WebMvcConfig.java │ └── MybatisPlusConfig.java └── common ├── Result.java └── GlobalExceptionHandler.java这里有一个值得强调的点entity 和 dto 一定要分开。我见过很多新手直接把 entity 返回到前端数据库字段一旦调整接口返回结构就变了前端跟着遭殃。正确做法是 Controller 接收 dto、返回 voentity 只在 Service 和 Mapper 之间流转外部永远感知不到数据库表结构的变化。这个习惯能省掉后期无数联调时间。2.2 核心接口设计与调用关系预约业务涉及几个关键接口我把它们列出来命名和职责都非常清晰POST /api/auth/register居民注册POST /api/auth/login登录返回 tokenGET /api/vaccine/releaseList发布中的疫苗列表带出剩余号源POST /api/appointment/book提交预约POST /api/appointment/cancel取消预约POST /api/appointment/verify现场核销GET /api/stats/overview统计看板数据以预约接口为例Service 层核心逻辑大概长这样Transactional(rollbackFor Exception.class) public Result book(AppointmentBookDTO dto) { // 1. 校验用户身份和预约资格 User user userService.getById(dto.getUserId()); if (user null) { return Result.error(用户不存在); } // 2. 校验疫苗时段是否还在放号状态 VaccinePeriod period vaccinePeriodMapper.selectById(dto.getPeriodId()); if (period null || period.getStatus() ! 1) { return Result.error(该时段不存在或已停止预约); } // 3. 扣减剩余号源用条件更新防止超卖 int updated vaccinePeriodMapper.decreaseStock(dto.getPeriodId()); if (updated 0) { return Result.error(该时段号源已被约满); } // 4. 生成预约记录 Appointment appointment new Appointment(); appointment.setUserId(user.getId()); appointment.setPeriodId(period.getId()); appointment.setAppointmentNo(generateNo()); appointment.setStatus(1); appointmentMapper.insert(appointment); return Result.success(appointment); }最核心的一行是decreaseStock它是一条带条件的 UPDATE 语句通过数据库层面保证库存不会减成负数具体原理在并发部分详细拆解。事务注解Transactional保证了扣库存和生成预约记录要么同时成功、要么同时回滚避免出现库存扣了但记录没生成的数据不一致问题。2.3 登录鉴权与角色权限的落地方式权限这块我没有引入 Shiro 或 Spring Security 这种重框架而是用拦截器 JWT 的轻量方案。原因很简单系统角色只有三种——管理员、接种点工作人员、普通居民权限规则复杂度有限用轻量方案完全够而且代码量少、容易看懂对学习者更友好。具体做法是登录成功之后生成 JWT把 userId 和 roleId 塞进 token前端每次请求在 header 里带Authorization: Bearer xxx。后端写一个AuthInterceptor在 preHandle 里解析 token、校验有效期、把用户信息放到 ThreadLocal 中。再配合一个RequireRole注解在需要管理员权限的接口上加注解拦截器里判断角色不匹配直接返回 403。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { response.setStatus(401); return false; } // 解析 JWT校验签名和过期时间 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); if (claims null) { response.setStatus(401); return false; } UserContext.set(claims.get(userId, Long.class), claims.get(roleId, Integer.class)); return true; } }用这种方式的好处是后续如果要加居民只能查看自己的预约记录这类数据权限直接在 Service 里从 UserContext 取 userId 过滤即可不需要写复杂的权限框架配置。我后来加我的预约页面时只加了一个 userId 条件五分钟就完成了。3. MySQL 数据表设计与字段约定3.1 核心表结构与建表要点数据库我一共设计了 9 张表核心几张表的结构如下。用户表sys_userid、username、passwordBCrypt 加密存储、real_name、phone、id_card、role_id、status、create_time。疫苗信息表vaccineid、vaccine_name、manufacturer、batch_no、vaccine_type对应第几针、dose_count、description、create_time。疫苗发布时段表vaccine_periodid、vaccine_id、period_date、start_time、end_time、total_stock、remain_stock、status、create_time。这张表是整个预约系统的核心一个批次疫苗发布后管理员按日期和时段拆成多条 period 记录每一条都代表一个可预约的时段。预约记录表appointmentid、appointment_no、user_id、period_id、appointment_time、status1 已预约、2 已取消、3 已核销、4 已爽约、verify_time、verify_user_id。这里特别强调一下 appointment_no 的设计。它不能简单用自增 id因为 id 太短容易被猜到别人只要连续请求就能遍历出所有预约号安全性和隐私性都不行。我使用的生成规则是日期 接种点编号 随机数 校验位例如20240115A1032917既方便人工识别又有一定随机性。3.2 库存字段为什么要单独建表第一次设计数据库时我犯过一个错误把 total_stock 和 remain_stock 直接放在 vaccine 表里每个疫苗只有一条记录预约时在一条记录上扣库存。表面看简单但实际业务里一个疫苗会拆成多个日期、多个时段每个时段的库存彼此独立。如果只在一个总库存上扣就会出现上午 8 点的号约完了下午 2 点的号也跟着不能约的尴尬情况业务上完全说不通。所以我把库存拆到 vaccine_period 表每个时段的库存单独管理。管理员发布疫苗时先选定一个疫苗然后批量生成未来 N 天的时段每个时段单独设置库存量。这样做的好处是预约按时间段隔离一个时段约满不影响其他时段核销时还能精确统计每个时段的到场率后续做数据分析也非常方便。3.3 字段类型和索引选择的经验三个实际踩过的坑都值得写下来。第一个是时间字段类型。MySQL 里 datetime 和 timestamp 的区别很多人分不清timestamp 有 2038 年问题并且受时区影响datetime 则更直观。这个项目里业务时间字段我统一用 datetimecreate_time 这种系统字段也统一用 datetime避免前后端时区不同导致显示错乱。第二个是状态字段的类型。我全部用 tinyint 而不是 varchar比如 appointment 表的 status 字段1 表示已预约、2 表示已取消、3 表示已核销、4 表示已爽约。数字的好处是查询快、占空间小而且代码里可以写常量类映射。坏处是可读性略差所以必须在代码注释和接口文档里写清楚每个数字的含义否则过两个月自己都忘了。第三个是索引。预约记录表里 user_id 和 period_id 都是高频查询字段必须建索引appointment_no 因为要用于核销查询也建了唯一索引顺便保证不重复。我的经验是建索引前先想清楚业务里最频繁的 WHERE 条件是什么不要见一个字段就加一个索引索引过多会拖慢写入速度。CREATE TABLE vaccine_period ( id bigint(20) NOT NULL AUTO_INCREMENT, vaccine_id bigint(20) NOT NULL COMMENT 疫苗ID, period_date date NOT NULL COMMENT 预约日期, start_time varchar(10) NOT NULL COMMENT 开始时间 HH:mm, end_time varchar(10) NOT NULL COMMENT 结束时间 HH:mm, total_stock int(11) NOT NULL DEFAULT 0 COMMENT 总号源, remain_stock int(11) NOT NULL DEFAULT 0 COMMENT 剩余号源, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-放号中 2-已停约 3-已过期, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_vaccine_id (vaccine_id), KEY idx_period_date (period_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时统一使用 InnoDB 引擎和 utf8mb4 字符集InnoDB 支持事务和行锁utf8mb4 才能完整存储生僻字和特殊符号。这两个是底线不能用错。4. 前端 Vue 页面结构与预约交互细节4.1 路由与菜单怎么组织前端我用 Vue 2 Element UI vue-router axios没有引入 Vuex因为项目的共享状态很少只有一个用户信息存在 localStorage 里就够了。路由按角色分成两套管理端和用户端。管理端路由是/admin/dashboard、/admin/vaccine、/admin/period、/admin/appointment、/admin/stats对应统计看板、疫苗管理、时段放号、预约核销、数据报表。用户端路由是/home、/vaccine/list、/appointment/my、/profile对应首页、疫苗列表、我的预约、个人中心。路由守卫有一个细节必须处理未登录用户访问任何业务页面都要跳转登录页登录之后根据角色决定能访问哪些路由。我用 vue-router 的beforeEach钩子实现router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else if (to.path /login token) { next(/home); } else { next(); } });菜单也是从后端接口返回的由角色动态生成。菜单表在数据库里用 parent_id 维护层级前端拿到数据后递归渲染成侧边栏。这样做的好处是管理员在页面上就能调整菜单内容不需要改代码重新发布对后期维护非常友好。4.2 预约流程的前端校验与体验优化预约页面是整个前端交互最复杂的部分。用户进入疫苗详情页后前端请求后端接口拿到未来几天的时段列表每个时段显示日期、时间段和剩余号源。剩余号源为 0 的时段直接置灰不可点击。这里有一个交互细节用户选择时段后需要确认个人信息姓名、身份证号、手机号因为预约信息要跟接种现场的核销对得上。身份证号校验我放在前端做一次、后端再做一次格式是 18 位最后一位可以是 X。手机上还有个常见问题用户填完表单切到别的应用再回来页面可能已经刷新了所以填表过程中要把已填内容自动保存到本地回来之后能直接恢复这个体验细节很影响口碑。真正提交预约之后前端跳转到预约成功页显示预约编号和二维码。二维码我用qrcode库生成内容是预约编号的 JSON 串现场核销时管理员扫二维码就能拿到编号。用二维码而不是纯数字好处是减少人工输入错误现场效率高很多。没有扫码枪的小接种点也可以直接在系统里输入编号查询。还有一个必须注意的细节提交按钮的防重复点击。点击预约之后立刻把按钮设为 loading 和 disabled 状态等接口返回再恢复。否则用户手快点了两次后端又没做好幂等控制就会产生两条重复预约记录。这部分前后端要配合只靠任何一边都会有漏洞。4.3 axios 请求封装与 token 处理axios 我做了统一封装核心是两个拦截器。请求拦截器里自动带 tokenservice.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; });响应拦截器里统一处理业务码。后端接口返回结构是{ code: 200, message: success, data: {...} }code 为 200 表示成功401 表示未登录403 表示无权限其他为业务错误。前端在响应拦截器里判断 code401 就清掉 token 跳登录页403 就弹出无权限提示业务错误直接 message 提示。service.interceptors.response.use( response { const res response.data; if (res.code 200) { return res; } else if (res.code 401) { localStorage.removeItem(token); router.push(/login); return Promise.reject(res); } else { Message.error(res.message); return Promise.reject(res); } }, error { Message.error(网络异常请稍后重试); return Promise.reject(error); } );这个封装看起来简单但省掉了每个页面重复写错误处理代码的体力活。项目里所有请求都走统一通道后期加新功能页面时只需要关注业务参数和结果使用不需要再关心 401、403 这些通用异常怎么处理。5. 从零到直接运行环境配置与启动避坑5.1 环境准备清单拿到源码想直接跑起来第一步是准备环境。我用的这些版本在自己机器上装对应版本即可大版本一致就没有问题软件版本用途JDK1.8 或 11运行 SpringBootMaven3.6后端依赖管理MySQL5.7 或 8.0数据库Node.js14前端构建开发工具IntelliJ IDEA VS Code开发调试一个容易忽略的点Maven 仓库下载速度。在国内环境建议在 Maven 的 settings.xml 里配置阿里云镜像否则第一次mvn clean package下载依赖可能要等很久。这不是项目本身的问题但踩的人非常多提前配好能省半小时。前端依赖安装用npm install如果网速慢或某些依赖安装失败可以用npm install --registryhttps://registry.npmmirror.com指定镜像源。装完之后npm run dev启动开发服务器默认端口是 8080和后端端口不一样这里涉及前后端分离项目的代理配置下面细说。5.2 数据库初始化和配置文件修改后端项目的application.yml里需要改三处数据库地址、数据库账号密码、JWT 密钥。JWT 密钥改成自己的随机字符串长度至少 32 位生成的 token 才不容易被暴力破解。spring: datasource: url: jdbc:mysql://localhost:3306/vaccine_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true数据库初始化文件放在db/init.sql用 Navicat 或命令行执行均可。执行前注意库名脚本开头有CREATE DATABASE IF NOT EXISTS vaccine_system和USE vaccine_system不会建错地方。脚本里既包含建表语句也包含初始数据比如管理员账号admin/123456启动后可以直接登录。如果执行时遇到Unknown collation之类的报错大概率是 SQL 文件里的字符集和本机 MySQL 版本不匹配。把脚本里的utf8mb4_general_ci改成utf8mb4_0900_ai_ci8.0 默认或者反过来问题就解决了。5.3 前后端联调时最常见的两个报错第一个是跨域报错。前端地址是http://localhost:8080后端是http://localhost:9090端口不同浏览器同源策略会拦截请求。我的做法是在后端写一个 CORS 配置类允许指定来源跨域访问Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }第二个是登录接口报 401。新手经常遇到后端 token 校验没问题但前端请求带不上 token 的情况。排查思路是先打开浏览器开发者工具的 Network 面板看请求头里有没有Authorization。如果没有检查请求拦截器是不是没有在 axios 实例上生效如果有再看后端是否解析成功。绝大多数问题出在前端——token 存进 localStorage 后页面刷新再发起请求拦截器里一定能拿到除非 key 写错或者用了 sessionStorage 被新标签页弄丢了。6. 并发预约下的超卖问题与锁方案6.1 超卖问题是怎么发生的预约系统最怕超卖明明只剩 10 个号结果 20 个人都预约成功。超卖的本质是并发下的事务隔离问题。假设某个时段剩余号源 remain_stock 1两个用户同时发起预约请求。如果 Service 层逻辑是先查剩余号源再判断大于 0然后扣减高并发下会发生这样的情况用户 A 的事务查询到 remain_stock 1进入判断准备扣减用户 B 的事务也查询到 remain_stock 1同样进入判断准备扣减A 更新 remain_stock 为 0提交成功B 也更新 remain_stock 为 0提交成功两条预约都成功但剩余号源变成 0。这就是经典的先查后改竞态条件。很多初学者写的预约代码都是这个模式压力测试一上就露馅。6.2 几种锁方案的对比解决超卖网上能搜到很多方案我逐个试过简单说下对比方案实现方式优点缺点悲观锁SELECT ... FOR UPDATE锁住库存行绝对安全锁等待时间长并发低乐观锁更新时比较 version 字段冲突少时性能好冲突多时大量重试体验差条件更新UPDATE ... WHERE remain_stock 0简单高效数据库原子保证需要检查影响行数Redis 分布式锁SETNX 过期时间跨服务可用引入额外组件锁续期要处理Redis 预减库存先用 Redis 扣异步同步库性能最好架构复杂数据一致性维护成本高对于这个项目的量级最后选了条件更新也就是直接写一条带条件的 UPDATE 语句让数据库保证原子性。表中这行记录被更新时MySQL 会自动加行锁两个并发事务只有一个能成功另一个等待锁释放后重新判断条件发现 remain_stock 已经是 0影响行数为 0就知道号源没了。6.3 我的落地方案和压测结果条件更新的 SQL 长这样Update(UPDATE vaccine_period SET remain_stock remain_stock - 1 WHERE id #{periodId} AND remain_stock 0) int decreaseStock(Param(periodId) Long periodId);Service 层调用后判断返回值int affected vaccinePeriodMapper.decreaseStock(period.getId()); if (affected 0) { throw new BusinessException(该时段号源已被约满); }只加这一条 SQL 还不够我在book方法上加了Transactional同时给预约记录表加了(user_id, period_id)的唯一索引防止同一个用户对同一个时段重复预约。两把锁配合把超卖和重复预约都堵死了。我用 JMeter 模拟了 100 个线程同时对同一个时段发预约请求时段的剩余号源设置为 30。压测结果是成功预约 30 条其余 70 条全部返回号源已被约满数据库里 remain_stock 正确归零没有任何超卖。这个方案在单机部署、单数据库实例场景下非常可靠代码量还少对当前项目规模属于性价比最高的选择。提示如果系统将来要部署多个实例或者请求量真的到了每秒几千再考虑引入 Redis 分布式锁。规模没到之前不要给自己增加额外的运维负担。7. 运行一段时间后的问题反思与扩展方向7.1 实际使用中发现的几个问题项目上线跑了一段时间我总结出几个当初设计时没考虑到位的地方。第一个是爽约问题。预约了不来号源就浪费了疫苗还有保质期和冷链要求浪费的代价比普通门诊号更大。一开始我只做了预约 → 核销两个状态后来加了爽约状态和爽约次数统计并且规定一个用户爽约 3 次就限制预约 7 天。规则不复杂但需要在 appointment 表加 miss_reason 字段核销时间超过预约时段结束时间还没核销的由定时任务自动标记为爽约。第二个是通知缺失。预约成功、时段临近、预约取消这些节点用户只能自己登录系统查。后来我在系统里集成了短信和公众号模板消息接口按需开通在关键节点自动推送。如果只是本地部署试用最简方案是把通知内容展示在系统站内信里用户登录后可见成本最低。第三个是疫苗批次追溯颗粒度不够。现在只记录了疫苗名称和厂家严格规范要求要追溯到具体批号batch_no甚至每一支疫苗的唯一码。表设计里预留了 batch_no 字段但接种核销时没有强制录入每一支的编号严格追溯还差一步。这个要想做得规范核销流程需要改成先扫疫苗码再扫预约码的双扫码模式。7.2 代码层面还可以优化的地方如果继续迭代我会先做三件事。第一把预约接口做成幂等的。现在靠唯一索引兜底重复提交会直接报错体验不够优雅。更好的做法是前端生成一个 requestId后端收到请求时先查有没有处理过这个 requestId有就直接返回已有结果。这样即使前端网络超时重试用户也不会看到重复预约的报错。第二把统计查询从联表 COUNT 改成定时汇总表。现在统计报表是实时查 appointment 表数据量小的时候没问题但积累了十几万条记录后报表接口会越来越慢。可以加一张 daily_stats 表每天凌晨定时任务把前一天的预约量、核销量、爽约量刷进去报表接口只查汇总表性能稳定很多。第三把时段配置做成模板化。现在管理员发布疫苗要一天一天选时段下一个批次的疫苗还要重新配置一遍。可以做一个放号模板功能比如工作日每天 8 个时段、周末每天 6 个时段存成模板发布新疫苗时直接套用模板再微调几个时间点就行。7.3 给正在参考这个项目的同学的建议如果你拿这套系统做毕业设计或者课程项目不要只停留在能跑要主动展示你在真实问题上的思考深度。可以把并发预约这部分单独写成测试报告把 JMeter 压测数据截图放进去说明你对比过几种方案、为什么选条件更新、还有哪些扩展路径。这比罗列一堆功能点更能体现工程能力答辩时也更有说服力。如果是企业真实使用我的建议是先做小范围试点把接种点的核销流程跑顺再调整预约规则试运行一段时间最后才放开给全部居民。系统只是工具流程设计得好不好直接决定现场效率。试点期间每天整理反馈把预约时限太短取消规则不明确这类问题逐个解决再全面推广。另外这类管理系统一定要重视日志。我在 Service 层关键操作上都加了log.info记录 userId、periodId、操作类型和结果。出了纠纷要查记录时日志和数据库记录必须能对上。这一点我是吃了亏才补上的一开始偷懒没加日志用户打电话说明明预约成功了你们说没有查了半天才发现他把预约取消了这才意识到操作日志的重要性。回到最初做这个项目的出发点一套能直接运行的完整源码最大价值不是让你抄而是让你看到一个真实业务系统从头到尾是怎么被拆解和实现的。疫苗发布、时段预约、现场核销、统计分析每一条链路都不算复杂但合在一起就是一个能实际投入使用的小系统。我自己做完之后最大的体会是这类管理系统真正难的不是技术而是把业务规则想清楚、把并发场景的漏洞堵上、把用户体验的细节打磨到位。这套源码在这个方向上是合格的希望它也能让你少走一些弯路。