ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的疫苗预约管理系统全栈开发实战

基于SpringBoot+Vue的疫苗预约管理系统全栈开发实战 简介这是一套基于SpringBoot与Vue前后端分离架构的疫苗接种管理系统完整毕设源码面向计算机相关专业毕业生、课程设计学生及Java实战学习者旨在解决接种人群信息实时跟踪、批次统计与接种记录管理等核心需求。压缩包共815个文件大小约53.62MB内容涵盖java后端逻辑、vue前端页面、SQL数据库脚本、maven配置文件、开发说明文档、演示视频及批量启动脚本等文件类型均衡便于直接导入IDE运行调试。目前已有147人学习下载。项目经过严格调试可直接作为毕业设计底座使用配套的部署说明、系统介绍与代码注释能帮助快速理解前后端交互流程数据库脚本和工具脚本则简化了环境搭建尤其适合需要短时间完成可演示系统的学习者。1. 疫苗预约管理系统全栈 CRUD 之外的边界问题接种管理这类系统无论在课程设计还是外包交付里出现频率都很高。它表面上是典型的前后端分离 CRUD疫苗目录、接种点、预约单、接种记录四个基本页面配四组接口。但真正决定这个项目能不能跑起来、答辩时能不能讲清楚的是预约与库存之间的联动、接种记录的状态流转、以及前后端联调时接口契约的一致性。SpringBoot 负责把业务规则收口在 Service 层Vue 负责把复杂状态呈现成可操作的界面两者凑在一起正好覆盖了一套小体量业务系统从设计到交付的完整闭环。围绕这套系统可复现的落地路径包括用 MyBatis-Plus 快速搭出数据访问层用事务和乐观锁保证预约扣库存不超卖前端用 Vue Router 做权限控制配合 JWT 完成登录态管理最后用 Nginx 挂载前端产物、用java -jar启动后端进程。下面的内容按 后端接口设计 → 前端工程化 → 数据库鉴权 → 部署验证 的顺序展开每一步都会给可直接抄进 IDE 的代码块和参数说明。2. 基于SpringBoot的接种管理后端实体、接口与预约事务2.1 核心实体与表结构映射后端的第一件事不是写 Controller而是确定领域模型。疫苗接种管理系统的核心对象有五个疫苗Vaccine、疫苗批次VaccineBatch、接种点VaccinationSite、预约单Appointment、接种记录VaccinationRecord。再加上用户User和角色Role构成一套标准的 RBAC 结构。使用 MyBatis-Plus 而非 JPA 的原因很实际国内这类交付项目大多要求 SQL 可控批量更新和复杂统计比如按周统计接种量写 XML 更直接。实体类上用TableName和TableId映射表名与主键逻辑删除用TableLogic标注deleted字段避免物理删除破坏统计数据的连续性。Data TableName(vaccine_batch) public class VaccineBatch { TableId(type IdType.AUTO) private Long id; private Long vaccineId; private Long siteId; private String batchNo; private Integer totalStock; private Integer remainStock; Version private Integer version; TableLogic private Integer deleted; }代码中Version是 MyBatis-Plus 乐观锁注解需要在配置类里注册OptimisticLockerInnerInterceptor才生效。配合remainStock字段预约扣减库存时用UPDATE ... SET remain_stock remain_stock - 1 WHERE remain_stock 0从根上避免超卖。deleted字段用于逻辑删除查询时 MyBatis-Plus 自动追加deleted 0条件不需要在每个 Mapper 里手动写。2.2 预约接口的并发与事务实现预约是系统的核心操作因为用户选择了某个接种点和疫苗批次之后系统需要同时完成“创建预约单 扣减库存 检查该用户当天是否重复预约”三个动作。这三个动作必须在一个事务里完成否则会出现预约单创建成功但库存没扣、或者库存扣了但预约单查不到的情况。Controller 层只做参数绑定和返回封装事务边界必须设置在 Service 层。下面是一段核心实现Transactional(rollbackFor Exception.class) public Appointment createAppointment(AppointmentCreateDTO dto) { // 1. 校验同一用户同一接种点当天是否已有预约 LocalDateTime start LocalDate.now().atStartOfDay(); LocalDateTime end start.plusDays(1); Long count appointmentMapper.selectCount(new LambdaQueryWrapperAppointment() .eq(Appointment::getUserId, dto.getUserId()) .eq(Appointment::getSiteId, dto.getSiteId()) .between(Appointment::getAppointmentTime, start, end)); if (count 0) { throw new BusinessException(同一接种点当天只能预约一次); } // 2. 乐观锁扣减库存返回 0 表示库存不足或批次已被修改 int updated vaccineBatchMapper.deductStock(dto.getBatchId()); if (updated 0) { throw new BusinessException(该批次疫苗库存不足请选择其他批次); } // 3. 创建预约单状态为 PENDING Appointment appointment new Appointment(); BeanUtils.copyProperties(dto, appointment); appointment.setStatus(PENDING); appointmentMapper.insert(appointment); return appointment; }对应的 Mapper 方法update iddeductStock UPDATE vaccine_batch SET remain_stock remain_stock - 1, version version 1 WHERE id #{batchId} AND remain_stock 0 /updateTransactional保证了步骤 2 和 3 要么同时成功要么同时回滚。乐观锁配合remain_stock 0条件在并发量不大的接种管理系统里已经足够。这里的BusinessException是自定义运行时异常被全局RestControllerAdvice捕获后统一转换为{ code: 50001, message: xxx }格式返回前端。需要注意的一个细节是不要把deductStock写成先select在 Java 里判断库存再update。两步操作之间存在时间窗口多个请求同时读到库存为 1 时都会进入 update 分支。直接用 SQL 条件更新是最简单可靠的并发控制方式。2.3 管理端接口清单与统一返回体后端接口按角色划分成两类面向 C 端用户的预约查询与提交接口面向管理员的疫苗库存维护、接种记录登记、统计报表接口。实际交付中接口路径设计有固定套路/api前缀后跟资源名版本号用/api/v1表示。方法路径功能角色POST/api/v1/auth/login登录获取 token公开GET/api/v1/appointment/page分页查询个人预约记录USERPOST/api/v1/appointment创建预约USERPOST/api/v1/appointment/confirm确认接种完成ADMINPUT/api/v1/vaccine/stock修改批次库存ADMINGET/api/v1/report/weekly周接种量统计ADMIN统一返回体ResultT包含code、message、data三个字段。code 200表示成功code 50001表示业务校验失败code 401表示未登录或 token 过期。前端 axios 拦截器只需要判断code就能做统一错误提示不用在每个页面里处理 HTTP 状态码。这种设计的好处是前端拿到401时可以直接清空本地 token 并router.push(/login)后端也不需要为了前端跳转而特地去设置 HTTP 状态码。项目源码里如果看到Result.success(data)和Result.fail(code, message)的静态方法直接复用即可。3. 基于Vue的接种管理前端工程搭建与权限路由3.1 Vite 工程化初始化与代理配置前端的推荐技术栈是 Vue 3 Vite Vue Router 4 Pinia。Vite 的开发服务器启动速度和热更新体验都比 Vue CLI 好新交付的项目已经很少用 webpack。如果手里拿到的是 Vue 2 的老项目也能按同样思路处理只是路由守卫的写法略有差异。初始化命令按官方模板执行npm create vitelatest vaccination-web -- --template vue cd vaccination-web npm install npm install vue-router4 pinia axios element-plus npm run dev--template vue生成的是 Vue 3 组合式 API 模板。如果机器上 Node 版本低于 18建议先升级再执行Vite 5 要求 Node 18这个和 SpringBoot 3.x 要求 JDK 17 是同一个版本门槛逻辑。开发环境联调时需要在vite.config.js里配置代理把/api开头的请求转发到后端服务。export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })changeOrigin: true会把请求头里的Host改为目标地址。如果不加这个配置后端如果做了端口或域名校验就会出现跨域之外的诡异问题。这个代理只在开发环境生效生产环境由 Nginx 配置location /api { proxy_pass ... }实现同样效果。3.2 axios 实例封装与拦截器项目里每个页面直接import axios from axios是典型的反面教材问题集中在三处1token 需要在每个请求里手动加容易漏2错误处理代码重复3后端返回统一结构后每个页面都要解包response.data.data。正确做法是抽出一个 request 模块。import axios from axios const request axios.create({ baseURL: /api/v1, 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 { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) }, error { return Promise.reject(error) } ) export default requestbaseURL写成/api/v1配合 Vite proxy 里的/api前缀开发环境不需要关心后端完整地址。生产环境下如果后端接口路径变了只需要在 Nginx 里调整一条proxy_pass规则前端代码不用改。timeout: 10000是请求超时时间单位毫秒10 秒适用于大多数管理端接口接种记录批量提交这种慢接口可以单独覆盖配置。调用时API 层和页面层分离// src/api/appointment.js export function createAppointment(data) { return request.post(/appointment, data) } // 页面中调用 const res await createAppointment(formData) if (res.code 200) { ElMessage.success(预约成功) }页面里通过res.code判断业务成功不直接拿到后端数据后就渲染避免后端返回{ code: 50001 }时页面出现空数据闪动。3.3 路由守卫与角色权限控制管理系统必然有用户端和管理员端的页面差异。用户端能看到“我的预约”和“疫苗目录”管理员端能看到“库存管理”和“统计报表”。前端路由守卫的作用是用户未登录时只能进登录页角色不匹配时提示无权限。// src/router/index.js const routes [ { path: /login, component: Login }, { path: /admin, component: AdminLayout, meta: { roles: [ADMIN] }, children: [ { path: stock, component: StockManage }, { path: report, component: WeeklyReport } ] } ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.roles !to.meta.roles.includes(userInfo.role)) { next(/403) return } next() })路由守卫必须配合后端鉴权一起使用不能作为安全边界因为前端代码完全暴露在浏览器里用户可以通过修改 localStorage 伪造角色。但如果没有前端路由守卫管理员页面 URL 直接输入就能打开交互上很难看。这里的meta.roles数组可以匹配多个角色比如某个统计页面同时允许 ADMIN 和 DOCTOR 访问时就写成roles: [ADMIN, DOCTOR]。角色信息建议在登录成功后和后端返回的 userInfo 一起存在 localStorage 里路由守卫每次跳转都从中读取。Vue DevTools 的 Vuex/Pinia 面板里可以直接查看当前用户状态调试角色切换问题时非常方便。4. 数据库设计预约状态流转与JWT鉴权串联4.1 五张核心业务表与索引策略数据库脚本是源码包里最容易被忽视、但实际交付时最能体现质量的产物。设计原则很简单按照业务对象拆表不做大宽表预约单和接种记录分开存随时可以从预约单追溯到疫苗批次和接种点。下面是核心表的建表脚本节选CREATE TABLE appointment ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, batch_id bigint(20) NOT NULL COMMENT 疫苗批次ID, site_id bigint(20) NOT NULL COMMENT 接种点ID, appointment_time datetime NOT NULL COMMENT 预约接种时间, status varchar(20) NOT NULL DEFAULT PENDING COMMENT PENDING/COMPLETED/CANCELLED, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_user_time (user_id, appointment_time), KEY idx_site_status (site_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约单表; CREATE TABLE vaccine_batch ( id bigint(20) NOT NULL AUTO_INCREMENT, vaccine_id bigint(20) NOT NULL COMMENT 疫苗ID, site_id bigint(20) NOT NULL COMMENT 接种点ID, batch_no varchar(64) NOT NULL COMMENT 批次号, total_stock int(11) NOT NULL COMMENT 入库总量, remain_stock int(11) NOT NULL COMMENT 剩余库存, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, deleted tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_batch_no (batch_no), KEY idx_site_vaccine (site_id, vaccine_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫苗批次表;字段类型上有两个常见坑需要避开。第一是金额和数量相关字段如果用 Java 的int对应数据库的INT库存扣减接近边界时会出现溢出本系统用INT够用但通用做法是DECIMAL或BIGINT。第二是时间字段统一用datetime连接串里加serverTimezoneAsia/Shanghai否则 Java 8 的LocalDateTime和 MySQL 会话时区不一致预约时间会偏移 8 小时。预约表里的idx_user_time覆盖“当天重复预约校验”的查询条件idx_site_status支撑管理员按接种点和状态筛选预约单。两张表都用utf8mb4字符集避免疫苗名称里可能出现的不常见汉字或特殊符号在utf8下报错。数据库初始化时建议在项目根目录放一个sql/init.sql包含CREATE DATABASE、建表语句和一份最小测试数据让拿到源码的人一条命令导入即可运行。4.2 基于JWT的登录鉴权链登录接口校验用户名密码通过后生成 JWT 返回前端。JWT 的优势是后端无状态不需要维护 Session前端拿到 token 后每次请求放在Authorization请求头里后端拦截器解析校验。使用 jjwt 库实现public String generateToken(User user) { return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireInMillis)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); }配置项里secretKey用至少 32 字节的随机字符串不要用短单词expireInMillis根据业务节奏设置管理端可以设 12 小时用户端建议 2 小时。过期时间写到application.yml里不硬编码在 Java 代码中后面调整时不需重新编译。拦截器校验 token 时有一个 Spring Boot 版本差异要格外注意。Spring Boot 2.7 及以下版本用WebMvcConfigurer.addInterceptors就能注册拦截器如果项目升级到了 Spring Boot 3.x或者说“springboot 版本太高”需要引入spring-boot-starter-web的新 Jakarta 命名空间拦截器注册方式基本不变但javax.servlet要换成jakarta.servlet。同时Spring Security 6 的配置写法从WebSecurityConfigurerAdapter换成了SecurityFilterChainBean网上很多教程用的是旧 API直接复制会编不过。注册拦截器并放行登录和静态资源Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/v1/auth/login, /error); }addPathPatterns(/api/**)覆盖所有后端接口excludePathPatterns放行登录接口。前端每次请求都会经过拦截器解析 token解析失败时抛出运行时异常由全局异常处理器转为 401 返回。4.3 预约-接种-库存的流水闭环预约单状态流转是一个简单状态机PENDING待接种→COMPLETED已完成/CANCELLED已取消。管理端把某个预约单标记为已完成时业务上等价于“该批次疫苗被消耗掉”但因为库存已经在用户创建预约时扣过了确认接种时不需要再次扣库存。反过来用户取消预约时需要把预约单状态改为CANCELLED并将对应批次的remain_stock加回。这个取消操作同样需要事务Transactional(rollbackFor Exception.class) public void cancelAppointment(Long appointmentId) { Appointment appointment appointmentMapper.selectById(appointmentId); if (!PENDING.equals(appointment.getStatus())) { throw new BusinessException(当前状态不可取消); } appointment.setStatus(CANCELLED); appointmentMapper.updateById(appointment); vaccineBatchMapper.restoreStock(appointment.getBatchId()); }restoreStock是remain_stock remain_stock 1的 SQL 更新。这里不校验原预约状态对应的版本号因为取消操作只针对当前已存在的预约单库存扣减在当时已经发生回调库存只是一个逆操作。提供一张状态表方便前后端对齐状态值含义允许的操作影响PENDING待接种取消、确认完成占用库存COMPLETED已完成无不占用库存CANCELLED已取消无释放库存5. 联调与部署从本地运行到“可交付”的验证清单5.1 前端 build 与产物部署前端开发完不是把npm run dev保持运行就算交付而是要产出静态文件交给 Nginx。执行构建命令npm run build ls dist/构建产物是dist/目录下的index.html、assets/中的 JS/CSS 文件。部署时把dist内容复制到 Nginx 的html目录并配置反向代理转发后端请求server { listen 80; server_name localhost; root /usr/share/nginx/html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html是 Vue Router 使用 history 模式时的关键配置作用是将所有不存在的文件路径回退到index.html由前端路由接管页面渲染。如果不加这一行用户直接访问http://localhost/admin刷新时会返回 404。使用 hash 模式可以绕开这个配置但 URL 里会多一个#不太美观。5.2 后端启动与接口联调验证后端工程导入 IDEA 后先检查application.yml里的数据库账号密码、端口号、JWT 密钥三个配置项。连接串格式建议加上时区和编码参数spring: datasource: url: jdbc:mysql://localhost:3306/vaccination?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8本地启动后端后用 curl 验证登录、鉴权、数据查询三个链路curl -X POST http://localhost:8080/api/v1/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} curl http://localhost:8080/api/v1/appointment/page?pageNum1pageSize10 \ -H Authorization: Bearer 返回的token第一条命令返回的 JSON 里应该有data.token字段第二条命令的响应是一个分页对象包含records、total、pages等字段这就是前端表格组件的直接数据来源。如果第二条命令返回 401优先检查 token 是否复制完整或者拦截器里的secretKey和生成 token 时用的是不是同一个配置项。5.3 三个必调参数与常见误用从实际交付经验看这套系统的配置参数有 3 个最容易出问题。JWT 过期时间用户端设置太短会导致用户频繁重新登录建议 2 小时管理端可以更长。文件上传大小如果系统要支持上传接种凭证图片需要在application.yml里设置spring.servlet.multipart.max-file-size: 10MB不设置时 SpringBoot 默认只允许 1MB。数据库连接池默认 HikariCP 足够但要注意最大连接数不要超过数据库上限单人开发场景保持默认即可。常见误用场景集中在两个地方。第一是使用Autowired注入 Mapper 后直接在线程池或异步方法里调用导致事务失效正确做法是把异步操作单独拆分到独立 Service并标注Transactional。第二是在前端使用setInterval轮询预约状态如果后端没有对应的长轮询或 WebSocket 接口这种实现会频繁打满接口流量简单场景建议在用户操作后手动刷新列表。最后在代码交付前执行一次全量验证后端mvn clean package -DskipTests打包生成target/vaccination-system.jar连同sql/init.sql、前端dist/目录打成一个包。用java -jar vaccination-system.jar --spring.profiles.activeprod启动再检查一遍登录、预约、取消、管理员确认接种四条主链路数据库里的库存和预约状态实时联动系统就可以作为完整交付物移交了。本文还有配套的精品资源点击获取
返回列表