疫苗发布与接种预约系统实战:SpringBoot+Vue+MySQL全栈解析
SpringBoot Vue MySQL 这套疫苗发布和接种预约系统的源码我近期反复跑了很多遍。说实话绝大多数人拿到源码后最容易卡住的不是业务逻辑而是环境匹配和启动顺序数据库脚本导不进去、后端端口起不来、前端连不上接口随便一个问题都能消磨掉半天耐心。这篇文章不会去复述项目文档而是以我实际运行、拆解和二次改造这套源码的经验为主线把项目在做什么、表怎么设计、后端接口怎么写、前端怎么联调以及怎么在本地快速跑通完整地过一遍。内容同样适合想拿类似管理系统练手、做课程设计或准备二次开发的朋友。1. 系统到底做什么核心模块与使用场景1.1 系统的业务闭环“疫苗发布和接种预约系统”本质上就是一个面向公共卫生场景的管理信息系统核心是两类角色普通用户和管理员。普通用户需要看到接种点发布的疫苗批次消息选择合适的时间段完成预约再随时查看自己的预约状态管理员则要维护疫苗基础信息、批次库存发布接种公告并在用户到现场后核对预约单、录入接种记录。把这两条线串起来就是一个完整的业务闭环发布疫苗 → 用户查询 → 在线预约 → 现场接种 → 记录归档。这个闭环听起来简单真正落地时会牵扯到不少细节。疫苗不是普通商品它有过期时间、批次号、库存数量同一个疫苗可能对应多个不同效期的批次预约也不是随便填个日期就完事要限制一个人不能重复预约同一批次还要保证多人同时抢约时库存不会变成负数。这些恰恰是这套源码里最值得研究的地方也决定了系统的数据表结构和接口逻辑。1.2 核心功能模块一览我拆解这套源码时习惯先把功能模块表拉出来再反推表结构和接口设计。下面是这套系统的主要模块划分模块面向角色主要功能对应核心表登录注册用户/管理员账号密码登录、手机号注册、JWT 鉴权sys_user疫苗发布管理员维护疫苗信息、批次库存、上下架vaccine_info、vaccine_batch公告管理管理员发布接种通知、注意事项、到苗提醒announcement在线预约用户选择疫苗批次、接种点、日期时段生成预约单appointment_order接种记录管理员/用户录入实际接种时间、批号、第几针vaccination_record预约管理管理员查看预约列表、确认接种、取消过期预约appointment_order实际项目里可能还会加一个 vaccination_site 接种点表用来维护接种点名称、地址、每日最大接待量。即便你的场景不是疫苗而是体检预约、证件办理预约、实验室设备预约这套模块拆分方式也是通用的换掉业务字段就能复用。1.3 适合什么人参考如果你是刚学完 SpringBoot 基础、想找一个完整项目练手的人这套系统的代码量不会大到劝退Controller、Service、Mapper 分层清晰能帮你把前后端数据流动走通。如果你是在做课程设计或者毕业设计这类管理系统最大的优点是业务话题明确功能边界清楚写论文时功能描述、需求分析、数据库设计都能直接拿来用而且可以顺着预约流程往下扩展比如加个导出统计、预约提醒工作量可控。如果你是已经在工作中的开发想看看别人怎么处理预约防重、库存扣减、JWT 鉴权这些通用问题这套源码同样有参考价值尤其是预约接口的并发处理思路值得多看几遍。2. 技术选型思路为什么 SpringBoot、Vue、MySQL 组合最稳2.1 为什么后端选 SpringBoot 而不是 SSM做这种管理信息系统最怕的不是功能复杂而是配置琐碎。早些年 SSM 时代Spring、SpringMVC、MyBatis 三个框架要分别写配置还要处理各种 xml 文件很多时间都花在环境搭建上。SpringBoot 的核心价值就是自动配置把大量约定好的默认行为直接内置我只要在 pom.xml 里引入依赖写一个启动类内置的 Tomcat 就能把服务跑起来java -jar一个命令完成部署。这套源码选 SpringBoot还因为它和 MyBatis-Plus 搭配非常省事。MyBatis-Plus 把单表 CRUD 做了封装简单的增删改查不用手写 SQL复杂查询再写 XML开发速度提升很明显。对预约系统这种以单表操作为主、少量多表联查的中小型项目来说SpringBoot MyBatis-Plus 是性价比极高的选择没必要引入太重的东西。2.2 为什么前端选 Vue 而不是 JSP 模板老一代管理系统喜欢用 JSP 或 Thymeleaf 做服务端渲染但这种方式最大的问题是前后端耦合太紧改一个按钮样式都可能要重启服务前端代码和后端 Java 代码混在同一个工程里后期维护很痛苦。Vue 的价值在于组件化和响应式。页面可以拆成疫苗列表、预约弹窗、记录表格这些独立组件组件之间互不干扰数据变化时视图自动更新不用手动操作 DOM。配合 Element UI 组件库一个后台管理界面半天就能搭起来。更重要的是Vue 工程能单独开发、单独部署和后端只通过接口通信开发期用代理解决跨域生产期可以打包成静态文件交给后端托管非常灵活。这套源码选 Vue本质上是选了现代前端的主流工作方式。2.3 为什么数据库选 MySQL 以及整体请求链路MySQL 在这套系统里的角色是存储一切业务数据的底座。选择它不是因为多高级而是因为它免费、轻量、开发者普遍熟悉、事务支持可靠。预约系统对数据一致性有要求疫苗批次库存扣减必须在事务里完成InnoDB 引擎的行锁和乐观锁机制能很好地支撑这种场景。整套系统的请求链路我用文字描述一下浏览器加载 Vue 页面后用户点击操作触发 Vue Router 路由变化页面组件调用 axios 发送 HTTP 请求请求经过前端开发服务器的代理转发到 SpringBoot 的 ControllerController 负责接收参数和身份校验调用 Service 层做业务处理Service 再通过 MyBatis-Plus 操作 MySQL 数据表拿到结果后一步步封装成 JSON 返回给前端前端根据响应码更新页面状态。理解这条链路后面所有排错工作都有方向了。3. 数据库设计表结构和预约状态机的关键设计3.1 核心表结构与字段说明这套系统我见到的版本表数量一般在六到八张之间。除了前面模块表里提到的我实际拆解时重点关注这六张sys_user用户表。字段包括主键 id、登录账号 username、passwordBCrypt 加密存储、姓名、身份证号、手机号、角色 role1 用户、2 管理员。登录账号要加唯一索引。vaccine_info疫苗基础信息表。保存疫苗名称、生产厂家、剂型、接种间隔天数等固定属性。vaccine_batch疫苗批次表。保存批号、生产日期、有效期、剩余库存 stock、上下架状态 status。同一疫苗可以有多个批次所以疫苗 id 在这里是逻辑外键。vaccination_site接种点表。保存接种点名称、地址、联系电话、每日最大接待量。appointment_order预约单表。这是整个系统的核心表保存预约单号、用户、疫苗批次、接种点、预约日期、预约时段、预约状态。vaccination_record接种记录表。保存实际接种时间、接种第几针、操作人一条预约单只能对应一条有效接种记录。3.2 建表 SQL 实操这里我给出三张核心表的建表 SQL其他表结构类似就省略了。注意字符集要用 utf8mb4不然存不了生僻字和特殊符号。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(64) NOT NULL COMMENT 登录账号, password VARCHAR(128) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(32) DEFAULT NULL COMMENT 真实姓名, id_card VARCHAR(20) DEFAULT NULL COMMENT 身份证号, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 1 COMMENT 1用户 2管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE vaccine_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, vaccine_id BIGINT NOT NULL COMMENT 疫苗信息表id, batch_no VARCHAR(64) NOT NULL COMMENT 生产批号, production_date DATE DEFAULT NULL COMMENT 生产日期, expire_date DATE NOT NULL COMMENT 有效期至, stock INT NOT NULL DEFAULT 0 COMMENT 剩余库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_batch_no (batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫苗批次表; CREATE TABLE appointment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 预约单号, user_id BIGINT NOT NULL COMMENT 用户id, batch_id BIGINT NOT NULL COMMENT 疫苗批次id, site_id BIGINT NOT NULL COMMENT 接种点id, appoint_date DATE NOT NULL COMMENT 预约日期, appoint_slot TINYINT NOT NULL COMMENT 时段 1上午 2下午, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待接种 1已接种 2已取消 3已失效, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, UNIQUE KEY uk_order_no (order_no), KEY idx_user_date (user_id, appoint_date, appoint_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约单表;建表之后别忘了给管理员账号初始化一条数据。密码直接用 BCrypt 工具类生成一串密文我一般会用在线工具或者写个临时 main 方法生成再把 username 设为 adminrole 设为 2。3.3 预约与库存约束唯一索引和乐观锁很多人建表只关注字段能否存下数据忽略了业务约束结果后面接口各种出 bug。这套源码在这块做得比较到位值得抄作业。第一个约束是防止重复预约。同一用户不能对同一个疫苗批次重复预约虽然可以在 Service 层先查再插但并发请求下查询和插入之间存在时间差可能两条请求同时通过查询导致重复数据。最稳妥的办法是建唯一索引但这套表结构里预约维度比较复杂同一用户同一日期同一时段只能约一次可以在业务层加查询校验必要时再建组合唯一索引把 user_id、batch_id、appoint_date、appoint_slot 联合做唯一约束。第二个约束是防止超卖。疫苗批次库存是一个数字多人同时预约时如果先查库存再更新库存很容易变成负数。正确做法是使用乐观锁更新直接执行一行 SQL让数据库判断剩余库存是否大于零影响行数为 0 就说明库存不足事务回滚。这个逻辑我在第四章里放了一份可以直接用的代码。4. 后端核心实现预约接口、登录鉴权和配置文件4.1 项目包结构与分层拿到源码第一步先看包结构。这套项目的分包方式很主流基本不用适应就能接手com.example.vaccine ├── controller // 接口层接收请求、返回结果 ├── service // 业务层核心逻辑都在这里 ├── mapper // 数据访问层对应 MyBatis-Plus 的 Mapper 接口 ├── entity // 实体类对应数据库表 ├── config // 配置类拦截器、跨域、Web 配置 ├── common // 通用类统一返回体、异常处理、常量 └── util // 工具类JWT、日期处理等分层最核心的原则是Controller 不要写业务逻辑只负责参数校验和结果封装业务逻辑全部下沉到 ServiceMapper 只做数据库操作。这样做的好处是接口一眼就能看懂出问题时按照层次排查很快二次开发时也容易定位改哪一层。4.2 预约接口防重复与防超卖的完整实现预约接口是整套系统里含金量最高的一段代码也是我建议你重点看的地方。接口路径一般是POST /api/appointment/create接收的参数包括疫苗批次 id、接种点 id、预约日期、预约时段。注意用户 id 不要从前端传而是从 JWT token 里解析防止有人伪造参数替别人预约。核心实现思路我用代码说明Transactional public AppointmentOrder createOrder(AppointmentReq req, Long userId) { // 1. 防重复同一用户同一疫苗批次同一时段只能预约一次 int exists appointmentMapper.countByUserAndBatch( userId, req.getBatchId(), req.getAppointDate(), req.getAppointSlot()); if (exists 0) { throw new BizException(您已预约过该批次疫苗请勿重复提交); } // 2. 防超卖乐观锁扣减库存影响行数为0说明库存不足 int rows vaccineBatchMapper.reduceStock(req.getBatchId()); if (rows 0) { throw new BizException(该批次疫苗库存不足请选择其他批次); } // 3. 生成预约单 AppointmentOrder order new AppointmentOrder(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(userId); order.setBatchId(req.getBatchId()); order.setSiteId(req.getSiteId()); order.setAppointDate(req.getAppointDate()); order.setAppointSlot(req.getAppointSlot()); order.setStatus(0); appointmentMapper.insert(order); return order; }对应的库存扣减 SQL 在 Mapper 里这样写UPDATE vaccine_batch SET stock stock - 1 WHERE id #{batchId} AND stock 0这段设计的巧妙之处在于库存扣减和重复校验在同一个事务里任何一个环节失败都会回滚不会出现预约单生成了但库存没扣、或者库存扣了但预约单没生成的情况。加上Transactional注解后Spring 会管理事务的提交和回滚。4.3 JWT 登录鉴权从登录到拦截器这套系统的登录鉴权用的是 JWT整体流程很标准用户提交账号密码后端校验通过后生成 token 返回前端把 token 存在 localStorage 里之后每个请求都在请求头带上Authorization: Bearer token后端通过拦截器统一校验。JWT 工具类核心代码大概是这样public class JwtUtil { private static final String SECRET your-secret-key-change-me; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器里做的事情也简单从请求头取出 token调用 parseToken 解析解析成功就放行并把用户信息放入 ThreadLocal 或请求上下文解析失败就返回 401。管理员接口还要额外判断 role 是否为 2不是就拒绝访问。这个方案比 Session 更适合前后端分离因为后端服务无状态扩展时不用考虑 Session 同步的问题。4.4 application.yml 与 MyBatis-Plus 的关键配置配置这块是很多人跑不起来项目的重灾区。一套能直接运行的配置长这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/vaccine?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里最容易被忽略的是serverTimezoneAsia/Shanghai。MySQL 8.x 的默认时区是 UTC如果不在连接串里指定插入和查询的时间会差 8 个小时。allowPublicKeyRetrievaltrue是为了解决 MySQL 8.x 使用 caching_sha2_password 认证插件时的一些异常加上之后能少踩一个坑。如果你用的是 MySQL 5.7驱动类可以换成com.mysql.jdbc.Driver但更推荐统一用 8.x 驱动它向下兼容 5.7。5. 前端搭建与联调路由、请求封装和页面流程5.1 前端目录结构与路由设计前端工程一般是标准的 Vue 项目结构src 目录下分成 views、router、api、components、utils 等文件夹。路由这块通常使用懒加载按页面模块拆分成用户端和管理端const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/Layout.vue), children: [ { path: vaccine, component: () import(/views/user/VaccineList.vue) }, { path: appointment, component: () import(/views/user/Appointment.vue) }, { path: record, component: () import(/views/user/MyRecord.vue) }, { path: admin/batch, component: () import(/views/admin/BatchManage.vue) }, { path: admin/order, component: () import(/views/admin/OrderManage.vue) } ] } ]路由守卫是必不可少的一环。我一般会在全局前置守卫里做两件事第一判断用户是否登录没有 token 就重定向到 /login第二判断目标路由是否需要管理员权限如果需要但当前用户的 role 不是 2就跳回首页并给出提示。这套机制比在每个页面里手动判断要省事得多。5.2 axios 封装、token 注入与跨域代理前端请求必须统一封装不然每个页面都写一遍 axios 逻辑后期维护是噩梦。核心思路是在 axios 实例的请求拦截器里注入 token在响应拦截器里统一处理错误码import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use(res { const code res.data.code if (code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录已过期)) } if (code ! 200) { Message.error(res.data.msg || 请求失败) return Promise.reject(new Error(res.data.msg)) } return res.data }, err { Message.error(err.response?.data?.msg || 网络异常) return Promise.reject(err) }) export default service开发环境下前后端端口不同存在跨域问题。最稳妥的做法是配置 devServer 代理让浏览器认为所有请求都发到同一个域名下从而绕过跨域限制module.exports { devServer: { port: 8088, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这段配置的意思是当前端请求/api/appointment/create时开发服务器会自动转发到http://localhost:8080/api/appointment/create。注意 target 端口必须和后端server.port一致否则就会收到 404 或者连接拒绝。5.3 预约页面核心交互与数据流前端预约流程我以用户视角走一遍进入疫苗列表页组件在mounted生命周期调用GET /api/vaccine/batch/list拉取上架中的疫苗批次渲染成卡片列表。用户点击“立即预约”后弹窗加载接种点列表和当前批次库余量选择日期和上午/下午时段点击确认时把表单数据提交到预约接口。这里有两个容易忽略的细节。第一个是提交按钮的 loading 状态在请求发出时要禁用按钮防止用户连续点击产生多个请求第二个是后端返回的预约单号要展示给用户看并保存到本地预约记录中。预约成功后页面跳转到“我的预约”展示该用户所有预约单状态字段映射成待接种、已接种、已取消、已失效等中文标签。6. 本地一键运行实操环境版本、初始化步骤与验证6.1 环境版本搭配我先给一套亲和度最高的环境组合。很多项目跑不起来不是代码问题而是版本之间不匹配尤其是 JDK、Node.js 和 SpringBoot 三者的版本关系必须对齐。组件推荐版本说明JDK1.8SpringBoot 2.7 的默认兼容版本Maven3.6.3建议配置阿里云镜像加速依赖下载MySQL5.7 或 8.08.0 需注意连接串时区参数Node.js14 LTSVue2 Element UI 最常见的版本环境npm6.14与 Node14 配套IDEA2021.3装好 Lombok 插件如果你拿到的是 SpringBoot 3.x 版本的源码那就必须用 JDK 17不要硬搭 JDK 1.8启动会直接报UnsupportedClassVersionError。判断方法很简单看 pom.xml 里parent标签的 spring-boot-starter-parent 版本号2.7.x 用 JDK 8 或 17 都可以3.x 必须 JDK 17。6.2 数据库初始化步骤数据库初始化是跑通整套系统最前置的一步顺序别搞错。先用命令行或 Navicat 创建数据库mysql -u root -p CREATE DATABASE vaccine DEFAULT CHARACTER SET utf8mb4;然后导入源码里提供的vaccine.sql脚本。如果源码没有提供 SQL 脚本就需要手动执行我前面给出的建表语句并初始化一条管理员账号。导入完成后再确认三件事表是否完整、admin 账号是否存在、密码是否为 BCrypt 密文。确认无误后打开后端项目的application.yml把数据库账号密码改成你自己本地的配置。6.3 后端启动流程后端启动我用 IDEA 走一遍。首先File - New - Project from Existing Sources选择后端项目目录Maven 会自动识别 pom.xml然后等待依赖下载完成。依赖下载慢是常态我一般会在settings.xml里加阿里云镜像mirror idaliyunmaven/id mirrorOf*/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror依赖下载完后找到启动类VaccineApplication.java右键运行。看到类似Tomcat started on port(s): 8080的日志就说明启动成功。注意启动类上方一定要有SpringBootApplication注解并且启动类放在所有 Controller、Service 的包最外层这样才能被 Spring 扫描到。6.4 前端启动与验证前端启动前先确认 Node 和 npm 版本。然后进入前端工程目录执行安装npm config set registry https://registry.npmmirror.com npm install这里特别提醒一句如果你用的是 Node 17 以上版本安装老项目依赖时常常会报错因为项目里可能用到了node-sass它对新版 Node 的兼容性很差。最省事的办法是直接用 Node 14要么就把样式方案改成sass。依赖安装完成后执行npm run serve看到类似App running at: http://localhost:8088就说明前端起来了。浏览器访问先跳到登录页用管理员账号登录进入后台发布一个疫苗批次再注册一个普通用户走一遍预约流程。预约成功后回去看数据库appointment_order表确认有一条状态为 0 的记录同时vaccine_batch表对应的批次库存减了 1整套系统就算真正跑通了。6.5 生产部署的两种方式开发环境跑通了部署上线还有个选择。第一种是前后端分开部署后端打成 jar 包运行前端npm run build生成 dist 静态目录交给 Nginx 托管再用 Nginx 反代后端接口。第二种更省事把前端 dist 目录下的静态文件复制到后端项目的resources/static目录下再重新打包一个 jar 就能同时提供页面和接口。第二种方式要注意两点前端 axios 的baseURL不能写死成开发环境的代理路径最好改成相对路径/apiVue Router 如果用了 history 模式后端要加一个资源映射把非接口路径都转发到 index.html否则刷新页面会 404这种情况一般加一个简单的 WebMvcConfigurer 就能解决。7. 高频问题排查与实战避坑7.1 MySQL 连接失败与时区报错新手跑后端项目十有八九会卡在数据库连接。最常见的一个报错是The server time zone value UTC is unrecognized原因是 MySQL 8.x 默认时区不是中国时区解决方式是在连接串里加serverTimezoneAsia/Shanghai。另一个常见报错是Public Key Retrieval is not allowed原因同样是 MySQL 8.x 默认的认证插件问题连接串加上allowPublicKeyRetrievaltrueuseSSLfalse即可。如果密码没错但连接失败去检查 MySQL 服务是否启动。Windows 下打开服务管理器确认 MySQ L 服务状态Linux 下执行systemctl status mysqld看一眼。很多时候不是代码问题是服务根本没起来。7.2 端口占用与启动失败后端启动日志提示端口被占用是最典型的报错。不同系统的排查命令不太一样Windows 用netstat -ano | findstr 8080查到占用进程的 PID 后要么结束进程要么给项目换端口。我个人的习惯是优先换端口因为系统里的进程分不清哪些能用哪些不能用乱杀容易误伤。改端口直接在application.yml里改一下就行前端代理的 target 端口记得同步修改。还有一个细节是 IDEA 启动时可以通过配置覆盖端口。在 Run Configuration 里的 Program arguments 加--server.port8081或者在 Environment variables 里设置SERVER_PORT8081效果一样。这样不需要改代码就能换端口排查环境问题时很方便。7.3 前端接口 404 或跨域被拦截前端页面能打开但接口全部 404第一步先确认后端是否真的启动成功然后确认代理配置的 target 端口是否正确。我见过太多人改了后端端口却忘了改 vue.config.js 里的 proxy结果前端还在往旧的 target 上发请求。跨域报错说明代理没生效。如果你直接用 axios 访问http://localhost:8080/api/xxx而不是走/api/xxx相对路径就绕过了 devServer 代理浏览器会拦截跨域响应。这种情况下要么统一用baseURL: /api要么在后端配置全局 CORS。开发期我推荐前者生产部署时也更干净。7.4 npm install 太慢或失败前端依赖安装失败九成是网络问题。先执行npm config set registry https://registry.npmmirror.com换成国内镜像再删除node_modules和package-lock.json重新安装。如果项目依赖里有node-sass在 Node 高版本下经常编译失败报错信息里能看到node-gyp或python的字样这时最稳妥的方案是把 Node 降到 14或者把依赖整体升级。补充一个技巧安装完成后如果个别依赖缺失导致启动报错不要整个重装直接npm install 缺失的包名单独补装速度会快很多。7.5 预约并发与数据一致性问题演示系统时最怕出现两个人同时预约同一个批次结果库存变成负数。这本质上是一个并发控制问题。我在 4.2 里用了乐观锁方案执行UPDATE ... WHERE stock 0只用一行 SQL 就能保证并发场景下库存不会被扣成负数代码简单且性能好。第二道防线是前端按钮 loading第三道防线是数据库唯一索引。三层都做了无论用户怎么疯狂点击数据都不会乱。这套思路不仅适用于疫苗预约任何库存类系统都可以直接抄。7.6 Lombok 和 JDK 版本引发的编译错误后端编译报错经常和 Lombok 有关。如果你用的是 JDK 17 但源码是 SpringBoot 2.xLombok 版本太老会直接编译失败提示找不到 getter/setter 方法。解决方式是把 Lombok 升级到 1.18.30 以上版本。如果你不想折腾直接用 JDK 1.8 最保险。另一个容易踩的坑是源码的target/complile提示Error: java: 无效的源发行版, 多半是 IDEA 的 Project Structure 里 Project SDK 和 Project language level 不一致。把语言级别改成 8JDK 版本选成 1.8重新编译即可。这套系统我前后跑了很多遍每次重装环境都会遇到不一样的小问题但核心永远集中在三处数据库连接、token 传递、预约库存扣减。如果你目标是先把界面跑起来环境版本对齐就能省掉一大半时间如果你打算二次开发我建议先吃透appointment_order和vaccine_batch两张表的设计再去看 Controller 层效率会高很多。我自己最常用的一招是把后端日志级别调到 DEBUG然后再点一次预约按钮所有的请求参数、SQL 和异常都打出来了问题一眼就能定位。这套源码的架构不算新颖但业务闭环完整该踩的坑都替你踩过了拿来做学习参考很值得。