做信息管理系统这些年,我接过不少类似的需求,也帮周围同学和同行排查过无数个“明明按教程走就是跑不起来”的鬼故事。今天拿一套疫苗发布和接种预约系统出来彻底拆一遍,这是非常典型的 SpringBoot + Vue + MySQL 全栈项目,后端 SpringBoot,前端 Vue,数据库 MySQL,刚好覆盖一条完整的中小型管理平台链路。整套源码本地实测过,环境配好就能跑,特别适合拿来做毕业设计、课程设计,或者作为全栈练手项目参考。
这类系统的现实意义不用多说——疫苗从入库、发布到预约接种,整个流程如果靠手工登记和微信接龙管理,很容易出现信息不同步、名额超卖、现场排长队的问题。而一套信息管理系统要解决的核心问题就两个:一是让管理员能高效发布疫苗信息、管理接种点库存;二是让普通用户能随时查看可预约的疫苗、在线完成预约、按时去现场接种。技术上的难点反而不是功能多复杂,而是如何保证“名额不超卖”“状态不混乱”“操作可追溯”,这是所有疫苗预约类系统都会踩的坑,也正是这套源码里最值得研究和复用的一部分。
下面我按项目梳理、技术选型、数据库设计、后端实现、前端实现、本地运行、问题排查这条线,事无巨细地聊一遍,尽量把源码里看不到的设计思路也补出来。
1. 项目整体设计与需求拆解
1.1 拆开标题看本质需求
标题里写着“疫苗发布和接种预约系统”,但源代码打开后你会发现,它其实是一个标准的双端管理平台:一个普通用户使用的预约前端,一个管理员使用的管理后台。把业务拆开,核心的数据流转大概是这样的:
- 管理员录入疫苗种类、批次、生产企业、有效期,并维护接种点的可预约库存。
- 管理员发布公告,用户登录后可以看到最近到货的疫苗和接种点开放信息。
- 普通用户选择疫苗、选择接种点、选择时间段,系统校验是否还有剩余名额。
- 校验通过后生成一条预约记录,同时扣减接种点的剩余名额。
- 用户按时到场,管理员在后台将预约状态更新为“已接种”,流程结束。
- 用户也可以取消未接种的预约,取消后名额自动释放回接种点。
这套流程表面上不复杂,但每一步都牵扯到状态变化和数据一致性。做这类系统最忌讳的就是把功能想得太简单,直接在 Controller 里堆 SQL,结果预约并发一上来,库存就变负数,或者同一个用户重复预约成功。源码里比较可取的一点是,预约处理时对名额扣减做了条件更新,后面我会详细讲。
1.2 功能模块梳理
从源代码的包结构和前端路由可以反推出完整的模块清单,我整理成表格方便对照:
| 模块 | 功能点 | 对应角色 |
|---|---|---|
| 用户认证 | 注册、登录、个人信息查看与修改 | 普通用户 |
| 疫苗信息 | 疫苗列表、疫苗详情、批次与有效期展示 | 普通用户 / 管理员 |
| 预约接种 | 选择接种点、时间、提交预约、取消预约 | 普通用户 |
| 我的预约 | 查看历史预约记录、当前状态 | 普通用户 |
| 公告管理 | 发布疫苗到货信息、系统通知 | 管理员 |
| 接种点管理 | 维护接种点名称、地址、开放时间、库存 | 管理员 |
| 疫苗管理 | 新增、编辑、上下架疫苗 | 管理员 |
| 预约记录管理 | 查看全部预约、确认接种、过期处理 | 管理员 |
| 用户管理 | 用户列表、禁用/启用账号 | 管理员 |
这个模块划分基本覆盖了实际业务里能想到的全部主体操作。对做毕设的同学来说,照着这个功能清单去写说明文档,也能把“需求分析”这一章节写得非常丰满。这里提一句,模块设计时把“公告管理”放在管理端而不是直接放在用户端,是更合理的做法,因为后台内容需要有人审核和发布,权限边界清楚,系统才不会乱。
1.3 这套源码“可直接运行”意味着什么
“可直接运行”这四个字在源码圈里的含金量差别很大。碰到过不少号称可直接运行的代码,下载下来要么缺数据库脚本,要么 node_modules 没带,要么端口和配置跟本机环境完全对不上,光折腾环境就能劝退一批人。这套源码比较省心的地方在于:数据库脚本齐全、前后端分离结构完整、配置文件位置集中,只要把 MySQL 的连接参数改掉,基本三步就能跑起来——建库导数据、启动后端、启动前端。
不过也别高兴得太早,能不能跑起来是一回事,跑起来之后能不能理解每一行代码在干什么,就是另一回事了。所以我后面所有章节都会结合源码目录结构和关键类,一点一点拆开讲。
2. 技术选型逻辑解析
2.1 后端框架:为什么是 SpringBoot 而不是 SSM 或别的
SpringBoot 在后端开发中的地位已经不需要再论证了。它最大的价值不是技术本身有多新奇,而是把 Spring 生态里那一大堆 XML 配置、繁琐的 Bean 装配给自动处理了,开发人员只需要通过注解和少量配置就能把一个 Web 应用跑起来。对疫苗预约系统这种 CRUD 为主、外加少量业务规则的场景,SpringBoot + Spring Data JPA 或者 MyBatis 都是非常成熟的选择。
源码里用的是 SpringBoot 的标准分层结构:Controller 层负责接收请求、Service 层处理业务逻辑、Repository/Mapper 层做数据访问、entity 包放实体类、dto 包放前后端交互的数据对象。这种分层看起来有点死板,但恰恰是最适合中小型管理系统长期维护的。不夸张地说,我刚带过的几个实习生,能把这套分层结构吃透,出去面全栈开发初级岗基本问题不大。
SpringBoot 内嵌 Tomcat 这一点也要提一下。传统 SSM 项目需要额外配置 Tomcat,而 SpringBoot 打出来的 jar 包可以直接通过java -jar启动,部署成本极低。做毕设答辩演示的时候,直接运行一个 jar 包比在 IDE 里折腾一堆配置要体面得多。
2.2 前端框架:Vue 的核心优势
Vue 在当前前端圈的使用量非常大,核心原因就是上手曲线平缓、组件化设计合理、中文生态资料丰富。这套系统的前端用的是 Vue 单页应用方案,配合 Vue Router 做路由管理,axios 发 HTTP 请求,UI 部分直接引入 Element UI(或类似组件库),所以页面看起来很规整,表单验证、弹窗、表格分页这些都不用自己从零写。
Vue 的响应式数据绑定在提交预约表单时非常舒服。用户在页面上选择疫苗后,系统可以自动联动加载对应接种点的剩余名额;选择时间片段后,提交按钮的动态禁用、剩余名额不足的红色提示,都能用v-model和computed轻松实现。这些在 jQuery 时代都得写一堆 DOM 操作,而在 Vue 里是数据驱动视图,代码可读性会高出一个档次。
有一点值得提醒:如果下载到的 Vue 代码是 2.x 版本,建议保持原版本不变,别急着升级到 Vue 3。因为 Element UI 和大量现成组件库在 Vue 3 下需要切换到 Element Plus,API 和写法都有不少变化,升级可能引入新的兼容问题。做项目以稳为主,能跑能改比追新重要得多。
2.3 数据库:MySQL 的适用场景
MySQL 是这个系统的数据基石。它是一个关系型数据库,数据严格结构化,支持事务和外键,正好匹配预约记录、用户信息、疫苗库存这类强关系数据。
数据库版本方面,5.7 和 8.0 都可用。如果本机安装的是 8.0,远程连接时需要注意认证插件问题——MySQL 8.0 默认的caching_sha2_password认证方式,某些旧版本的数据库连接驱动不一定支持,容易出现Public Key Retrieval is not allowed这类报错。解决办法有两个,要么把数据库用户密码改回mysql_native_password方式,要么在数据库连接 URL 上增加allowPublicKeyRetrieval=true参数。这是新手最容易卡一天的坑,这里先提个醒,后面问题排查章节还会再展开。
为什么这套系统不需要 Redis?很多同类预约系统会引入 Redis 做库存预扣减,但代价是引入了分布式一致性的复杂度。如果接种点规模不大、单机并发也不过几十上百,MySQL 的事务和行级锁完全能扛住,没必要为了技术炫技增加系统负担。这套源码用 MySQL 自带的条件更新来解决并发扣减,方案更轻量也更适合作为教学和毕设项目。
3. 数据库设计与表结构拆解
3.1 核心数据表有哪些
打开源码里的数据库脚本文件,大概会看到六到七张核心表,我把它们梳理一下:
| 表名 | 核心字段 | 作用 |
|---|---|---|
| user | id, username, password, real_name, phone, id_card, role, status | 存储用户和管理员账号 |
| vaccine | id, name, manufacturer, batch_no, category, description, status | 存储疫苗基础信息 |
| vaccination_site | id, name, address, open_time, total_capacity, available_count, version | 存储接种点信息和剩余名额 |
| appointment | id, user_id, vaccine_id, site_id, appointment_date, time_slot, status, create_time | 存储预约记录 |
| announcement | id, title, content, create_time | 存储公告 |
| vaccine_stock(可能有) | id, vaccine_id, site_id, stock_count | 疫苗批次在接种点的分布库存 |
这里拿vaccination_site表特别注意一下可用名额字段。预约系统最怕的就是库存不对,所以这位作者设计了一个available_count字段,同时很可能还带了一个version字段用于乐观锁,或者至少会在更新时加条件判断,这是整个系统正确性的关键所在。
3.2 名额扣减的并发设计
预约场景下,两个用户同一秒预约最后一个名额,如果代码是“先查剩余数量,再在内存里减一,再写回数据库”,最后很可能出现负数库存或者超卖。这种问题在写代码时不容易被发现,往往要等到上线后被并发请求打爆才会暴露。
这套系统采用的是一个比较经典的方案:更新语句里加条件判断,让数据库在事务层面保证原子性。核心 SQL 长这样:
UPDATE vaccination_site SET available_count = available_count - 1 WHERE id = ? AND available_count > 0;注意最后那个available_count > 0条件。只有库存还有剩余时,这条 UPDATE 才会更新到行,返回的影响行数为 1;如果已经没有名额,影响行数就是 0,服务端据此判断预约失败并返回“名额不足”。这种方式在低并发场景下非常简单可靠,不需要引入分布式锁,也不容易出现死锁。
在 Java 代码里,对应的逻辑要放在一个事务方法中,通常用@Transactional注解,确保“检查用户是否已预约”“扣减名额”“插入预约记录”三个操作要么全部成功、要么全部失败。事务一旦回滚,名额和预约记录都会恢复到操作前状态。
3.3 预约状态机设计
一个完整的预约状态流转是有规矩的,不能随便从“已预约”直接跳到“已接种”。一般会定义这几个状态:
- 待接种:预约成功,等用户到场。
- 已取消:用户主动取消,名额已经释放。
- 已接种:管理员确认用户已完成接种。
- 已过期:预约时间段已经过去,用户没有到场,系统定时任务自动处理。
状态流转的核心规则是:从待接种可以到已取消、已接种、已过期;从已取消就不能再改;从已过期也不能被确认接种。代码里用 switch 或者简单的状态判断就能实现。这个小设计看起来不起眼,但是能有效防止管理员误操作把已取消的预约改成已接种,或者把过期的预约又恢复成待接种。
取消预约时别忘了同步恢复名额。这个操作要放在同一个事务里,否则会出现用户取消成功但名额没回来的问题。我见过不少二手代码在这块偷懒,预约记录状态改了,库存却纹丝不动,最后管理员只能手动去数据库里改字段,非常痛苦。
4. 后端核心实现与关键代码解析
4.1 工程结构与启动入口
后端项目的包结构大概是这样的:
src/main/java ├── com.example.vaccine │ ├── VaccineApplication.java // 启动类 │ ├── config/ // 配置类(CORS、拦截器、定时任务) │ ├── controller/ // 控制层(AuthController、VaccineController、AppointmentController、AdminController) │ ├── service/ // 业务层 │ │ ├── impl/ │ ├── mapper/ 或 repository/ // 数据访问层 │ ├── entity/ // 实体类 │ ├── dto/ // 请求和响应对象 │ └── util/ // 工具类(JwtUtil、Result封装)启动类是一个标准的 SpringBoot 入口,内容非常简单:
@SpringBootApplication @MapperScan("com.example.vaccine.mapper") public class VaccineApplication { public static void main(String[] args) { SpringApplication.run(VaccineApplication.class, args); } }如果你用的是源码压缩包,第一件事就是确认pom.xml里的 SpringBoot 版本和你本机的 JDK 是否匹配。比如 SpringBoot 2.7 支持 JDK 8 和 11,SpringBoot 3.x 则强制要求 JDK 17。这点不匹配,编译必报错,而且是那种一眼看不懂的抽象错误。
4.2 认证与权限设计
系统的登录认证用的是 JWT 方案,这是一个相对轻量的有状态替代。用户登录成功后,服务端生成一个带有效期的 token 返回给前端,前端存在 localStorage 里,之后每次请求在请求头带上Authorization: Bearer <token>。后端通过拦截器统一校验 token,解析出用户 ID 和角色,然后放进 ThreadLocal 或请求上下文里供业务方法使用。
拦截器的核心逻辑大致如下:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equals(request.getMethod())) { return true; // 放行跨域预检请求 } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); if (claims != null) { request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } } response.setStatus(401); return false; } }角色权限的判断一般在 Controller 层用注解或方法内判断,比如if (!"ADMIN".equals(role)) { return Result.error("无权限"); }。如果项目规模再大一点,可以引入 Spring Security 或 Shiro,但当前场景下,一个拦截器加角色判断完全够用,还省掉了一堆安全框架的配置成本。
这里特别提醒一下:JWT 的密钥千万不要硬编码在代码里还提交到公开仓库,至少要放到application.yml配置里。虽然这是个教学项目,但养成习惯很重要。另外 token 过期时间别设太长,7 天以内比较稳妥。
4.3 预约接口的业务编排
预约功能是整个系统最核心的接口。代码层面处理流程如下:
- 从 token 里拿到用户 ID,查询用户状态是否正常。
- 检查该用户当前是否已有待接种的预约,防止重复预约。
- 根据前端提交的 vaccineId、siteId、预约日期和时间段,校验疫苗和接种点是否存在且可用。
- 执行 UPDATE 扣减名额,判断影响行数。
- 插入预约记录,初始状态为“待接种”。
- 所有步骤在
@Transactional中完成,任何一步失败都整体回滚。
这段逻辑对应的 Service 方法大致长这样:
@Transactional(rollbackFor = Exception.class) public Result createAppointment(AppointmentRequest req, Long userId) { // 1. 校验用户 User user = userMapper.selectById(userId); if (user == null || user.getStatus() == 0) { return Result.error("用户不存在或已被禁用"); } // 2. 防止重复预约 int count = appointmentMapper.countPendingByUserId(userId); if (count > 0) { return Result.error("您已有待接种的预约"); } // 3. 校验接种点与库存,条件更新 int rows = siteMapper.decreaseAvailableCount(req.getSiteId()); if (rows == 0) { return Result.error("该接种点暂无剩余名额"); } // 4. 插入预约记录 Appointment appointment = new Appointment(); appointment.setUserId(userId); appointment.setVaccineId(req.getVaccineId()); appointment.setSiteId(req.getSiteId()); appointment.setAppointmentDate(req.getAppointmentDate()); appointment.setTimeSlot(req.getTimeSlot()); appointment.setStatus("PENDING"); appointmentMapper.insert(appointment); return Result.success("预约成功"); }注意decreaseAvailableCount这个方法的 SQL 就是前面说的条件更新。这一段代码也是最容易被面试官追问的地方,能讲清楚“为什么这里要用条件更新而不是先查后减”,说明你是真的理解并发场景,而不是只会写 CRUD。
4.4 定时任务自动过期处理
预约系统还有一个绕不开的场景:约了不来。如果系统不处理这种“占着名额不过期”的记录,一段时间后库存会被大量无效预约耗尽。解决办法是用 Spring 自带的@Scheduled定时任务,每天凌晨或者每小时扫描一次超过预约日期但状态还是“待接种”的记录,改成“已过期”,同时恢复对应名额。
@Component public class ExpireTask { @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次 public void expireAppointments() { List<Appointment> pendingList = appointmentMapper.selectExpiredPending(); for (Appointment appointment : pendingList) { appointment.setStatus("EXPIRED"); appointmentMapper.updateById(appointment); siteMapper.increaseAvailableCount(appointment.getSiteId()); } } }这里的坑在于判断“过期”的时间标准。预约日期是当天,那么只要当前时间晚于预约时间段结束时间,就视为过期。写成 SQL 时要注意时区问题,now()和 Java 的LocalDateTime.now()必须保证在同一时区,否则会出现刚预约就被过期的诡异 bug。上线前可以用一条模拟数据手动测试一下这个任务。
5. 前端核心实现与交互细节
5.1 前端项目结构与路由
前端采用 Vue CLI 创建的单页应用,目录结构一般长这样:
src ├── api/ // axios 接口封装 │ ├── auth.js │ ├── vaccine.js │ ├── appointment.js ├── assets/ ├── components/ // 公共组件 ├── router/index.js // 路由配置 ├── views/ // 页面组件 │ ├── Login.vue │ ├── Register.vue │ ├── VaccineList.vue │ ├── Appointment.vue │ ├── MyAppointment.vue │ ├── admin/ │ │ ├── AdminHome.vue │ │ ├── VaccineManage.vue │ │ ├── SiteManage.vue │ │ ├── AppointmentManage.vue ├── App.vue └── main.js路由配置里需要做权限控制。简单做法是在路由元信息里加requiresAuth和requiresAdmin,然后在全局前置守卫里判断 token 和角色:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.requiresAdmin && localStorage.getItem('role') !== 'ADMIN') { next('/'); } else { next(); } });这个守卫写得好不好,直接决定系统是不是“裸奔”状态。很多管理系统后端接口保护得不错,但前端路由却不做拦截,结果普通用户直接改 URL 就能访问管理后台页面,虽然接口会拒绝,但体验和安全性都很糟糕。
5.2 axios 请求封装与统一错误处理
axios 封装是前端项目里最值得复用的代码。源码里通常有一个request.js,做几件事:设置 baseURL、请求拦截器里加 token、响应拦截器里统一处理错误码。
import axios from 'axios'; import { Message } from 'element-ui'; import router from '@/router'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.clear(); router.push('/login'); Message.error('登录已过期,请重新登录'); } else { Message.error(error.response?.data?.message || '网络异常'); } return Promise.reject(error); } ); export default request;做了这个统一处理,业务代码里就不需要到处写try catch和弹窗提示了。比如预约接口返回Result.error("名额不足"),前端只需要判断res.code !== 200时提示一下即可。这种风格的项目代码看起来干净许多,也更容易维护。
5.3 预约表单的联动与防重复提交
预约页面可能是整个前端交互最复杂的一页,逻辑链路是:选择疫苗 → 选择接种点 → 选择日期和时间段 → 看到剩余名额 → 提交预约。
前端要做几件关键事情:
- 当用户选择疫苗和接种点后,前端根据
siteId调接口查询剩余名额并展示。 - 提交按钮在名额为 0 或用户未完整选择时保持禁用。
- 提交期间按钮进入 loading 状态,防止用户连点产生重复请求。
一个比较实用的提交逻辑片段:
async function submitAppointment() { if (submitting.value) return; submitting.value = true; try { const res = await createAppointment(form.value); if (res.code === 200) { Message.success('预约成功'); router.push('/my-appointment'); } else { Message.error(res.message); } } finally { submitting.value = false; } }这里的submitting变量是防重复提交的关键。就算用户双击按钮,第二次进入函数直接 return,不会发出重复请求。这个细节在预约场景里非常重要,否则一次点击发出两个请求,就会出现一个用户两条预约记录的情况。
5.4 前端跨域与开发环境代理
前后端分离项目,本地联调最容易遇到的就是跨域问题。前端跑在http://localhost:8080,后端跑在http://localhost:8081,端口不同即跨域。解决办法有两个方向:后端加 CORS 配置,前端配置开发代理。
后端加 CORS 配置是常见做法。一个简单的写法:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }前端也可以在vue.config.js里配代理,这样前端代码里写的请求地址都是/api/xxx,由开发服务器转发到后端:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };两种方案一起用其实也可以,但要注意allowCredentials(true)和allowedOriginPatterns("*")的搭配,SpringBoot 2.4 之后对allowedOrigins("*")加了不少限制,用allowedOriginPatterns更省心。
6. 本地运行全流程实录
6.1 环境准备清单
这部分说多了都是泪。真正做到“就像跑起来”的用户,基本都是环境预先配好了的。我整理一份建议的环境版本,避免版本冲突:
| 软件 | 推荐版本 | 检查命令 |
|---|---|---|
| JDK | 1.8 或 11(对应 SpringBoot 2.x) | java -version |
| Maven | 3.6.x 或 3.8.x | mvn -v |
| Node.js | 14.x 或 16.x | node -v |
| npm | 随 Node 版本 | npm -v |
| MySQL | 5.7 或 8.0 | mysql --version |
| IDE | IDEA 或 VSCode | — |
如果你下载的源码是较新的 SpringBoot 3.x,那 JDK 必须 17 以上,Node 版本建议 16 以上。版本不匹配是最常见的启动失败原因,没有之一。
6.2 后端启动步骤
先建数据库。打开 MySQL 命令行或 Navicat,执行数据库脚本,通常是一个vaccine.sql或db.sql文件,里面建了库和表,还插入了初始数据。执行完成后确认一下表是否都已创建。
然后打开src/main/resources/application.yml,修改数据库连接:
server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/vaccine_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的数据库密码启动方式有两种:在 IDEA 里直接运行VaccineApplication.java,或在项目根目录执行:
mvn spring-boot:run看到日志输出Started VaccineApplication就说明后端起来了。为了验证接口是否可用,可以在浏览器访问http://localhost:8081/api/vaccine/list,如果返回 JSON 数据就说明数据库连接正常。
6.3 前端启动步骤
前端启动最怕的就是 npm 依赖安装失败。进到前端项目目录,先安装依赖:
npm install国内用户如果下载慢或者卡住,建议先切换 npm 镜像源:
npm config set registry https://registry.npmmirror.com安装完成后启动开发服务器:
npm run serve看到App running at: http://localhost:8080就成功了。浏览器访问这个地址,用初始化账号登录。初始账号一般会在数据库脚本或 README 里注明,比如admin / 123456和user / 123456。
6.4 直接运行常见的小坑
源码“可直接运行”不代表完全没有坑,总结几个高频问题:
- MySQL 连不上:90% 是因为
application.yml里的密码没改成自己本机的密码,或者没有启动 MySQL 服务。 - 数据库驱动版本不匹配:MySQL 5.7 用
com.mysql.jdbc.Driver或com.mysql.cj.jdbc.Driver都行,但 MySQL 8.0 必须用后者,否则会报 ClassNotFound。 - Node 版本过高:Vue 2 项目在老版本依赖下,Node 17 以上可能会报
opensslErrorStack。解决办法是降 Node 版本,或者在启动命令里加NODE_OPTIONS=--openssl-legacy-provider。 - 前端请求接口 404:确认后端端口是否真的在
8081,以及前端代理或 axiosbaseURL是否匹配。 - 端口被占用:后端端口被占用时改
application.yml里的server.port,前端端口被占用时可以执行npm run serve -- --port 8082临时换端口。
7. 常见问题与排查技巧实录
7.1 启动期的报错速查表
| 现象 | 常见原因 | 处理方法 |
|---|---|---|
Port 8081 was already in use | 端口被占用 | 找到占用进程杀掉,或改后端端口 |
Access denied for user 'root'@'localhost' | 数据库密码错误或权限不足 | 核对application.yml的密码 |
Unknown database 'vaccine_db' | 没有执行 SQL 脚本建库 | 重新执行建库脚本 |
Failed to bind properties | 配置项名称写错 | 对照官方配置检查缩进和单词拼写 |
npm install 卡住不动 | 网络原因 | 切换镜像源重试 |
Cannot find module 'node-sass' | 依赖安装不完整 | 删除 node_modules,重新 install |
| 前端页面能开但接口全 404 | 代理配置错误或后端没启动 | 分别检查后端能否访问、代理转发是否有效 |
排查这类问题有个笨办法但很有效:先把后端接口通过浏览器或 Postman 直接调用,确认后端没问题之后,再去查前端代理,这样能把上下两层问题隔离开,不至于瞎猜。
7.2 业务逻辑层面的隐蔽 Bug
相比启动问题,业务逻辑的坑更隐蔽,编译期不会报错,运行期也不会崩溃,但数据会慢慢乱掉。重点提醒几个:
- 库存为负:如果扣减名额没有加
available_count > 0条件,高并发下必然出现负数。检查siteMapper里的 SQL。 - 重复预约:如果创建预约时没有检查用户已有待接种记录,同一个用户可以“开挂”约好几条。需要确认
createAppointment方法里是否有查询校验。 - 取消预约名额不恢复:
cancelAppointment方法必须同时做更新预约状态和恢复库存。如果用了事务,要确认两个操作在同一事务里。 - 日期比较时区 bug:数据库的
datetime和 Java 的LocalDateTime如果时区不一致,会出现“当天预约被判定为过期”之类的问题。连接 URL 里加serverTimezone=Asia/Shanghai可以缓解。
调试这类问题,我的习惯是在 Service 里加关键日志,比如扣减影响行数、预约记录 ID、状态变化前后的值。日志是定位业务 bug 的第一利器,别老想着单步调试,分布式环境下日志比断点好用得多。
7.3 代码可优化与扩展的方向
作为毕设,如果想让项目看起来更“有水平”,在现有源码基础上做这几个扩展会很加分:
- 验证码登录:接入图形验证码或短信验证码,提升系统真实感。
- 疫苗批次追溯:在疫苗表基础上增加批次入库记录和出库记录,做到来可溯、去可查。
- 预约放量策略:不同时间段设置不同放量名额,而不是所有时段一个值。
- 统计报表:用 ECharts 画出每日预约人数、疫苗种类预约占比、接种点负荷分析,充分展示前后端能力。
- 微信小程序端:如果有余力,复用后端接口,写一个小程序预约端,直接让项目的完整度上一个大台阶。
个人体会是,这套项目的价值不在于代码量,而在于它覆盖了“预约资源”这种通用业务的核心难点。把库存、状态机、事务、权限这些点学透,后面无论换什么业务(会议室预约、门诊挂号、图书馆座位),都能平移过去。
最后再分享一个小技巧,如果你拿到源码后第一时间不是去启动,而是先读数据库脚本,把每个表的字段和注释过一遍,再对着启动后的页面操作一遍,你会对一个陌生项目的理解速度快很多。我每次接手别人的项目都是这个习惯,十次有九次都比直接看代码更高效。