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 的 Controller;Controller 负责接收参数和身份校验,调用 Service 层做业务处理,Service 再通过 MyBatis-Plus 操作 MySQL 数据表,拿到结果后一步步封装成 JSON 返回给前端,前端根据响应码更新页面状态。理解这条链路,后面所有排错工作都有方向了。
3. 数据库设计:表结构和预约状态机的关键设计
3.1 核心表结构与字段说明
这套系统我见到的版本,表数量一般在六到八张之间。除了前面模块表里提到的,我实际拆解时重点关注这六张:
sys_user:用户表。字段包括主键 id、登录账号 username、password(BCrypt 加密存储)、姓名、身份证号、手机号、角色 role(1 用户、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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约单表';建表之后别忘了给管理员账号初始化一条数据。密码直接用 BCrypt 工具类生成一串密文,我一般会用在线工具或者写个临时 main 方法生成,再把 username 设为 admin,role 设为 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 不要写业务逻辑,只负责参数校验和结果封装;业务逻辑全部下沉到 Service;Mapper 只做数据库操作。这样做的好处是接口一眼就能看懂,出问题时按照层次排查很快,二次开发时也容易定位改哪一层。
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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true 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这里最容易被忽略的是serverTimezone=Asia/Shanghai。MySQL 8.x 的默认时区是 UTC,如果不在连接串里指定,插入和查询的时间会差 8 个小时。allowPublicKeyRetrieval=true是为了解决 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 三者的版本关系必须对齐。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SpringBoot 2.7 的默认兼容版本 |
| Maven | 3.6.3+ | 建议配置阿里云镜像加速依赖下载 |
| MySQL | 5.7 或 8.0 | 8.0 需注意连接串时区参数 |
| Node.js | 14 LTS | Vue2 + Element UI 最常见的版本环境 |
| npm | 6.14 | 与 Node14 配套 |
| IDEA | 2021.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> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <url>https://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不能写死成开发环境的代理路径,最好改成相对路径/api;Vue Router 如果用了 history 模式,后端要加一个资源映射,把非接口路径都转发到 index.html,否则刷新页面会 404,这种情况一般加一个简单的 WebMvcConfigurer 就能解决。
7. 高频问题排查与实战避坑
7.1 MySQL 连接失败与时区报错
新手跑后端项目,十有八九会卡在数据库连接。最常见的一个报错是The server time zone value 'UTC' is unrecognized,原因是 MySQL 8.x 默认时区不是中国时区,解决方式是在连接串里加serverTimezone=Asia/Shanghai。另一个常见报错是Public Key Retrieval is not allowed,原因同样是 MySQL 8.x 默认的认证插件问题,连接串加上allowPublicKeyRetrieval=true&useSSL=false即可。
如果密码没错但连接失败,去检查 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.port=8081,或者在 Environment variables 里设置SERVER_PORT=8081,效果一样。这样不需要改代码就能换端口,排查环境问题时很方便。
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.x,Lombok 版本太老会直接编译失败,提示找不到 getter/setter 方法。解决方式是把 Lombok 升级到 1.18.30 以上版本。如果你不想折腾,直接用 JDK 1.8 最保险。
另一个容易踩的坑是源码的target/complile提示Error: java: 无效的源发行版, 多半是 IDEA 的 Project Structure 里 Project SDK 和 Project language level 不一致。把语言级别改成 8,JDK 版本选成 1.8,重新编译即可。
这套系统我前后跑了很多遍,每次重装环境都会遇到不一样的小问题,但核心永远集中在三处:数据库连接、token 传递、预约库存扣减。如果你目标是先把界面跑起来,环境版本对齐就能省掉一大半时间;如果你打算二次开发,我建议先吃透appointment_order和vaccine_batch两张表的设计,再去看 Controller 层,效率会高很多。我自己最常用的一招是:把后端日志级别调到 DEBUG,然后再点一次预约按钮,所有的请求参数、SQL 和异常都打出来了,问题一眼就能定位。这套源码的架构不算新颖,但业务闭环完整,该踩的坑都替你踩过了,拿来做学习参考很值得。